- Integrated TourButton component into SettingsMenuPage, SkillsPage, SmsWalletPage, StaffPage, StaffSessionDetailPage, StaffTreatmentSessionsPage, SubscriptionPage, TagsSettingsPage, TreatmentCasesPage to enhance user onboarding experience. - Created new tour definitions for appointments, clinics, staff management, financial management, and patient management, ensuring comprehensive guidance for users navigating the admin panel. - Updated documentation to reflect the addition of tours and their implementation details.
224 lines
9.2 KiB
TypeScript
224 lines
9.2 KiB
TypeScript
import React from 'react';
|
|
import PageHeader from '../components/ui/PageHeader';
|
|
import DataTable, { type Column } from '../components/ui/DataTable';
|
|
import SearchableSelect from '../components/ui/SearchableSelect';
|
|
import PersianDateInput from '../components/ui/PersianDateInput';
|
|
import { useUrlState } from '../hooks/useUrlState';
|
|
import { usePermissions } from '../hooks/usePermissions';
|
|
import { useHolidays } from '../hooks/useResourceCalendar';
|
|
import { formatDate, currentJalaliYear } from '../lib/utils';
|
|
import type { NationalHoliday } from '../types';
|
|
import SettingsLayout from '../components/layout/SettingsLayout';
|
|
|
|
/** سال بدون جداکنندهٔ هزارگان — `formatNumber` «۱٬۴۰۵» میداد. */
|
|
const faYear = (y: number) => y.toLocaleString('fa-IR', { useGrouping: false });
|
|
|
|
/**
|
|
* تعطیلات رسمی و استثناهای این محیط.
|
|
*
|
|
* خودِ تعطیلات کشوریاند و اینجا فقط دیده میشوند؛ آنچه محیط تغییر میدهد «باز بودن
|
|
* یا نبودنِ» همان روز برای خودش است. ساخت و حذفِ خودِ تعطیلی کارِ مدیر سیستم است
|
|
* (`/admin/national-holidays`).
|
|
*/
|
|
export default function HolidaysSettingsPage() {
|
|
const thisYear = currentJalaliYear();
|
|
const [urlState, setUrlState] = useUrlState({ year: String(thisYear) });
|
|
const year = Number(urlState.year) || thisYear;
|
|
|
|
const { holidays, overrides, loading, error, setOverride, removeOverride } = useHolidays(year);
|
|
const { can } = usePermissions();
|
|
const canUpdate = can('appointment_settings', 'update');
|
|
|
|
const overrideByDate = new Map(overrides.map((o) => [o.date, o]));
|
|
|
|
const columns: Column<NationalHoliday>[] = [
|
|
// یک ستون تاریخ، نه دو تا: پیشتر «تاریخ» رشتهٔ خامِ `1405-05-13` بود و ستونِ
|
|
// «میلادی» با formatDate همان روز را **شمسی** نشان میداد — یک تاریخ، دو بار،
|
|
// با برچسبی که دروغ میگفت.
|
|
{ key: 'date', header: 'تاریخ', render: (h) => <span style={{ fontWeight: 600 }}>{formatDate(h.date)}</span> },
|
|
{ key: 'title', header: 'مناسبت', render: (h) => <span style={{ fontSize: 13 }}>{h.title}</span> },
|
|
{
|
|
key: 'status',
|
|
header: 'وضعیت این محیط',
|
|
render: (h) =>
|
|
overrideByDate.get(h.date)?.is_working ? (
|
|
<span className="badge green"><span className="bdot" />باز است</span>
|
|
) : (
|
|
<span className="badge gray"><span className="bdot" />تعطیل</span>
|
|
),
|
|
},
|
|
];
|
|
|
|
const years = Array.from({ length: 5 }, (_, i) => thisYear - 1 + i);
|
|
|
|
return (
|
|
<SettingsLayout active="holidays">
|
|
<div className="fade-in">
|
|
<PageHeader
|
|
title="تعطیلات رسمی"
|
|
tourId="holidays"
|
|
description="تعطیلات کشوری برای همهٔ محیطها اعمال میشود. اگر این محیط روزی را باز است، همینجا استثنا بزنید."
|
|
backTo="/admin/settings-menu"
|
|
/>
|
|
|
|
{error && (
|
|
<div
|
|
className="card card-pad"
|
|
style={{ marginBottom: 'var(--gap)', color: 'var(--danger)', background: 'var(--danger-bg)', fontSize: 13 }}
|
|
>
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<DataTable
|
|
columns={columns}
|
|
data={holidays}
|
|
loading={loading}
|
|
emptyMessage={`برای سال ${faYear(year)} تعطیلی ثبت نشده است`}
|
|
headerExtra={
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginRight: 'auto' }}>
|
|
<label id="holidays-year-label" style={{ fontSize: 12, color: 'var(--text-2)' }}>سال</label>
|
|
<div style={{ minWidth: 130 }}>
|
|
<SearchableSelect
|
|
options={years.map((y) => ({ value: String(y), label: faYear(y) }))}
|
|
value={String(year)}
|
|
onChange={(v) => setUrlState({ year: v ? String(v) : String(thisYear) })}
|
|
placeholder="سال"
|
|
ariaLabelledBy="holidays-year-label"
|
|
height={36}
|
|
/>
|
|
</div>
|
|
</div>
|
|
}
|
|
actions={
|
|
canUpdate
|
|
? (h) => {
|
|
const override = overrideByDate.get(h.date);
|
|
return (
|
|
<div style={{ display: 'flex', gap: 6 }}>
|
|
{override?.is_working ? (
|
|
<button
|
|
type="button"
|
|
className="btn secondary sm"
|
|
disabled={removeOverride.isPending}
|
|
onClick={() => removeOverride.mutate(override.uuid)}
|
|
>
|
|
تعطیل کن
|
|
</button>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
className="btn secondary sm"
|
|
disabled={setOverride.isPending}
|
|
onClick={() => setOverride.mutate({ date: h.date, is_working: true })}
|
|
>
|
|
این روز باز است
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
: undefined
|
|
}
|
|
/>
|
|
|
|
<ClosureCard
|
|
overrides={overrides.filter((o) => !o.is_working)}
|
|
canUpdate={canUpdate}
|
|
saving={setOverride.isPending}
|
|
onAdd={(date, note) => setOverride.mutate({ date, is_working: false, note })}
|
|
onRemove={(uuid) => removeOverride.mutate(uuid)}
|
|
/>
|
|
</div>
|
|
</SettingsLayout>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* جهت دوم: روزی که تعطیل رسمی نیست ولی این محیط بسته است.
|
|
* این با «استثنای منبع» فرق دارد — آن روی یک منبع است و این روی کل محیط.
|
|
*/
|
|
function ClosureCard({
|
|
overrides, canUpdate, saving, onAdd, onRemove,
|
|
}: {
|
|
overrides: { uuid: string; date: number; note: string | null }[];
|
|
canUpdate: boolean;
|
|
saving: boolean;
|
|
onAdd: (date: number, note: string | null) => void;
|
|
onRemove: (uuid: string) => void;
|
|
}) {
|
|
const [date, setDate] = React.useState('');
|
|
const [note, setNote] = React.useState('');
|
|
|
|
const timestamp = date === '' ? null : Math.floor(new Date(`${date}T00:00:00`).getTime() / 1000);
|
|
|
|
return (
|
|
<div className="card card-pad" style={{ marginTop: 'var(--gap)' }}>
|
|
<h2 className="section-title" style={{ margin: '0 0 4px' }}>تعطیلیهای این محیط</h2>
|
|
<p style={{ fontSize: 12, color: 'var(--text-3)', margin: '0 0 12px' }}>
|
|
روزهایی که تعطیل رسمی نیستند ولی این محیط بسته است. برای مرخصی یک نفر یا سرویس یک دستگاه،
|
|
از تقویم همان منبع استفاده کنید.
|
|
</p>
|
|
|
|
{overrides.length === 0 ? (
|
|
<p style={{ fontSize: 13, color: 'var(--text-3)', margin: 0 }}>موردی ثبت نشده است.</p>
|
|
) : (
|
|
<div style={{ display: 'grid', gap: 8, marginBottom: 12 }}>
|
|
{overrides.map((o) => (
|
|
<div key={o.uuid} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13 }}>
|
|
<span style={{ flex: 1, color: 'var(--text-2)' }}>
|
|
{formatDate(o.date)}{o.note ? ` · ${o.note}` : ''}
|
|
</span>
|
|
{canUpdate && (
|
|
<button type="button" className="mini-btn" onClick={() => onRemove(o.uuid)} aria-label="حذف تعطیلی">
|
|
✕
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{canUpdate && (
|
|
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'flex-end' }}>
|
|
{/* تقویم شمسی، نه `input type=date`: آن میلادی است، placeholderش انگلیسی
|
|
(`mm/dd/yyyy`) و در دارکمود سفید میماند چون کنترل نیتیو تم را نمیشناسد. */}
|
|
<div style={{ display: 'grid', gap: 6, minWidth: 190 }}>
|
|
<span style={{ fontSize: 12, color: 'var(--text-2)' }}>تاریخ</span>
|
|
<PersianDateInput
|
|
value={date}
|
|
onChange={setDate}
|
|
placeholder="روز تعطیل را انتخاب کنید"
|
|
ariaLabel="تاریخ تعطیلی"
|
|
/>
|
|
</div>
|
|
|
|
<div className="field-block" style={{ flex: 1, minWidth: 180 }}>
|
|
<label htmlFor="closure-note">توضیح</label>
|
|
<input
|
|
id="closure-note"
|
|
className="field"
|
|
value={note}
|
|
onChange={(e) => setNote(e.target.value)}
|
|
placeholder="اختیاری"
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
className="btn secondary"
|
|
disabled={saving || timestamp === null}
|
|
onClick={() => {
|
|
onAdd(timestamp!, note.trim() === '' ? null : note.trim());
|
|
setDate('');
|
|
setNote('');
|
|
}}
|
|
>
|
|
افزودن
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|