import React, { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { PlusIcon, TrashIcon } from '@heroicons/react/24/outline'; import PageHeader from '../components/ui/PageHeader'; import SearchableSelect from '../components/ui/SearchableSelect'; import { usePermissions } from '../hooks/usePermissions'; import { useBranchWorkingHours, useBranches } from '../hooks/useBranches'; import type { WorkingHourRange } from '../types'; /** ۰ = شنبه — همان قرارداد محاسبهٔ اسلات در بکاند. */ const DAY_LABELS = ['شنبه', 'یکشنبه', 'دوشنبه', 'سهشنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه']; const MINUTES_IN_DAY = 1440; /** * `endOfDay` وجود دارد چون `` سقفش ۲۳:۵۹ است و مقدار ۲۴:۰۰ را * نه نشان میدهد و نه میسازد. بدون این پرچم، بازهٔ شبانهروزیِ ذخیرهشده (۱۴۴۰) * بیصدا از فرم میافتاد و اولین ذخیره آن را خراب میکرد. */ type Draft = { start: string; end: string; endOfDay: boolean }; function toTime(minute: number): string { return `${String(Math.floor(minute / 60)).padStart(2, '0')}:${String(minute % 60).padStart(2, '0')}`; } /** `"24:00"` باید ۱۴۴۰ بدهد نه صفر — پایان روز است، نه آغازش. */ function toMinutes(time: string): number | null { const m = /^(\d{1,2}):(\d{2})$/.exec(time.trim()); if (!m) return null; const minutes = Number(m[1]) * 60 + Number(m[2]); return minutes >= 0 && minutes <= MINUTES_IN_DAY ? minutes : null; } /** * ساعت کاری هفتگی یک شعبه. * * ذخیره یک PUT است و کل هفته را جایگزین میکند؛ روزِ خالی یعنی شعبه آن روز بسته * است. اعتبارسنجی نهایی سمت سرور است — این فرم فقط جلوی ارسال ورودی واضحاً خراب * را میگیرد تا کاربر منتظر رفتوبرگشت نماند. */ export default function BranchWorkingHoursPage() { const { branchUuid } = useParams<{ branchUuid: string }>(); const { workingHours, loading, save } = useBranchWorkingHours(branchUuid); const { branches } = useBranches(); const { can } = usePermissions(); const canUpdate = can('appointment_settings', 'update'); const branch = branches.find((b) => b.uuid === branchUuid); const [draft, setDraft] = useState>({}); const [error, setError] = useState(null); useEffect(() => { if (!workingHours) return; const next: Record = {}; DAY_LABELS.forEach((_, day) => { next[day] = (workingHours.days[String(day)] ?? []).map((r: WorkingHourRange) => ({ start: toTime(r.start_minute), end: r.end_minute === MINUTES_IN_DAY ? '23:59' : toTime(r.end_minute), endOfDay: r.end_minute === MINUTES_IN_DAY, })); }); setDraft(next); }, [workingHours]); const addRange = (day: number) => { setDraft((d) => ({ ...d, [day]: [...(d[day] ?? []), { start: '09:00', end: '13:00', endOfDay: false }] })); }; const removeRange = (day: number, index: number) => { setDraft((d) => ({ ...d, [day]: (d[day] ?? []).filter((_, i) => i !== index) })); }; const editRange = (day: number, index: number, patch: Partial) => { setDraft((d) => ({ ...d, [day]: (d[day] ?? []).map((r, i) => (i === index ? { ...r, ...patch } : r)), })); }; const copyToWholeWeek = (day: number) => { const source = draft[day] ?? []; const next: Record = {}; DAY_LABELS.forEach((_, d) => { next[d] = source.map((r) => ({ ...r })); }); setDraft(next); }; const submit = () => { const days: Record = {}; for (const [dayKey, ranges] of Object.entries(draft)) { const parsed: { start_minute: number; end_minute: number }[] = []; for (const range of ranges) { const start = toMinutes(range.start); const end = range.endOfDay ? MINUTES_IN_DAY : toMinutes(range.end); if (start === null || end === null) { setError(`ساعت روز ${DAY_LABELS[Number(dayKey)]} را به شکل ۰۹:۰۰ وارد کنید`); return; } if (end <= start) { setError(`در روز ${DAY_LABELS[Number(dayKey)]} پایان بازه باید بعد از شروع آن باشد`); return; } parsed.push({ start_minute: start, end_minute: end }); } days[dayKey] = parsed; } setError(null); save.mutate(days); }; const totalRanges = Object.values(draft).reduce((sum, ranges) => sum + ranges.length, 0); return ( {save.isPending ? 'در حال ذخیره...' : 'ذخیرهٔ هفته'} ) : undefined } /> {error && ( {error} )} {branch && canUpdate && ( منطقهٔ زمانی شعبه {totalRanges === 0 ? 'هیچ بازهای تعریف نشده' : `${totalRanges} بازه در هفته`} )} {loading ? ( در حال بارگذاری... ) : ( {DAY_LABELS.map((label, day) => { const ranges = draft[day] ?? []; return ( {label} {ranges.length === 0 && ( بسته )} {canUpdate && ( {ranges.length > 0 && ( copyToWholeWeek(day)}> اعمال روی همهٔ روزها )} addRange(day)}> بازه )} {ranges.map((range, index) => ( از editRange(day, index, { start: e.target.value })} style={{ width: 120 }} /> تا {range.endOfDay ? ( ۲۴:۰۰ ) : ( editRange(day, index, { end: e.target.value })} style={{ width: 120 }} /> )} editRange(day, index, { endOfDay: e.target.checked })} /> تا پایان روز {canUpdate && ( removeRange(day, index)} aria-label="حذف بازه" > )} ))} ); })} )} ); } /** * فهرست منطقهٔ زمانی کوتاه و ثابت است — بکاند با `DateTimeZone::listIdentifiers()` * اعتبارسنجی میکند، پس این فهرست تنها راحتی است و منبع حقیقت نیست. */ const TIMEZONES = ['Asia/Tehran', 'Asia/Dubai', 'Asia/Baghdad', 'Europe/Istanbul', 'UTC']; function TimezoneSelect({ branchUuid, value }: { branchUuid: string; value: string }) { const { update } = useBranches(); const options = TIMEZONES.includes(value) ? TIMEZONES : [value, ...TIMEZONES]; return ( ({ value: tz, label: tz }))} value={value} onChange={(v) => v && update.mutate({ uuid: branchUuid, d: { timezone: String(v) } })} placeholder="منطقهٔ زمانی" height={38} /> ); }