Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
245 lines
8.7 KiB
TypeScript
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>
|
|
)
|
|
}
|