import React, { useId, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; import { XMarkIcon } from '@heroicons/react/24/outline'; import ConfirmDialog from '../ui/ConfirmDialog'; import SearchableSelect from '../ui/SearchableSelect'; import PersianDateInput from '../ui/PersianDateInput'; import { useResourceAvailability, useResourceExceptions } from '../../hooks/useResourceCalendar'; import { formatDate, formatNumber } from '../../lib/utils'; import { DAY_LABELS } from './ResourceWorkingHoursPanel'; import NationalHolidaysCard from '../holidays/NationalHolidaysCard'; import type { ResourceAvailability, 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); } /** * تعطیلات و استثناهای یک منبع، کنار پیش‌نمایش دو هفتهٔ ساعت آزاد. * * پیش‌نمایش عمداً «ساعت آزاد» نامیده نشده بلکه «خام» است: نوبت‌های ثبت‌شده در آن * کسر نشده‌اند و اشتباه گرفتنش با «وقت قابل رزرو» به بیش‌رزروی می‌انجامد. * * دو ستون، نه `auto-fit`: تعطیلات و استثنا هر دو ورودی‌اند و کنار هم می‌مانند، * پیش‌نمایش خروجی است و ستون خودش را می‌گیرد. */ 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} />
toDelete && remove.mutate(toDelete.uuid, { onSuccess: () => setToDelete(null) })} onCancel={() => setToDelete(null)} />
); } /** * پیش‌نمایش دو هفته. * * ۱۴ سطرِ یکنواختِ «شیفتی تعریف نشده» فقط نویز است و با یک حالت خالیِ راه‌حل‌دار * جمع می‌شود. شرط عمداً تنگ است: اگر حتی یک روز به دلیل دیگری (تعطیل رسمی، * مرخصی) بسته باشد سطرها می‌مانند، چون آن دلیل خودش اطلاعات است — نه نویز. */ function PreviewCard({ availability }: { availability?: ResourceAvailability }) { const days = availability?.days ?? []; const nothingConfigured = days.length > 0 && days.every((d) => d.intervals.length === 0 && d.reasons.length > 0 && d.reasons.every((r) => r === 'no_shift')); return (

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

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

{days.length === 0 ? (
) : nothingConfigured ? (

در دو هفتهٔ آینده هیچ ساعتی باز نیست. اول در «شیفت هفتگی» روزهای کاری را تعریف کنید.

) : (
{days.map((day) => (
{DAY_LABELS[day.day_of_week]} · {formatDate(day.date)} {day.intervals.length === 0 ? ( {day.reasons.map((r) => REASON_LABELS[r] ?? r).join('، ') || '—'} ) : ( {formatNumber(day.total_minutes)} دقیقه )}
))}
)}
); } 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(''); // id یکتا لازم است: این کارت در صفحهٔ منبع و صفحهٔ تنظیمات نوبت‌دهی هر دو رندر می‌شود. const uid = useId(); 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; const typeLabel = useMemo( () => EXCEPTION_TYPES.find((t) => t.value === type)?.label ?? '', [type], ); return (

مرخصی و سرویس

{exceptions.length === 0 ? (

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

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

برای ثبت یا حذف استثنا مجوز ویرایش تنظیمات نوبت‌دهی لازم است.

)}
); }