fix(holidays): honest dates and reachable fields on the holidays page
The table had two date columns for one date: "تاریخ" printed the raw 1405-05-13 string in Latin digits, and the column labelled "میلادی" ran the same day through formatDate — which returns Jalali. One date, twice, under a label that lied. It is now a single formatted Jalali column. The closure form used a native <input type="date">: Gregorian, an English mm/dd/yyyy placeholder in an RTL Persian panel, and a white box in dark mode because a native control does not follow the theme. It is the shared Persian picker now. That picker turned out to be a div with an onClick — no role, no tab stop, no accessible name, and its clear button was a span. Since every page that picks a date goes through it, it gained role/tabIndex/Enter-Space, an ariaLabel prop, and a real button for clear. The page passes labels for the year select and both form fields, and the global topbar search got an aria-label, which takes the runtime accessibility probe on this page to clean. useHolidays now returns an error, so a failed request reads as an error instead of an empty year — previously indistinguishable. The page had no test file; it has eight. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,7 +38,7 @@ export default function Topbar({ onMobileMenuOpen }: { onMobileMenuOpen?: () =>
|
|||||||
{/* Search bar — placeholder «جستجو» مطابق clinic-pro-tauri */}
|
{/* Search bar — placeholder «جستجو» مطابق clinic-pro-tauri */}
|
||||||
<div className="topbar-search">
|
<div className="topbar-search">
|
||||||
<MagnifyingGlassIcon style={{ width: 17, height: 17, flexShrink: 0 }} />
|
<MagnifyingGlassIcon style={{ width: 17, height: 17, flexShrink: 0 }} />
|
||||||
<input placeholder="جستجو" readOnly />
|
<input placeholder="جستجو" aria-label="جستجو" readOnly />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ interface Props {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
/** انتخاب سال→ماه→روز را فعال میکند (مناسب تاریخ تولد). */
|
/** انتخاب سال→ماه→روز را فعال میکند (مناسب تاریخ تولد). */
|
||||||
enableYearPicker?: boolean;
|
enableYearPicker?: boolean;
|
||||||
|
/** نام دسترسپذیر؛ تریگر `input` نیست پس `<label htmlFor>` رویش نمینشیند. */
|
||||||
|
ariaLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,9 +12,14 @@ interface Props {
|
|||||||
minWidth?: number;
|
minWidth?: number;
|
||||||
/** انتخاب سال→ماه→روز را فعال میکند (پیشفرض خاموش). */
|
/** انتخاب سال→ماه→روز را فعال میکند (پیشفرض خاموش). */
|
||||||
enableYearPicker?: boolean;
|
enableYearPicker?: boolean;
|
||||||
|
/**
|
||||||
|
* نام دسترسپذیر. تریگر یک `div` است نه `input`، پس `<label htmlFor>` رویش نمینشیند
|
||||||
|
* و بدون این، screen reader فقط «button» میشنود. پیشفرض روی placeholder میافتد.
|
||||||
|
*/
|
||||||
|
ariaLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PersianDatePicker({ value, onChange, placeholder = 'انتخاب تاریخ', height = 38, minWidth = 148, enableYearPicker = false }: Props) {
|
export default function PersianDatePicker({ value, onChange, placeholder = 'انتخاب تاریخ', height = 38, minWidth = 148, enableYearPicker = false, ariaLabel }: Props) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const triggerRef = useRef<HTMLDivElement>(null);
|
const triggerRef = useRef<HTMLDivElement>(null);
|
||||||
const [pos, setPos] = useState<{ top: number; right: number } | null>(null);
|
const [pos, setPos] = useState<{ top: number; right: number } | null>(null);
|
||||||
@@ -29,7 +34,16 @@ export default function PersianDatePicker({ value, onChange, placeholder = 'ان
|
|||||||
return (
|
return (
|
||||||
<div ref={triggerRef} style={{ position: 'relative', display: 'inline-block' }}>
|
<div ref={triggerRef} style={{ position: 'relative', display: 'inline-block' }}>
|
||||||
<div
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-label={ariaLabel ?? placeholder}
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
|
// با موس باز میشد و با کیبورد نه؛ Enter/Space همان کاری را میکنند که کلیک.
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); }
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
display: 'flex', alignItems: 'center', gap: 7,
|
display: 'flex', alignItems: 'center', gap: 7,
|
||||||
height, padding: '0 10px', borderRadius: 'var(--r-sm)',
|
height, padding: '0 10px', borderRadius: 'var(--r-sm)',
|
||||||
@@ -41,12 +55,17 @@ export default function PersianDatePicker({ value, onChange, placeholder = 'ان
|
|||||||
<CalendarDaysIcon style={{ width: 15, height: 15, color: 'var(--text-3)', flexShrink: 0 }} />
|
<CalendarDaysIcon style={{ width: 15, height: 15, color: 'var(--text-3)', flexShrink: 0 }} />
|
||||||
<span style={{ flex: 1 }}>{value ? formatDate(value) : placeholder}</span>
|
<span style={{ flex: 1 }}>{value ? formatDate(value) : placeholder}</span>
|
||||||
{value && (
|
{value && (
|
||||||
<span
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="پاک کردن تاریخ"
|
||||||
onClick={(e) => { e.stopPropagation(); onChange(''); }}
|
onClick={(e) => { e.stopPropagation(); onChange(''); }}
|
||||||
style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', color: 'var(--text-3)' }}
|
style={{
|
||||||
|
display: 'flex', alignItems: 'center', cursor: 'pointer', color: 'var(--text-3)',
|
||||||
|
background: 'none', border: 0, padding: 0,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<XMarkIcon style={{ width: 13, height: 13 }} />
|
<XMarkIcon style={{ width: 13, height: 13 }} />
|
||||||
</span>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,9 @@ export function useHolidays(year: number) {
|
|||||||
holidays: query.data?.data?.holidays ?? [],
|
holidays: query.data?.data?.holidays ?? [],
|
||||||
overrides: query.data?.data?.overrides ?? [],
|
overrides: query.data?.data?.overrides ?? [],
|
||||||
loading: query.isLoading,
|
loading: query.isLoading,
|
||||||
|
// بدون این، درخواستِ شکستخورده مثل «سالِ خالی» دیده میشود و کاربر دنبال
|
||||||
|
// تعطیلاتی میگردد که ثبت شدهاند ولی نیامدهاند.
|
||||||
|
error: query.isError ? ((query.error as Error)?.message || 'خطای نامشخص') : null,
|
||||||
setOverride,
|
setOverride,
|
||||||
removeOverride,
|
removeOverride,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { renderWithProviders } from '../test/utils';
|
||||||
|
|
||||||
|
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
||||||
|
vi.mock('../hooks/usePermissions', () => ({ usePermissions: () => ({ can: () => true }) }));
|
||||||
|
|
||||||
|
const setOverride = { mutate: vi.fn(), isPending: false };
|
||||||
|
const removeOverride = { mutate: vi.fn(), isPending: false };
|
||||||
|
|
||||||
|
let holidays: Array<{ uuid: string; date: number; jalali_date: string; title: string }> = [];
|
||||||
|
let overrides: Array<{ uuid: string; date: number; is_working: boolean; note: string | null }> = [];
|
||||||
|
let error: string | null = null;
|
||||||
|
|
||||||
|
vi.mock('../hooks/useResourceCalendar', () => ({
|
||||||
|
useHolidays: () => ({ holidays, overrides, loading: false, error, setOverride, removeOverride }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import HolidaysSettingsPage from './HolidaysSettingsPage';
|
||||||
|
|
||||||
|
// ۱۴۰۵-۰۵-۱۳ ⇒ ۲۰۲۶-۰۸-۰۴
|
||||||
|
const MARTYRDOM = {
|
||||||
|
uuid: 'h-1',
|
||||||
|
date: Math.floor(new Date(2026, 7, 4).getTime() / 1000),
|
||||||
|
jalali_date: '1405-05-13',
|
||||||
|
title: 'شهادت',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('HolidaysSettingsPage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
holidays = [MARTYRDOM];
|
||||||
|
overrides = [];
|
||||||
|
error = null;
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* پیشتر دو ستون تاریخ بود: «تاریخ» رشتهٔ خامِ `1405-05-13` و «میلادی» که با
|
||||||
|
* formatDate همان روز را **شمسی** میداد. یک تاریخ، دو بار، با برچسبی که دروغ میگفت.
|
||||||
|
*/
|
||||||
|
it('یک ستون تاریخ دارد و شمسی است، نه رشتهٔ خام', () => {
|
||||||
|
renderWithProviders(<HolidaysSettingsPage />);
|
||||||
|
|
||||||
|
expect(screen.getByText('۱۴۰۵/۰۵/۱۳')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('1405-05-13')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('میلادی')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('روز تعطیل را برای این محیط باز میکند', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
renderWithProviders(<HolidaysSettingsPage />);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'این روز باز است' }));
|
||||||
|
|
||||||
|
expect(setOverride.mutate).toHaveBeenCalledWith({ date: MARTYRDOM.date, is_working: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('روزِ استثنا خورده را دوباره تعطیل میکند', async () => {
|
||||||
|
overrides = [{ uuid: 'o-1', date: MARTYRDOM.date, is_working: true, note: null }];
|
||||||
|
const user = userEvent.setup();
|
||||||
|
renderWithProviders(<HolidaysSettingsPage />);
|
||||||
|
|
||||||
|
expect(screen.getByText('باز است')).toBeInTheDocument();
|
||||||
|
await user.click(screen.getByRole('button', { name: 'تعطیل کن' }));
|
||||||
|
|
||||||
|
expect(removeOverride.mutate).toHaveBeenCalledWith('o-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
/** بدون این، درخواستِ شکستخورده مثل «سالِ خالی» دیده میشد. */
|
||||||
|
it('خطای سرور را نشان میدهد، نه سالِ خالی', () => {
|
||||||
|
error = 'خطای داخلی سرور';
|
||||||
|
holidays = [];
|
||||||
|
renderWithProviders(<HolidaysSettingsPage />);
|
||||||
|
|
||||||
|
expect(screen.getByText('خطای داخلی سرور')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('سالِ خالی را با ارقام فارسی میگوید', () => {
|
||||||
|
holidays = [];
|
||||||
|
renderWithProviders(<HolidaysSettingsPage />, { route: '/admin/holidays?year=1404' });
|
||||||
|
|
||||||
|
expect(screen.getByText('برای سال ۱۴۰۴ تعطیلی ثبت نشده است')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** انتخابگر سال و ورودی تاریخ هر دو باید نام دسترسپذیر داشته باشند. */
|
||||||
|
it('فیلدها نام دارند', () => {
|
||||||
|
renderWithProviders(<HolidaysSettingsPage />);
|
||||||
|
|
||||||
|
expect(screen.getByRole('button', { name: 'تاریخ تعطیلی' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText('توضیح')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** ورودی نیتیو میلادی بود و در دارکمود سفید میماند؛ باید رفته باشد. */
|
||||||
|
it('ورودی تاریخ نیتیو ندارد', () => {
|
||||||
|
const { container } = renderWithProviders(<HolidaysSettingsPage />);
|
||||||
|
|
||||||
|
expect(container.querySelector('input[type="date"]')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('تعطیلی محیط را با تاریخ و توضیح ثبت میکند', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
renderWithProviders(<HolidaysSettingsPage />);
|
||||||
|
|
||||||
|
// بدون تاریخ، ثبت ممکن نیست.
|
||||||
|
expect(screen.getByRole('button', { name: 'افزودن' })).toBeDisabled();
|
||||||
|
|
||||||
|
await user.type(screen.getByLabelText('توضیح'), 'تعطیلی داخلی');
|
||||||
|
expect(screen.getByRole('button', { name: 'افزودن' })).toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import PageHeader from '../components/ui/PageHeader';
|
import PageHeader from '../components/ui/PageHeader';
|
||||||
import DataTable, { type Column } from '../components/ui/DataTable';
|
import DataTable, { type Column } from '../components/ui/DataTable';
|
||||||
import SearchableSelect from '../components/ui/SearchableSelect';
|
import SearchableSelect from '../components/ui/SearchableSelect';
|
||||||
|
import PersianDateInput from '../components/ui/PersianDateInput';
|
||||||
import { useUrlState } from '../hooks/useUrlState';
|
import { useUrlState } from '../hooks/useUrlState';
|
||||||
import { usePermissions } from '../hooks/usePermissions';
|
import { usePermissions } from '../hooks/usePermissions';
|
||||||
import { useHolidays } from '../hooks/useResourceCalendar';
|
import { useHolidays } from '../hooks/useResourceCalendar';
|
||||||
@@ -9,31 +10,33 @@ import { formatDate, currentJalaliYear } from '../lib/utils';
|
|||||||
import type { NationalHoliday } from '../types';
|
import type { NationalHoliday } from '../types';
|
||||||
import SettingsLayout from '../components/layout/SettingsLayout';
|
import SettingsLayout from '../components/layout/SettingsLayout';
|
||||||
|
|
||||||
|
/** سال بدون جداکنندهٔ هزارگان — `formatNumber` «۱٬۴۰۵» میداد. */
|
||||||
|
const faYear = (y: number) => y.toLocaleString('fa-IR', { useGrouping: false });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* تعطیلات رسمی و استثناهای این محیط.
|
* تعطیلات رسمی و استثناهای این محیط.
|
||||||
*
|
*
|
||||||
* خودِ تعطیلات کشوریاند و اینجا فقط دیده میشوند؛ آنچه محیط تغییر میدهد «باز بودن
|
* خودِ تعطیلات کشوریاند و اینجا فقط دیده میشوند؛ آنچه محیط تغییر میدهد «باز بودن
|
||||||
* یا نبودنِ» همان روز برای خودش است.
|
* یا نبودنِ» همان روز برای خودش است. ساخت و حذفِ خودِ تعطیلی کارِ مدیر سیستم است
|
||||||
|
* (`/admin/national-holidays`).
|
||||||
*/
|
*/
|
||||||
export default function HolidaysSettingsPage() {
|
export default function HolidaysSettingsPage() {
|
||||||
const thisYear = currentJalaliYear();
|
const thisYear = currentJalaliYear();
|
||||||
const [urlState, setUrlState] = useUrlState({ year: String(thisYear) });
|
const [urlState, setUrlState] = useUrlState({ year: String(thisYear) });
|
||||||
const year = Number(urlState.year) || thisYear;
|
const year = Number(urlState.year) || thisYear;
|
||||||
|
|
||||||
const { holidays, overrides, loading, setOverride, removeOverride } = useHolidays(year);
|
const { holidays, overrides, loading, error, setOverride, removeOverride } = useHolidays(year);
|
||||||
const { can } = usePermissions();
|
const { can } = usePermissions();
|
||||||
const canUpdate = can('appointment_settings', 'update');
|
const canUpdate = can('appointment_settings', 'update');
|
||||||
|
|
||||||
const overrideByDate = new Map(overrides.map((o) => [o.date, o]));
|
const overrideByDate = new Map(overrides.map((o) => [o.date, o]));
|
||||||
|
|
||||||
const columns: Column<NationalHoliday>[] = [
|
const columns: Column<NationalHoliday>[] = [
|
||||||
{ key: 'jalali_date', header: 'تاریخ', render: (h) => <span style={{ fontWeight: 600 }}>{h.jalali_date}</span> },
|
// یک ستون تاریخ، نه دو تا: پیشتر «تاریخ» رشتهٔ خامِ `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: 'title', header: 'مناسبت', render: (h) => <span style={{ fontSize: 13 }}>{h.title}</span> },
|
||||||
{
|
|
||||||
key: 'gregorian',
|
|
||||||
header: 'میلادی',
|
|
||||||
render: (h) => <span style={{ fontSize: 12, color: 'var(--text-3)' }}>{formatDate(h.date)}</span>,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'status',
|
key: 'status',
|
||||||
header: 'وضعیت این محیط',
|
header: 'وضعیت این محیط',
|
||||||
@@ -57,21 +60,34 @@ export default function HolidaysSettingsPage() {
|
|||||||
backTo="/admin/settings-menu"
|
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
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={holidays}
|
data={holidays}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
emptyMessage={`برای سال ${year} تعطیلی ثبت نشده است`}
|
emptyMessage={`برای سال ${faYear(year)} تعطیلی ثبت نشده است`}
|
||||||
headerExtra={
|
headerExtra={
|
||||||
<div style={{ minWidth: 160, marginRight: 'auto' }}>
|
<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
|
<SearchableSelect
|
||||||
options={years.map((y) => ({ value: String(y), label: String(y) }))}
|
options={years.map((y) => ({ value: String(y), label: faYear(y) }))}
|
||||||
value={String(year)}
|
value={String(year)}
|
||||||
onChange={(v) => setUrlState({ year: v ? String(v) : String(thisYear) })}
|
onChange={(v) => setUrlState({ year: v ? String(v) : String(thisYear) })}
|
||||||
placeholder="سال"
|
placeholder="سال"
|
||||||
|
ariaLabelledBy="holidays-year-label"
|
||||||
height={36}
|
height={36}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
canUpdate
|
canUpdate
|
||||||
@@ -95,7 +111,7 @@ export default function HolidaysSettingsPage() {
|
|||||||
disabled={setOverride.isPending}
|
disabled={setOverride.isPending}
|
||||||
onClick={() => setOverride.mutate({ date: h.date, is_working: true })}
|
onClick={() => setOverride.mutate({ date: h.date, is_working: true })}
|
||||||
>
|
>
|
||||||
این روز بازیم
|
این روز باز است
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -136,7 +152,7 @@ function ClosureCard({
|
|||||||
const timestamp = date === '' ? null : Math.floor(new Date(`${date}T00:00:00`).getTime() / 1000);
|
const timestamp = date === '' ? null : Math.floor(new Date(`${date}T00:00:00`).getTime() / 1000);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card" style={{ padding: 16, marginTop: 16 }}>
|
<div className="card card-pad" style={{ marginTop: 'var(--gap)' }}>
|
||||||
<h2 className="section-title" style={{ margin: '0 0 4px' }}>تعطیلیهای این محیط</h2>
|
<h2 className="section-title" style={{ margin: '0 0 4px' }}>تعطیلیهای این محیط</h2>
|
||||||
<p style={{ fontSize: 12, color: 'var(--text-3)', margin: '0 0 12px' }}>
|
<p style={{ fontSize: 12, color: 'var(--text-3)', margin: '0 0 12px' }}>
|
||||||
روزهایی که تعطیل رسمی نیستند ولی این محیط بسته است. برای مرخصی یک نفر یا سرویس یک دستگاه،
|
روزهایی که تعطیل رسمی نیستند ولی این محیط بسته است. برای مرخصی یک نفر یا سرویس یک دستگاه،
|
||||||
@@ -153,7 +169,7 @@ function ClosureCard({
|
|||||||
{formatDate(o.date)}{o.note ? ` · ${o.note}` : ''}
|
{formatDate(o.date)}{o.note ? ` · ${o.note}` : ''}
|
||||||
</span>
|
</span>
|
||||||
{canUpdate && (
|
{canUpdate && (
|
||||||
<button type="button" className="btn secondary sm" onClick={() => onRemove(o.uuid)} aria-label="حذف تعطیلی">
|
<button type="button" className="mini-btn" onClick={() => onRemove(o.uuid)} aria-label="حذف تعطیلی">
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -163,9 +179,30 @@ function ClosureCard({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{canUpdate && (
|
{canUpdate && (
|
||||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'flex-end' }}>
|
||||||
<input type="date" className="field" value={date} onChange={(e) => setDate(e.target.value)} style={{ minWidth: 170 }} />
|
{/* تقویم شمسی، نه `input type=date`: آن میلادی است، placeholderش انگلیسی
|
||||||
<input className="field" value={note} onChange={(e) => setNote(e.target.value)} placeholder="توضیح (اختیاری)" style={{ flex: 1, minWidth: 180 }} />
|
(`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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn secondary"
|
className="btn secondary"
|
||||||
|
|||||||
Reference in New Issue
Block a user