'use client' import { useEffect, useState } from 'react' import { FormProvider, useForm } from 'react-hook-form' import { useRouter } from 'next/navigation' import dynamic from 'next/dynamic' import { texts } from '@/texts' import type { AdminCreateEventPayload } from '@/services/events' import Button from '@/components/formElements/Button' import Input from '@/components/formElements/Input' import CategoryTreeSelect from '@/components/events/create/CategoryTreeSelect' import EventMediaGalleryUploader from '@/components/events/create/EventMediaGalleryUploader' import FaqEditor from '@/components/events/create/FaqEditor' import MapLocationPicker from '@/components/events/create/MapLocationPicker' import { WizardFormFullWidth, WizardFormGrid } from '@/components/events/create/WizardFormLayout' import type { FaqItem, StagedMediaItem } from '@/components/events/create/types' import { combineDateAndMinutes, hasRequiredPosters } from '@/components/events/create/types' import { DetailSkeleton } from '@/components/feedback/LoadingState' import InlineNotice from '@/components/feedback/InlineNotice' import { AdminFormActions, AdminFormSection } from '@/components/forms/AdminFormLayout' import { useEventScheduleConstraints } from '@/components/events/schedule/eventScheduleConstraints' import VerifiedHostPicker, { type VerifiedHostOption } from '@/features/events/create/VerifiedHostPicker' import { slugifyEventTitle, shouldAutogenerateEventSlug } from '@/features/events/eventSlug' import { createEventAsAdmin, createEventFaq, fetchEventCategories, type EventCategory } from '@/services/events' import { GET_ADMIN_USER_DETAIL } from '@/services/adminUserDetail' import { fetchAllCities, type City } from '@/services/geography' import { extractServerErrorDetail } from '@/services/errorHandler' import { addToast } from '@/lib/toast' import { showFormValidationToast } from '@/lib/formValidationToast' import { AGE_RESTRICTION_OPTIONS, GENDER_RESTRICTION_OPTIONS } from '@/lib/eventAudience' const TextEditor = dynamic(() => import('@/components/formElements/TextEditor'), { ssr: false }) interface EventCreateFormValues { title: string slug: string categoryId: string shortDescription: string description: string startDate: string startTime: number endDate: string endTime: number cityId: string address: string lat: number lng: number isFree: boolean price: number capacity: number reservedCapacity: number genderRestriction: 'open' | 'female_only' | 'male_only' ageRestriction: 'open' | 'age_15_19' | 'age_20_24' | 'age_25_plus' cancellationFeePercent: number cancellationFeePercent12To24Hours: number cancellationFeePercentMoreThan24Hours: number isDiscoverable: boolean autoCreateGroup: boolean addressVisibility: 'public' | 'attendees_only' generalArea: string waitlistAutoOffer: boolean sendReviewRequestSms: boolean } const ADDRESS_VISIBILITY_OPTIONS = [ { name: texts.events.addressVisibilityPublic, selectKey: 'public' }, { name: texts.events.addressVisibilityAttendeesOnly, selectKey: 'attendees_only' }, ] const DEFAULT_VALUES: EventCreateFormValues = { title: '', slug: '', categoryId: '', shortDescription: '', description: '', startDate: '', startTime: 600, endDate: '', endTime: 720, cityId: '', address: '', lat: 35.6892, lng: 51.389, isFree: false, price: 0, capacity: 20, reservedCapacity: 0, genderRestriction: 'open', ageRestriction: 'open', cancellationFeePercent: 30, cancellationFeePercent12To24Hours: 20, cancellationFeePercentMoreThan24Hours: 10, isDiscoverable: true, autoCreateGroup: true, addressVisibility: 'public', generalArea: '', waitlistAutoOffer: true, sendReviewRequestSms: true, } interface EventCreateFormProps { cancelHref: string successHrefFor: (eventId: string) => string initialOrganizerId?: string | null } async function loadVerifiedHost(userId: string): Promise { const result = await GET_ADMIN_USER_DETAIL(userId) if (!result.ok) return null if (result.data.identityStatus !== 'verified') return null return { id: result.data.id, firstName: result.data.firstName, lastName: result.data.lastName, mobile: result.data.mobile, } } const EventCreateForm = ({ cancelHref, successHrefFor, initialOrganizerId }: EventCreateFormProps) => { const router = useRouter() const FormActions = AdminFormActions const FormSection = AdminFormSection const [categories, setCategories] = useState([]) const [cities, setCities] = useState([]) const [media, setMedia] = useState([]) const [faqs, setFaqs] = useState([]) const [host, setHost] = useState(null) const [isLoading, setIsLoading] = useState(true) const [isSaving, setIsSaving] = useState(false) const [loadError, setLoadError] = useState(null) const form = useForm({ defaultValues: DEFAULT_VALUES }) const title = form.watch('title') const slug = form.watch('slug') const isFree = form.watch('isFree') const lat = form.watch('lat') const lng = form.watch('lng') const addressVisibility = form.watch('addressVisibility') const startDate = form.watch('startDate') const startTime = form.watch('startTime') const endDate = form.watch('endDate') const { todayIso, startMinimumTime, endMinimumTime } = useEventScheduleConstraints(startDate, startTime, endDate) useEffect(() => { if (!shouldAutogenerateEventSlug(slug, title)) return const nextSlug = slugifyEventTitle(title) if (nextSlug && nextSlug !== slug) { form.setValue('slug', nextSlug, { shouldDirty: false }) } }, [form, slug, title]) useEffect(() => { let cancelled = false const load = async () => { try { setIsLoading(true) setLoadError(null) const [cats, cityList] = await Promise.all([fetchEventCategories(), fetchAllCities()]) if (cancelled) return setCategories(cats) setCities(cityList) if (initialOrganizerId) { const preselected = await loadVerifiedHost(initialOrganizerId) if (!cancelled && preselected) setHost(preselected) } } catch { if (!cancelled) setLoadError(texts.events.categoriesListFailed) } finally { if (!cancelled) setIsLoading(false) } } void load() return () => { cancelled = true } }, [initialOrganizerId]) const handleSubmit = form.handleSubmit(async (values) => { if (!host) { addToast({ title: texts.events.createEventHostRequired, color: 'danger' }) return } try { const startsAt = combineDateAndMinutes(values.startDate, values.startTime) const endsAt = combineDateAndMinutes(values.endDate, values.endTime) if (new Date(endsAt) <= new Date(startsAt)) { addToast({ title: texts.validation.eventWizard.endAfterStart, color: 'danger' }) return } if (!hasRequiredPosters(media)) { addToast({ title: texts.events.postersRequiredTitle, description: texts.events.postersRequiredDescription, color: 'danger', }) return } const selectedCity = cities.find((city) => String(city.id) === values.cityId) if (!selectedCity) { addToast({ title: texts.validation.cityRequired, color: 'danger' }) return } if ( values.cancellationFeePercent < values.cancellationFeePercent12To24Hours || values.cancellationFeePercent12To24Hours < values.cancellationFeePercentMoreThan24Hours ) { addToast({ title: texts.validation.eventWizard.cancelPenaltyOrder, color: 'danger' }) return } const price = values.isFree ? 0 : Number(values.price) const payload: AdminCreateEventPayload = { organizerId: host.id, title: values.title.trim(), slug: values.slug.trim(), categoryId: Number(values.categoryId), shortDescription: values.shortDescription.trim() || undefined, description: values.description || undefined, startsAt, endsAt, provinceId: selectedCity.provinceId, cityId: Number(values.cityId), address: values.address.trim(), lat: values.lat, lng: values.lng, isFree: values.isFree, price, capacity: Number(values.capacity), reservedCapacity: Number(values.reservedCapacity) || 0, genderRestriction: values.genderRestriction, ageRestriction: values.ageRestriction, cancellationFeePercent: values.cancellationFeePercent, cancellationFeePercent12To24Hours: values.cancellationFeePercent12To24Hours, cancellationFeePercentMoreThan24Hours: values.cancellationFeePercentMoreThan24Hours, settings: { isDiscoverable: values.isDiscoverable, autoCreateGroup: values.autoCreateGroup, addressVisibility: values.addressVisibility, generalArea: values.addressVisibility === 'attendees_only' ? values.generalArea.trim() : undefined, waitlistAutoOffer: values.waitlistAutoOffer, sendReviewRequestSms: values.sendReviewRequestSms, }, media: media.map((item, index) => ({ mediaType: 'image' as const, url: item.url, sortOrder: item.sortOrder ?? index, isPoster: item.isPoster, isSquarePoster: item.isSquarePoster, })), } setIsSaving(true) const created = await createEventAsAdmin(payload) for (const [index, faq] of faqs.entries()) { const question = faq.question.trim() const answer = faq.answer.trim() if (!question || !answer) continue await createEventFaq(created.id, { question, answer, sortOrder: index, }) } addToast({ title: texts.events.createEventSuccess, color: 'success' }) router.push(successHrefFor(created.id)) } catch (err) { const detail = extractServerErrorDetail((err as { response?: { data?: unknown } })?.response?.data) addToast({ title: texts.events.createEventFailed, description: detail ?? undefined, color: 'danger', }) } finally { setIsSaving(false) } }, showFormValidationToast) if (isLoading) return if (loadError) return

{loadError}

return (
void handleSubmit(e)} > {texts.events.createEventApprovedHint}

{texts.common.description}

{ form.setValue('description', value, { shouldDirty: true }) }} />
{ form.setValue('address', address, { shouldDirty: true }) }} onChange={({ lat: nextLat, lng: nextLng }) => { form.setValue('lat', nextLat, { shouldDirty: true }) form.setValue('lng', nextLng, { shouldDirty: true }) }} /> {addressVisibility === 'attendees_only' && ( )}
) } export default EventCreateForm