Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
227 lines
8.4 KiB
TypeScript
227 lines
8.4 KiB
TypeScript
'use client'
|
||
|
||
import { zodResolver } from '@hookform/resolvers/zod'
|
||
import { useEffect } from 'react'
|
||
import { FormProvider, useForm } from 'react-hook-form'
|
||
|
||
import { texts, format } from '@/texts'
|
||
import type { EventWizardEditLocks, EventWizardFormData } from '@/components/events/create/consumer/types'
|
||
import AudienceRestrictionSelector from '@/components/events/create/AudienceRestrictionSelector'
|
||
import TicketTypeSelector from '@/components/events/create/TicketTypeSelector'
|
||
import ConsumerInput from '@/components/consumer/ConsumerInput'
|
||
import InlineNotice from '@/components/feedback/InlineNotice'
|
||
import { EventWizardStep3Validation, type EventWizardStep3Values } from '@/validation/eventWizard'
|
||
import { showFormValidationToast } from '@/lib/formValidationToast'
|
||
import { addToast } from '@/lib/toast'
|
||
|
||
interface Step3PricingCapacityProps {
|
||
data: EventWizardFormData
|
||
locks?: EventWizardEditLocks
|
||
onBindSubmit: (submit: (() => void) | null) => void
|
||
onChange: (patch: Partial<EventWizardFormData>) => void
|
||
onNext: (patch: Partial<EventWizardFormData>) => void
|
||
}
|
||
|
||
export default function Step3PricingCapacity({ data, locks, onBindSubmit, onChange, onNext }: Step3PricingCapacityProps) {
|
||
const pricingLocked = Boolean(locks?.pricing)
|
||
const audienceLocked = Boolean(locks?.audience)
|
||
const cancellationLocked = Boolean(locks?.cancellation)
|
||
const behavioralLocked = Boolean(locks?.behavioralSettings)
|
||
const minCapacity = locks?.minCapacity ?? 1
|
||
const showLockNotice = Boolean(locks?.notice && (pricingLocked || audienceLocked || cancellationLocked || behavioralLocked))
|
||
|
||
const form = useForm<EventWizardStep3Values>({
|
||
resolver: zodResolver(EventWizardStep3Validation),
|
||
defaultValues: {
|
||
isFree: data.isFree,
|
||
price: data.price,
|
||
capacity: data.capacity,
|
||
reservedCapacity: data.reservedCapacity,
|
||
genderRestriction: data.genderRestriction,
|
||
ageRestriction: data.ageRestriction,
|
||
cancellationFeePercent: data.cancellationFeePercent,
|
||
isDiscoverable: data.isDiscoverable,
|
||
autoCreateGroup: data.autoCreateGroup,
|
||
waitlistAutoOffer: data.waitlistAutoOffer,
|
||
sendReviewRequestSms: data.sendReviewRequestSms,
|
||
},
|
||
})
|
||
|
||
const isFree = form.watch('isFree')
|
||
|
||
useEffect(() => {
|
||
const subscription = form.watch((values) => {
|
||
onChange({
|
||
isFree: values.isFree ?? false,
|
||
price: Number(values.price ?? 0),
|
||
capacity: Number(values.capacity ?? 0),
|
||
reservedCapacity: Number(values.reservedCapacity ?? 0),
|
||
genderRestriction: values.genderRestriction ?? 'open',
|
||
ageRestriction: values.ageRestriction ?? 'open',
|
||
cancellationFeePercent: Number(values.cancellationFeePercent ?? 0),
|
||
isDiscoverable: values.isDiscoverable ?? false,
|
||
autoCreateGroup: values.autoCreateGroup ?? true,
|
||
waitlistAutoOffer: values.waitlistAutoOffer ?? true,
|
||
sendReviewRequestSms: values.sendReviewRequestSms ?? false,
|
||
})
|
||
})
|
||
|
||
return () => {
|
||
subscription.unsubscribe()
|
||
}
|
||
}, [form, onChange])
|
||
|
||
const handleSubmit = form.handleSubmit((values) => {
|
||
if (values.capacity < minCapacity) {
|
||
addToast({
|
||
title: format(texts.events.capacityBelowOccupied, { min: minCapacity }),
|
||
color: 'danger',
|
||
})
|
||
|
||
return
|
||
}
|
||
|
||
onNext({
|
||
isFree: values.isFree,
|
||
price: values.isFree ? 0 : values.price,
|
||
capacity: values.capacity,
|
||
reservedCapacity: values.reservedCapacity,
|
||
genderRestriction: values.genderRestriction,
|
||
ageRestriction: values.ageRestriction,
|
||
cancellationFeePercent: values.cancellationFeePercent,
|
||
isDiscoverable: values.isDiscoverable,
|
||
autoCreateGroup: values.autoCreateGroup,
|
||
waitlistAutoOffer: values.waitlistAutoOffer,
|
||
sendReviewRequestSms: values.sendReviewRequestSms,
|
||
})
|
||
}, showFormValidationToast)
|
||
|
||
useEffect(() => {
|
||
onBindSubmit(() => {
|
||
void handleSubmit()
|
||
})
|
||
|
||
return () => {
|
||
onBindSubmit(null)
|
||
}
|
||
}, [handleSubmit, onBindSubmit])
|
||
|
||
return (
|
||
<FormProvider {...form}>
|
||
<form
|
||
className="consumer-surface flex flex-col gap-4 p-4"
|
||
onSubmit={(event) => {
|
||
event.preventDefault()
|
||
void handleSubmit()
|
||
}}
|
||
>
|
||
{showLockNotice ? <InlineNotice>{locks?.notice}</InlineNotice> : null}
|
||
|
||
<div className="col-span-2">
|
||
<span className="label mb-1 block text-sm font-medium text-secondary-30 font-semibold">ایجاد بلیط</span>
|
||
<span className="label mb-1 block text-xs font-normal text-secondary-30">
|
||
نوع ثبتنام را انتخاب کن؛ برای رویداد پولی، مبلغ نهایی بلیط را وارد کن.
|
||
</span>
|
||
</div>
|
||
<TicketTypeSelector
|
||
disabled={pricingLocked}
|
||
isFree={isFree}
|
||
onChange={(nextIsFree) => {
|
||
if (pricingLocked) return
|
||
form.setValue('isFree', nextIsFree, { shouldDirty: true, shouldValidate: true })
|
||
}}
|
||
/>
|
||
|
||
{!isFree ? (
|
||
<ConsumerInput
|
||
disabled={pricingLocked}
|
||
formatOptions={{ style: 'decimal' }}
|
||
generalType="numberInput"
|
||
label={texts.events.pricePerTicketLabel}
|
||
minValue={0}
|
||
name="price"
|
||
/>
|
||
) : null}
|
||
|
||
<ConsumerInput
|
||
description={(locks?.minCapacity ?? 1) > 1 ? format(texts.events.capacityMinOccupiedHint, { min: minCapacity }) : undefined}
|
||
generalType="numberInput"
|
||
label={texts.events.capacity}
|
||
minValue={minCapacity}
|
||
name="capacity"
|
||
/>
|
||
|
||
<ConsumerInput
|
||
description={locks != null ? texts.events.reservedCapacityEditLockedHint : texts.events.reservedCapacityWizardHint}
|
||
disabled={locks != null}
|
||
generalType="numberInput"
|
||
label={texts.events.reservedCapacityLabel}
|
||
minValue={0}
|
||
name="reservedCapacity"
|
||
/>
|
||
<div className="col-span-2">
|
||
<span className="label mb-1 block text-sm font-medium text-secondary-30 font-semibold">شرکت کنندگان</span>
|
||
<span className="label mb-1 block text-xs font-normal text-secondary-30">
|
||
اگر رویداد برای همه نیست، محدودیت جنسیت یا سن را انتخاب کن. پیشفرض آزاد است.
|
||
</span>
|
||
</div>
|
||
|
||
<AudienceRestrictionSelector
|
||
ageRestriction={form.watch('ageRestriction')}
|
||
disabled={audienceLocked}
|
||
genderRestriction={form.watch('genderRestriction')}
|
||
onAgeRestrictionChange={(ageRestriction) => {
|
||
if (audienceLocked) return
|
||
form.setValue('ageRestriction', ageRestriction, { shouldDirty: true, shouldValidate: true })
|
||
}}
|
||
onGenderRestrictionChange={(genderRestriction) => {
|
||
if (audienceLocked) return
|
||
form.setValue('genderRestriction', genderRestriction, { shouldDirty: true, shouldValidate: true })
|
||
}}
|
||
/>
|
||
|
||
<ConsumerInput
|
||
disabled={cancellationLocked}
|
||
generalType="numberInput"
|
||
label={texts.events.cancellationFeeLabel}
|
||
maxValue={100}
|
||
minValue={0}
|
||
name="cancellationFeePercent"
|
||
/>
|
||
|
||
<details className="group rounded-2xl border border-consumer-border bg-consumer-surface-muted p-4">
|
||
<summary className="cursor-pointer list-none text-sm font-bold text-consumer-text marker:hidden">
|
||
{texts.events.moreSettings}
|
||
</summary>
|
||
<div className="mt-4 flex flex-col gap-4 border-t border-consumer-border pt-4">
|
||
<ConsumerInput
|
||
disabled={behavioralLocked}
|
||
generalType="switch"
|
||
label={texts.events.discoverableLabel}
|
||
name="isDiscoverable"
|
||
/>
|
||
<ConsumerInput
|
||
disabled={behavioralLocked}
|
||
generalType="switch"
|
||
label={texts.events.autoCreateGroupLabel}
|
||
name="autoCreateGroup"
|
||
/>
|
||
<ConsumerInput
|
||
disabled={behavioralLocked}
|
||
generalType="switch"
|
||
label={texts.events.waitlistAutoOfferLabel}
|
||
name="waitlistAutoOffer"
|
||
/>
|
||
<ConsumerInput
|
||
disabled={behavioralLocked}
|
||
generalType="switch"
|
||
label={texts.events.sendReviewSmsLabel}
|
||
name="sendReviewRequestSms"
|
||
/>
|
||
</div>
|
||
</details>
|
||
</form>
|
||
</FormProvider>
|
||
)
|
||
}
|