admin/services/events.ts
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

337 lines
12 KiB
TypeScript

import type {
CreateEventDto,
CreateEventFaqDto,
CreateEventMediaDto,
EventCategoryResponseDto,
EventResponseDto,
OrganizerGuestListResponseDto,
} from '@/api/generated/models'
import { getAdminEvents } from '@/api/generated/admin-events/admin-events'
import { getEventCategories } from '@/api/generated/event-categories/event-categories'
import { getEventExtras } from '@/api/generated/event-extras/event-extras'
import { getMyEvents } from '@/api/generated/my-events/my-events'
import { unwrapApiPayload } from '@/helpers/listResponse'
import axiosInstance from '@/config/axios'
import { API_ROUTES } from '@/services/config'
import { texts } from '@/texts'
import { ANALYTICS_EVENTS, trackAnalyticsEventOnce } from '@/lib/analytics'
export type EventCategory = EventCategoryResponseDto
export type CreateEventPayload = CreateEventDto
export type CreatedEvent = EventResponseDto
export type CreateEventMediaPayload = CreateEventMediaDto
export type CreateEventFaqPayload = CreateEventFaqDto
export type OrganizerGuestList = OrganizerGuestListResponseDto
export interface PreviousAttendee {
userId: string
firstName: string | null
lastName: string | null
}
interface MyHostedEventSummary {
id: string
title: string
slug: string
status: string
startsAt: string
bookedCount: number
reservedCapacity?: number
capacity: number
isFree: boolean
price: number
createdAt: string
adminApprovedAt?: string | null
rejectionReason?: string | null
}
/** The endpoint returns the full event today while this intersection keeps older cached summaries valid. */
export type MyHostedEvent = MyHostedEventSummary & Partial<Omit<EventResponseDto, keyof MyHostedEventSummary>>
const myEventsApi = getMyEvents()
const adminEventsApi = getAdminEvents()
const eventCategoriesApi = getEventCategories()
const eventExtrasApi = getEventExtras()
export async function fetchEventCategories(): Promise<EventCategory[]> {
const response = await eventCategoriesApi.eventCategoriesControllerList()
const payload = unwrapApiPayload(response.data)
return Array.isArray(payload) ? (payload as EventCategory[]) : []
}
export async function createEvent(payload: CreateEventPayload): Promise<CreatedEvent> {
const response = await myEventsApi.myEventsControllerCreate(payload)
const data = unwrapApiPayload<CreatedEvent>(response.data)
if (!data?.id) {
throw new Error(texts.common.invalidServerResponse)
}
trackAnalyticsEventOnce(ANALYTICS_EVENTS.EVENT_CREATED, data.id, {
event_id: data.id,
event_slug: data.slug,
category_id: payload.categoryId,
is_free: payload.isFree,
})
return data
}
// Self-service endpoints (events/:id, events/:id/publish) — ownership-
// checked server-side against the logged-in user. Only works if the
// currently logged-in user is the event's own organizer_id. For an admin
// acting on any organizer's event, use the *AsAdmin variants below instead.
export async function publishEvent(eventId: string): Promise<CreatedEvent> {
const response = await myEventsApi.myEventsControllerPublish(eventId)
const event = unwrapApiPayload<CreatedEvent>(response.data)
trackAnalyticsEventOnce(ANALYTICS_EVENTS.EVENT_PUBLISHED, event.id, {
event_id: event.id,
event_slug: event.slug,
})
return event
}
export async function cancelEvent(eventId: string): Promise<CreatedEvent> {
const response = await myEventsApi.myEventsControllerCancel(eventId)
return unwrapApiPayload<CreatedEvent>(response.data)
}
export async function completeEvent(eventId: string): Promise<CreatedEvent> {
const response = await myEventsApi.myEventsControllerComplete(eventId)
return unwrapApiPayload<CreatedEvent>(response.data)
}
/** Soft-delete when booked_count = 0. */
export async function deleteEvent(eventId: string): Promise<CreatedEvent> {
const response = await axiosInstance.delete(API_ROUTES.EVENTS.DELETE(eventId))
return unwrapApiPayload<CreatedEvent>(response.data)
}
/** Admin approval is one half of the two-key publication workflow. */
export async function approveEventAsAdmin(eventId: string): Promise<CreatedEvent> {
const response = await adminEventsApi.adminEventsControllerApprove(eventId)
return unwrapApiPayload<CreatedEvent>(response.data)
}
/** Force-publish bypass: skips the request/approve flow entirely. */
export async function publishEventAsAdmin(eventId: string): Promise<CreatedEvent> {
const response = await adminEventsApi.adminEventsControllerPublish(eventId)
return unwrapApiPayload<CreatedEvent>(response.data)
}
export async function rejectEventAsAdmin(eventId: string, rejectionReason: string): Promise<CreatedEvent> {
const response = await adminEventsApi.adminEventsControllerReject(eventId, { rejectionReason })
return unwrapApiPayload<CreatedEvent>(response.data)
}
export async function cancelEventAsAdmin(eventId: string): Promise<CreatedEvent> {
const response = await adminEventsApi.adminEventsControllerCancel(eventId)
return unwrapApiPayload<CreatedEvent>(response.data)
}
export async function completeEventAsAdmin(eventId: string): Promise<CreatedEvent> {
const response = await adminEventsApi.adminEventsControllerComplete(eventId)
return unwrapApiPayload<CreatedEvent>(response.data)
}
export async function deleteEventAsAdmin(eventId: string): Promise<CreatedEvent> {
const response = await axiosInstance.delete(API_ROUTES.EVENTS.ADMIN_DELETE(eventId))
return unwrapApiPayload<CreatedEvent>(response.data)
}
export type EventEditAccessMode = 'admin' | 'owner'
export async function updateEvent(eventId: string, payload: Partial<CreateEventPayload>): Promise<CreatedEvent> {
const response = await myEventsApi.myEventsControllerUpdate(eventId, payload)
return unwrapApiPayload<CreatedEvent>(response.data)
}
export async function updateEventReservedCapacity(
eventId: string,
reservedCapacity: number,
accessMode: EventEditAccessMode = 'owner'
): Promise<CreatedEvent> {
const path = accessMode === 'admin' ? API_ROUTES.EVENTS.ADMIN_RESERVED_CAPACITY(eventId) : API_ROUTES.EVENTS.RESERVED_CAPACITY(eventId)
const response = await axiosInstance.patch(path, { reservedCapacity })
return unwrapApiPayload<CreatedEvent>(response.data)
}
/** Admin can edit any organizer's event (same field locks as host). */
export async function updateEventAsAdmin(eventId: string, payload: Partial<CreateEventPayload>): Promise<CreatedEvent> {
const response = await axiosInstance.patch(API_ROUTES.EVENTS.ADMIN_DETAIL(eventId), payload)
return unwrapApiPayload<CreatedEvent>(response.data)
}
/** Admin-only: set (or, with null, clear) this event's platform commission override. */
export async function updateEventCommissionAsAdmin(eventId: string, commissionPercent: number | null): Promise<CreatedEvent> {
const response = await axiosInstance.patch(API_ROUTES.EVENTS.ADMIN_COMMISSION(eventId), { commissionPercent })
return unwrapApiPayload<CreatedEvent>(response.data)
}
/**
* Full event for edit/clone forms. Backend authorizes owner OR admin;
* callers do not pass access mode.
*/
export async function fetchEventForEdit(eventId: string): Promise<CreatedEvent> {
const response = await axiosInstance.get(API_ROUTES.EVENTS.FOR_EDIT(eventId))
return unwrapApiPayload<CreatedEvent>(response.data)
}
/** Admin detail including bookmarkCount + organizer contact. */
export async function fetchAdminEventDetail(eventId: string): Promise<CreatedEvent> {
const response = await axiosInstance.get(API_ROUTES.EVENTS.ADMIN_DETAIL(eventId))
return unwrapApiPayload<CreatedEvent>(response.data)
}
export async function createEventMedia(eventId: string, payload: CreateEventMediaPayload): Promise<void> {
await eventExtrasApi.eventMediaControllerCreate(eventId, payload)
}
export async function updateEventMedia(eventId: string, mediaId: string, payload: Partial<CreateEventMediaPayload>): Promise<void> {
await eventExtrasApi.eventMediaControllerUpdate(eventId, mediaId, payload)
}
export async function deleteEventMedia(eventId: string, mediaId: string): Promise<void> {
await eventExtrasApi.eventMediaControllerRemove(eventId, mediaId)
}
export async function createEventFaq(eventId: string, payload: CreateEventFaqPayload): Promise<void> {
await eventExtrasApi.eventFaqsControllerCreate(eventId, payload)
}
export async function updateEventFaq(eventId: string, faqId: string, payload: Partial<CreateEventFaqPayload>): Promise<void> {
await eventExtrasApi.eventFaqsControllerUpdate(eventId, faqId, payload)
}
export async function deleteEventFaq(eventId: string, faqId: string): Promise<void> {
await eventExtrasApi.eventFaqsControllerRemove(eventId, faqId)
}
export interface EventRevisionPayload {
title?: string
slug?: string
shortDescription?: string
description?: string
categoryId?: number
startsAt?: string
endsAt?: string
provinceId?: number
cityId?: number
address?: string
lat?: number
lng?: number
isFree?: boolean
price?: number
capacity?: number
genderRestriction?: string
ageRestriction?: string
cancellationFeePercent?: number
settings?: CreateEventPayload['settings']
media: CreateEventMediaPayload[]
faqs?: CreateEventFaqPayload[]
}
export interface EventRevision {
id: string
eventId: string
status: string
payload: EventRevisionPayload & Record<string, unknown>
createdBy: string
reviewedBy: string | null
rejectionReason: string | null
submittedAt: string
decidedAt: string | null
}
export async function submitEventRevision(eventId: string, payload: EventRevisionPayload): Promise<EventRevision> {
const response = await axiosInstance.post(API_ROUTES.EVENTS.REVISION_SUBMIT(eventId), payload)
return unwrapApiPayload<EventRevision>(response.data)
}
export async function fetchPendingEventRevision(eventId: string): Promise<EventRevision | null> {
try {
const response = await axiosInstance.get(API_ROUTES.EVENTS.REVISION_PENDING(eventId))
return unwrapApiPayload<EventRevision>(response.data)
} catch (error) {
const status = (error as { response?: { status?: number } })?.response?.status
if (status === 404) return null
throw error
}
}
export async function withdrawPendingEventRevision(eventId: string): Promise<void> {
await axiosInstance.delete(API_ROUTES.EVENTS.REVISION_WITHDRAW(eventId))
}
export async function fetchAdminPendingEventRevision(eventId: string): Promise<EventRevision | null> {
try {
const response = await axiosInstance.get(API_ROUTES.EVENTS.ADMIN_REVISION_PENDING(eventId))
return unwrapApiPayload<EventRevision>(response.data)
} catch (error) {
const status = (error as { response?: { status?: number } })?.response?.status
if (status === 404) return null
throw error
}
}
export async function approveEventRevisionAsAdmin(eventId: string, revisionId: string): Promise<CreatedEvent> {
const response = await axiosInstance.patch(API_ROUTES.EVENTS.ADMIN_REVISION_APPROVE(eventId, revisionId))
return unwrapApiPayload<CreatedEvent>(response.data)
}
export async function rejectEventRevisionAsAdmin(eventId: string, revisionId: string, rejectionReason: string): Promise<EventRevision> {
const response = await axiosInstance.patch(API_ROUTES.EVENTS.ADMIN_REVISION_REJECT(eventId, revisionId), {
rejectionReason,
})
return unwrapApiPayload<EventRevision>(response.data)
}
export async function linkGuestListToEvent(eventId: string, listId: string): Promise<void> {
await eventExtrasApi.eventGuestListLinksControllerLink(eventId, { listId })
}
export async function fetchOrganizerGuestLists(): Promise<OrganizerGuestList[]> {
const response = await eventExtrasApi.organizerGuestListsControllerList({ page: 1, pageSize: 100 })
const payload = unwrapApiPayload(response.data)
const items = Array.isArray(payload.items) ? payload.items : Array.isArray(payload) ? payload : []
return items as OrganizerGuestList[]
}
export async function fetchPreviousAttendees(): Promise<PreviousAttendee[]> {
const response = await axiosInstance.get('/organizer-guest-lists/previous-attendees')
const payload = unwrapApiPayload(response.data)
return Array.isArray(payload) ? (payload as PreviousAttendee[]) : []
}
export async function setPreviousAttendeeGuests(eventId: string, userIds: string[]): Promise<void> {
// Persists the invite set. SMS is sent automatically when the event is published
// (or immediately if it is already published and has not been notified yet).
await axiosInstance.put(`/events/${eventId}/guest-list-links/invited-guests`, { userIds })
}