339 lines
12 KiB
TypeScript
339 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
|
|
cancellationFeePercent12To24Hours?: number
|
|
cancellationFeePercentMoreThan24Hours?: 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 })
|
|
}
|