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