import React, { useState } from 'react'; import ConfirmDialog from '../ui/ConfirmDialog'; import SearchableSelect from '../ui/SearchableSelect'; import PersianDateInput from '../ui/PersianDateInput'; import { useResourceAvailability, useResourceExceptions } from '../../hooks/useResourceCalendar'; import { formatDate } from '../../lib/utils'; import { DAY_LABELS } from './ResourceWorkingHoursPanel'; import NationalHolidaysCard from '../holidays/NationalHolidaysCard'; import type { ResourceException } from '../../types'; /** چرا یک روز خالی است — بدون ترجمه، پاسخ خام سرور به کاربر نشان داده می‌شد. */ const REASON_LABELS: Record = { national_holiday: 'تعطیل رسمی', tenant_holiday: 'تعطیلی این محیط', no_shift: 'شیفتی تعریف نشده', exception: 'مرخصی یا سرویس', resource_inactive: 'منبع غیرفعال است', address_inactive: 'محل نوبت‌دهی غیرفعال است', }; const EXCEPTION_TYPES = [ { value: 'leave', label: 'مرخصی' }, { value: 'absence', label: 'غیبت' }, { value: 'maintenance', label: 'سرویس دوره‌ای' }, { value: 'closure', label: 'تعطیلی موردی' }, ]; /** نیمه‌شبِ امروز به‌صورت timestamp ثانیه‌ای. */ function todayMidnight(): number { const d = new Date(); d.setHours(0, 0, 0, 0); return Math.floor(d.getTime() / 1000); } /** * تعطیلات و استثناهای یک منبع، کنار پیش‌نمایش دو هفتهٔ ساعت آزاد. * * پیش‌نمایش عمداً «ساعت آزاد» نامیده نشده بلکه «خام» است: نوبت‌های ثبت‌شده در آن * کسر نشده‌اند و اشتباه گرفتنش با «وقت قابل رزرو» به بیش‌رزروی می‌انجامد. */ export default function ResourceExceptionsPanel({ resourceUuid, canUpdate }: { resourceUuid?: string; canUpdate: boolean; }) { const { exceptions, create, remove } = useResourceExceptions(resourceUuid); const [toDelete, setToDelete] = useState(null); const previewFrom = todayMidnight(); const previewTo = previewFrom + 13 * 86400; const { availability } = useResourceAvailability(resourceUuid, previewFrom, previewTo); return (
create.mutate(payload)} onDelete={setToDelete} />

پیش‌نمایش دو هفته

ساعت خام — نوبت‌های ثبت‌شده هنوز از آن کسر نشده‌اند.

{(availability?.days ?? []).map((day) => (
{DAY_LABELS[day.day_of_week]} · {formatDate(day.date * 1000)} {day.intervals.length === 0 ? ( {day.reasons.map((r) => REASON_LABELS[r] ?? r).join('، ') || '—'} ) : ( {day.total_minutes} دقیقه )}
))}
toDelete && remove.mutate(toDelete.uuid, { onSuccess: () => setToDelete(null) })} onCancel={() => setToDelete(null)} />
); } function ExceptionsCard({ exceptions, canUpdate, saving, onCreate, onDelete, }: { exceptions: ResourceException[]; canUpdate: boolean; saving: boolean; onCreate: (payload: { type: string; starts_at: number; ends_at: number; reason?: string | null }) => void; onDelete: (e: ResourceException) => void; }) { const [type, setType] = useState('leave'); const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [reason, setReason] = useState(''); const toTimestamp = (value: string): number | null => { if (value === '') return null; const ms = new Date(`${value}T00:00:00`).getTime(); return Number.isNaN(ms) ? null : Math.floor(ms / 1000); }; const start = toTimestamp(startDate); const end = toTimestamp(endDate); // پایان روزِ انتخاب‌شده، نه آغازش: مرخصیِ «تا سه‌شنبه» شامل خودِ سه‌شنبه است. const endExclusive = end === null ? null : end + 86400; const invalid = start === null || endExclusive === null || endExclusive <= start; return (

مرخصی و سرویس

{exceptions.length === 0 ? (

استثنایی ثبت نشده است.

) : (
{exceptions.map((e) => (
{e.type_label} {formatDate(e.starts_at * 1000)} تا {formatDate(e.ends_at * 1000)} {e.reason ? ` · ${e.reason}` : ''} {canUpdate && ( )}
))}
)} {canUpdate && (
setType(v ? String(v) : 'leave')} placeholder="نوع استثنا" height={36} /> {/* تقویم شمسی، نه `input type=date` میلادی: اپراتور تاریخ را شمسی می‌گوید و ترجمهٔ ذهنی همان‌جایی است که استثنا یک روز جابه‌جا ثبت می‌شود. */}
setReason(e.target.value)} placeholder="توضیح (اختیاری)" />
)}
); }