'use client' import type { ReactNode } from 'react' import { useState } from 'react' import { jalaaliMonthLength, toGregorian, toJalaali } from 'jalaali-js' import Picker from 'react-mobile-picker' import { Controller, useFormContext, type Control, type UseFormReturn } from 'react-hook-form' import ConsumerModal from '@/components/consumer/ConsumerModal' import ConsumerPickerTrigger from '@/components/events/create/ConsumerPickerTrigger' import Button from '@/components/formElements/Button' import AngleLeftIcon from '@/components/icons/AngleLeftIcon' import CalendarEditIcon from '@/components/icons/CalendarEditIcon' import CalendarIcon from '@/components/icons/CalendarIcon' import ClockIcon from '@/components/icons/ClockIcon' import CloseLinearIcon from '@/components/icons/CloseLinearIcon' import TimeCircleIcon from '@/components/icons/TimeCircleIcon' import { buildOutsideLabel } from '@/components/formElements/input/inputUtils' import { texts } from '@/texts' import { formatNumber } from '@/helpers' import { cn } from '@/lib/cn' const HOURS = Array.from({ length: 24 }, (_, index) => String(index)) const MINUTES = Array.from({ length: 60 }, (_, index) => String(index)) const MONTHS = Array.from({ length: 12 }, (_, index) => String(index + 1)) const PERSIAN_MONTHS = texts.common.jalaliMonths export interface DatePickerValue { day: string month: string year: string [key: string]: string } export interface TimePickerValue { hour: string minute: string [key: string]: string } type Variant = 'flat' | 'bordered' | 'faded' | 'underlined' interface Props { className?: string /** RHF mode: pass control or render under FormProvider. */ control?: Control description?: ReactNode disabled?: boolean /** Controlled mode (no RHF): inline error text. */ errorMessage?: string kind: 'eventDatePicker' | 'eventTimePicker' label?: string minDate?: string minValue?: number name?: string placeholder?: string readonly?: boolean required?: boolean size?: 'sm' | 'md' | 'lg' triggerStyle?: 'default' | 'consumer' /** Controlled date ISO string or time minutes-of-day. */ value?: string | number variant: Variant onValueChange?: (value: string | number) => void } const toPersianNumber = (value: number | string) => formatNumber(Number(value)) const padTimePart = (value: number | string) => String(value).padStart(2, '0') function validDateOrToday(value: unknown): Date { if (typeof value === 'string' && value.trim()) { const parsed = new Date(value) if (Number.isFinite(parsed.getTime())) return parsed } return new Date() } export function getEventDatePickerValue(value: unknown): DatePickerValue { const { jy, jm, jd } = toJalaali(validDateOrToday(value)) return { day: String(jd), month: String(jm), year: String(jy) } } export function eventDatePickerValueToIso(value: DatePickerValue, previousValue: unknown): string { const previousDate = validDateOrToday(previousValue) const fallbackYear = toJalaali(previousDate).jy const year = Number(value.year) || fallbackYear const month = Number(value.month) const day = Math.min(Number(value.day), jalaaliMonthLength(year, month)) const { gy, gm, gd } = toGregorian(year, month, day) return new Date(gy, gm - 1, gd).toISOString() } export function getEventTimePickerValue(value: unknown): TimePickerValue { const minutes = typeof value === 'number' && Number.isFinite(value) ? Math.min(1439, Math.max(0, Math.floor(value))) : 0 return { hour: String(Math.floor(minutes / 60)), minute: String(minutes % 60), } } function dateAtStartOfDay(value: unknown): Date { const date = validDateOrToday(value) date.setHours(0, 0, 0, 0) return date } export function constrainEventDatePickerValue(value: unknown, minDate: unknown): DatePickerValue { const minimum = dateAtStartOfDay(minDate) const hasExplicitValue = (typeof value === 'string' && value.trim() !== '') || (typeof value === 'number' && Number.isFinite(value)) || value instanceof Date const current = hasExplicitValue ? dateAtStartOfDay(value) : minimum return getEventDatePickerValue(current < minimum ? minimum.toISOString() : current.toISOString()) } function pickerDateToTimestamp(value: DatePickerValue): number { const { gy, gm, gd } = toGregorian(Number(value.year), Number(value.month), Number(value.day)) return new Date(gy, gm - 1, gd).setHours(0, 0, 0, 0) } function WheelItem({ value, label, distance, disabled = false }: { value: string; label: string; distance: number; disabled?: boolean }) { return ( {({ selected }) => ( {label} )} ) } export function DateWheel({ value, minDate, onChange, }: { value: DatePickerValue minDate: Date onChange: (value: DatePickerValue) => void }) { const minimum = getEventDatePickerValue(minDate.toISOString()) const selectedYear = Number(value.year) const days = Array.from({ length: jalaaliMonthLength(selectedYear, Number(value.month)) }, (_, index) => String(index + 1)) const handleChange = (nextValue: DatePickerValue, key: string) => { const month = Number(nextValue.month) const year = key === 'month' ? (month < Number(minimum.month) ? Number(minimum.year) + 1 : Number(minimum.year)) : Number(nextValue.year) const maxDay = jalaaliMonthLength(year, month) const minimumDay = year === Number(minimum.year) && month === Number(minimum.month) ? Number(minimum.day) : 1 onChange({ ...nextValue, day: String(Math.max(minimumDay, Math.min(Number(nextValue.day), maxDay))), year: String(year), }) } return (
{texts.common.day} {texts.common.month}
{days.map((day, index) => ( ))}
/
{MONTHS.map((month, index) => ( ))}
) } function timePickerValueToMinutes(value: TimePickerValue): number { return Number(value.hour) * 60 + Number(value.minute) } export function TimeWheel({ value, minValue, onChange, }: { value: TimePickerValue minValue: number onChange: (value: TimePickerValue) => void }) { const handleChange = (nextValue: TimePickerValue, key: string) => { if (key === 'hour' && timePickerValueToMinutes(nextValue) < minValue) { const minimum = getEventTimePickerValue(minValue) onChange(Number(nextValue.hour) === Number(minimum.hour) ? minimum : nextValue) return } onChange(nextValue) } return (
{texts.common.hour} {texts.common.minute}
{HOURS.map((hour, index) => ( ))}
:
{MINUTES.map((minute, index) => ( ))}
) } function WheelFrame({ children }: { children: ReactNode }) { return
{children}
} function triggerClasses(size: Props['size'], variant: Variant, invalid: boolean) { return cn( 'flex w-full items-center justify-between gap-3 px-3 text-right text-sm outline-none transition', size === 'sm' ? 'h-9' : size === 'lg' ? 'h-12' : 'h-10', variant === 'underlined' ? 'rounded-none border-b bg-transparent' : 'rounded-xl', variant === 'bordered' && 'border bg-transparent', variant === 'faded' && 'border bg-default-100', variant === 'flat' && 'bg-default-100', invalid ? 'border-fourth-900 text-fourth-900' : 'border-default-200 text-text-main', 'disabled:cursor-not-allowed disabled:opacity-50' ) } function useOptionalFormControl(controlProp?: Control): Control | null { const context = useFormContext() as UseFormReturn | null return controlProp ?? context?.control ?? null } function ScheduleFieldView({ value, errorMessage, onChange, onBlur, ...props }: Omit & { value: unknown errorMessage?: string onChange: (value: string | number) => void onBlur?: () => void }) { const [isOpen, setIsOpen] = useState(false) const [dateDraft, setDateDraft] = useState({ day: '1', month: '1', year: '1400' }) const [timeDraft, setTimeDraft] = useState({ hour: '0', minute: '0' }) const isDate = props.kind === 'eventDatePicker' const invalid = Boolean(errorMessage) const openPicker = () => { if (props.disabled || props.readonly) return if (isDate) { setDateDraft(constrainEventDatePickerValue(value, props.minDate)) } else { const minimum = Math.min(1440, Math.max(0, props.minValue ?? 0)) const current = typeof value === 'number' ? value : minimum setTimeDraft(getEventTimePickerValue(Math.max(current, minimum))) } setIsOpen(true) } const accept = () => { if (isDate) { onChange(eventDatePickerValueToIso(dateDraft, value)) } else { onChange(Number(timeDraft.hour) * 60 + Number(timeDraft.minute)) } onBlur?.() setIsOpen(false) } let displayValue = '' if (isDate && typeof value === 'string' && value.trim()) { const selected = getEventDatePickerValue(value) displayValue = `${toPersianNumber(selected.day)} ${PERSIAN_MONTHS[Number(selected.month) - 1]}` } else if (!isDate && typeof value === 'number') { const selected = getEventTimePickerValue(value) displayValue = `${toPersianNumber(padTimePart(selected.hour))}:${toPersianNumber(padTimePart(selected.minute))}` } const minimumDate = dateAtStartOfDay(props.minDate) const minimumTime = Math.min(1440, Math.max(0, props.minValue ?? 0)) const draftIsValid = isDate ? pickerDateToTimestamp(dateDraft) >= minimumDate.getTime() : timePickerValueToMinutes(timeDraft) >= minimumTime return (
{buildOutsideLabel(props.label, isDate ? : , props.required, 0)}
{props.triggerStyle === 'consumer' ? ( {displayValue || props.placeholder || (isDate ? texts.common.selectDate : texts.common.selectTime)} ) : ( )} {errorMessage ?

{errorMessage}

: null} {props.description ?

{props.description}

: null} : } cancelLabel={isDate ? texts.common.cancel : texts.common.back} isOpen={isOpen} submitDisabled={!draftIsValid} submitLabel="انتخاب" title={props.label ?? (isDate ? texts.common.selectDate : texts.common.selectTime)} titleIcon={isDate ? : } onOpenChange={setIsOpen} onSubmit={accept} > {isDate ? ( ) : ( )}
) } export default function EventScheduleFieldControl(props: Props) { const control = useOptionalFormControl(props.control) if (props.onValueChange) { return ( ) } if (!control || !props.name) { throw new Error('EventScheduleFieldControl needs FormProvider/control+name, or onValueChange') } return ( ( { field.onChange(nextValue) }} /> )} /> ) }