admin/components/events/create/consumer/steps/Step3PricingCapacity.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

227 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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>
)
}