admin/features/events/eventEditPolicy.ts

101 lines
2.6 KiB
TypeScript

/**
* Mirrors backend `event-edit-policy.ts` for host/admin edit UI.
* Keep in sync when changing the lock matrix.
*/
export const EVENT_SENSITIVE_FIELDS = [
'startsAt',
'endsAt',
'provinceId',
'cityId',
'address',
'lat',
'lng',
'isFree',
'price',
'cancellationFeePercent',
'cancellationFeePercent12To24Hours',
'cancellationFeePercentMoreThan24Hours',
'genderRestriction',
'ageRestriction',
] as const
export type EventSensitiveField = (typeof EVENT_SENSITIVE_FIELDS)[number]
export interface EventEditPolicy {
canEdit: boolean
hasActiveBookings: boolean
minCapacity: number
lockedFields: ReadonlySet<EventSensitiveField>
/** When true, behavioral settings (discoverability, etc.) cannot change. */
lockBehavioralSettings: boolean
}
const TERMINAL = new Set(['cancelled', 'completed'])
const LIVE = new Set(['published', 'full'])
export function isTerminalEventStatus(status: string): boolean {
return TERMINAL.has(status)
}
export function isLiveEventStatus(status: string): boolean {
return LIVE.has(status)
}
/**
* Slug cannot change on terminal events.
* Live slug changes go through pending revisions (applied on admin approve).
*/
export function isEventSlugLocked(status: string): boolean {
return TERMINAL.has(status)
}
/**
* Event is locked for mutations when it reached a terminal status OR its
* scheduled end time has passed (covers early complete vs clock end).
*/
export function isEventEnded(status: string, endsAt: string | Date, now: Date = new Date()): boolean {
if (isTerminalEventStatus(status)) return true
return new Date(endsAt).getTime() <= now.getTime()
}
export function resolveEventEditPolicy(status: string, bookedCount: number, reservedCapacity = 0): EventEditPolicy {
const occupied = bookedCount + reservedCapacity
const minCapacity = Math.max(1, occupied)
if (isTerminalEventStatus(status)) {
return {
canEdit: false,
hasActiveBookings: bookedCount > 0,
minCapacity,
lockedFields: new Set(EVENT_SENSITIVE_FIELDS),
lockBehavioralSettings: true,
}
}
if (status === 'draft' || !LIVE.has(status)) {
return {
canEdit: true,
hasActiveBookings: false,
minCapacity,
lockedFields: new Set(),
lockBehavioralSettings: false,
}
}
const hasActiveBookings = bookedCount > 0
return {
canEdit: true,
hasActiveBookings,
minCapacity,
lockedFields: hasActiveBookings ? new Set(EVENT_SENSITIVE_FIELDS) : new Set(),
lockBehavioralSettings: hasActiveBookings,
}
}
export function isFieldLocked(policy: EventEditPolicy, field: EventSensitiveField): boolean {
return policy.lockedFields.has(field)
}