admin/components/events/create/steps/Step2ScheduleLocation.tsx
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

245 lines
8.7 KiB
TypeScript

'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { useEffect, useRef, useState } from 'react'
import { FormProvider, useForm, useWatch } from 'react-hook-form'
import { texts } from '@/texts'
import type { EventWizardFormData } from '@/components/events/create/types'
import Input from '@/components/formElements/Input'
import MapLocationPicker from '@/components/events/create/MapLocationPicker'
import { WizardFormFullWidth, WizardFormGrid, WizardSectionTitle, WizardStepFooter } from '@/components/events/create/WizardFormLayout'
import { INITIAL_WIZARD_DATA } from '@/components/events/create/types'
import { useEventScheduleConstraints } from '@/components/events/schedule/eventScheduleConstraints'
import { addToast } from '@/lib/toast'
import { fetchAllCities, type City } from '@/services/geography'
import { EventWizardStep2Validation, type EventWizardStep2Values } from '@/validation/eventWizard'
import { showFormValidationToast } from '@/lib/formValidationToast'
const ADDRESS_VISIBILITY_OPTIONS = [
{ name: texts.events.addressVisibilityPublic, selectKey: 'public' },
{ name: texts.events.addressVisibilityAttendeesOnly, selectKey: 'attendees_only' },
]
interface Step2ScheduleLocationProps {
data: EventWizardFormData
onBack: () => void
onChange: (patch: Partial<EventWizardFormData>) => void
onNext: (patch: Partial<EventWizardFormData>) => void
}
export default function Step2ScheduleLocation({ data, onBack, onChange, onNext }: Step2ScheduleLocationProps) {
const [cities, setCities] = useState<City[]>([])
const form = useForm<EventWizardStep2Values>({
resolver: zodResolver(EventWizardStep2Validation),
defaultValues: {
startDate: data.startDate,
startTime: data.startTime,
endDate: data.endDate,
endTime: data.endTime,
provinceId: data.provinceId,
cityId: data.cityId,
address: data.address,
addressVisibility: data.addressVisibility,
generalArea: data.generalArea,
},
})
const addressVisibility = form.watch('addressVisibility')
const addressValue = useWatch({ control: form.control, name: 'address' })
// اگر کاربر قبلاً موقعیت را روی نقشه دستی مشخص کرده (مثلاً هنگام کپی یا ویرایش رویداد)،
// با تغییر شهر نباید آن پین بازنویسی شود.
const hasManualLocationRef = useRef(data.lat !== INITIAL_WIZARD_DATA.lat || data.lng !== INITIAL_WIZARD_DATA.lng)
const selectedCityId = useWatch({ control: form.control, name: 'cityId' })
const startDate = useWatch({ control: form.control, name: 'startDate' })
const startTime = useWatch({ control: form.control, name: 'startTime' })
const endDate = useWatch({ control: form.control, name: 'endDate' })
const { todayIso, startMinimumTime, endMinimumTime } = useEventScheduleConstraints(startDate, startTime, endDate)
useEffect(() => {
fetchAllCities()
.then(setCities)
.catch(() => addToast({ title: texts.events.citiesLoadFailed, color: 'danger' }))
}, [])
useEffect(() => {
if (hasManualLocationRef.current) return
const selectedCity = cities.find((city) => String(city.id) === selectedCityId)
if (selectedCity?.lat != null && selectedCity?.lng != null) {
onChange({ lat: selectedCity.lat, lng: selectedCity.lng })
}
}, [cities, onChange, selectedCityId])
useEffect(() => {
const subscription = form.watch((values) => {
onChange({
startDate: values.startDate ?? '',
startTime: values.startTime ?? data.startTime,
endDate: values.endDate ?? '',
endTime: values.endTime ?? data.endTime,
provinceId: values.provinceId ?? '',
cityId: values.cityId ?? '',
address: values.address ?? '',
addressVisibility: values.addressVisibility ?? 'public',
generalArea: values.generalArea ?? '',
})
})
return () => {
subscription.unsubscribe()
}
}, [data.endTime, data.startTime, form, onChange])
const handleSubmit = form.handleSubmit((values) => {
const selectedCity = cities.find((city) => String(city.id) === values.cityId)
if (!selectedCity) {
form.setError('cityId', { message: texts.validation.cityRequired })
addToast({ title: texts.common.formIncomplete, description: texts.validation.cityRequired, color: 'danger' })
return
}
onNext({
startDate: values.startDate,
startTime: values.startTime,
endDate: values.endDate,
endTime: values.endTime,
provinceId: String(selectedCity.provinceId),
cityId: values.cityId,
address: values.address,
lat: data.lat,
lng: data.lng,
addressVisibility: values.addressVisibility,
generalArea: values.addressVisibility === 'attendees_only' ? (values.generalArea?.trim() ?? '') : '',
})
}, showFormValidationToast)
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit}>
<WizardFormGrid>
<WizardSectionTitle description={texts.events.scheduleSectionDescription}>{texts.events.scheduleSectionTitle}</WizardSectionTitle>
<Input
required
generalType="eventDatePicker"
label={texts.events.startsAtDateLabel}
minDate={todayIso}
name="startDate"
/>
<Input
required
generalType="eventTimePicker"
label={texts.events.startsAtTimeLabel}
minValue={startMinimumTime}
name="startTime"
/>
<Input
required
generalType="eventDatePicker"
label={texts.events.endsAtDateLabel}
minDate={startDate || todayIso}
name="endDate"
/>
<Input
required
generalType="eventTimePicker"
label={texts.events.endsAtTimeLabel}
minValue={endMinimumTime}
name="endTime"
/>
<WizardSectionTitle description={texts.events.venueSectionDescription}>{texts.events.venueSectionTitle}</WizardSectionTitle>
{/* انتخاب استان فعلاً غیرفعال است؛ استان از روی شهر تعیین می‌شود.
<Input
required
generalType="select"
label={texts.common.province}
name="provinceId"
selectKey="id"
selectOptions={provinces}
selectValue="name"
/> */}
<Input
required
generalType="select"
label={texts.common.city}
name="cityId"
selectKey="id"
selectOptions={cities}
selectValue="name"
/>
<WizardFormFullWidth>
<Input
required
generalType="textarea"
label={texts.events.address}
name="address"
placeholder={texts.events.addressPlaceholder}
textAreaMinRows={3}
/>
<p className="mt-1 text-xs text-default-500">{texts.events.addressHostContactHint}</p>
</WizardFormFullWidth>
<WizardFormFullWidth>
<MapLocationPicker
displayAddress={addressValue}
lat={data.lat}
lng={data.lng}
mode="inline"
onAddressResolved={(address) => {
form.setValue('address', address, { shouldDirty: true, shouldTouch: true })
onChange({ address })
}}
onChange={({ lat, lng }) => {
hasManualLocationRef.current = true
onChange({ lat, lng })
}}
/>
</WizardFormFullWidth>
<WizardSectionTitle description={texts.events.addressVisibilitySectionDescription}>
{texts.events.addressVisibilitySectionTitle}
</WizardSectionTitle>
<WizardFormFullWidth>
<Input
generalType="radio"
label={texts.events.addressVisibilityLabel}
name="addressVisibility"
radioOptions={ADDRESS_VISIBILITY_OPTIONS}
selectKey="selectKey"
/>
</WizardFormFullWidth>
{addressVisibility === 'attendees_only' && (
<WizardFormFullWidth>
<Input
required
generalType="textarea"
label={texts.events.generalAreaLabel}
name="generalArea"
placeholder={texts.events.generalAreaPlaceholder}
textAreaMinRows={2}
/>
</WizardFormFullWidth>
)}
<WizardStepFooter
primaryLabel={texts.events.nextStep}
onBack={onBack}
onPrimary={handleSubmit}
/>
</WizardFormGrid>
</form>
</FormProvider>
)
}