feat: replace checkboxes with Switch component for better UI consistency

- Updated DoctorDetailPage, MySecretariesPage, RecordNumberSettingsPage, RepresentationsPage, ResourcePoolsPage, ResourceTypesPage, SecretariesPage, SecretaryDetailPage, SettingsPage, SkillsPage, SmsWalletPage, and TagsSettingsPage to use the new Switch component instead of native checkboxes.
- Enhanced accessibility by ensuring the Switch component uses appropriate roles and labels.
- Added tests for the new Switch component to ensure functionality and accessibility compliance.
- Updated styles to accommodate the new Switch component design.
This commit is contained in:
hamed
2026-08-05 16:09:46 +03:30
parent f9bbdf1e7c
commit 0e7970d6e0
38 changed files with 634 additions and 350 deletions
@@ -1,7 +1,11 @@
import React, { useEffect, useState } from 'react';
import { XMarkIcon } from '@heroicons/react/24/outline';
import Modal from '../ui/Modal';
import { useQuery } from '@tanstack/react-query';
import SearchableSelect from '../ui/SearchableSelect';
import Field from '../ui/Field';
import Input from '../ui/Input';
import Switch from '../ui/Switch';
import { api, type ApiResponse } from '../../lib/api';
import type { ClinicResource, ResourcePayload, ResourceType } from '../../types';
import { useResourceDetail } from '../../hooks/useResources';
@@ -98,8 +102,14 @@ export default function ResourceFormModal({
return (
<Modal open={open} onClose={onClose} title={isEdit ? 'ویرایش منبع' : 'افزودن منبع'} size="lg">
<div style={{ display: 'grid', gap: 14 }}>
<Field label="نام منبع">
<input className="field" value={name} onChange={(e) => setName(e.target.value)} placeholder="لیزر آلکساندرایت ۱" />
<Field label="نام منبع" htmlFor="resource-name">
<Input
id="resource-name"
value={name}
autoFocus
onChange={(e) => setName(e.target.value)}
placeholder="لیزر آلکساندرایت ۱"
/>
</Field>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 12 }}>
@@ -134,14 +144,17 @@ export default function ResourceFormModal({
)}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 12 }}>
<Field label="ظرفیت هم‌زمان">
<input className="field" type="number" min={1} value={capacity} onChange={(e) => setCapacity(e.target.value)} />
{/* `numeric` به‌جای `type="number"`: ورودیِ عددیِ فارسی در `type="number"`
نامعتبر است و مرورگر رشتهٔ خالی می‌دهد — یعنی «۳» تایپ‌شده به ۰ می‌رسید.
`Input numeric` ارقام را زنده به لاتین برمی‌گرداند. */}
<Field label="ظرفیت هم‌زمان" htmlFor="resource-capacity">
<Input id="resource-capacity" numeric value={capacity} onChange={(e) => setCapacity(e.target.value)} />
</Field>
<Field label="آماده‌سازی (دقیقه)">
<input className="field" type="number" min={0} value={setupMinutes} onChange={(e) => setSetupMinutes(e.target.value)} />
<Field label="آماده‌سازی (دقیقه)" htmlFor="resource-setup">
<Input id="resource-setup" numeric value={setupMinutes} onChange={(e) => setSetupMinutes(e.target.value)} />
</Field>
<Field label="تمیزکاری (دقیقه)">
<input className="field" type="number" min={0} value={cleanupMinutes} onChange={(e) => setCleanupMinutes(e.target.value)} />
<Field label="تمیزکاری (دقیقه)" htmlFor="resource-cleanup">
<Input id="resource-cleanup" numeric value={cleanupMinutes} onChange={(e) => setCleanupMinutes(e.target.value)} />
</Field>
</div>
@@ -152,7 +165,7 @@ export default function ResourceFormModal({
<div style={{ display: 'grid', gap: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<label style={{ fontSize: 12, color: 'var(--text-2)' }}>ویژگیها (اختیاری)</label>
<span className="cp-label">ویژگیها (اختیاری)</span>
<button
type="button"
className="btn secondary sm"
@@ -164,32 +177,33 @@ export default function ResourceFormModal({
{attributes.map((row, index) => (
<div key={index} style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<input
className="field"
<Input
list="resource-attribute-keys"
value={row.key}
placeholder="gender"
aria-label={`کلید ویژگی ${formatNumber(index + 1)}`}
onChange={(e) =>
setAttributes((a) => a.map((r, i) => (i === index ? { ...r, key: e.target.value } : r)))
}
style={{ flex: 1 }}
/>
<input
className="field"
<Input
value={row.value}
placeholder="female"
aria-label={`مقدار ویژگی ${formatNumber(index + 1)}`}
onChange={(e) =>
setAttributes((a) => a.map((r, i) => (i === index ? { ...r, value: e.target.value } : r)))
}
style={{ flex: 1 }}
/>
{/* دکمهٔ فقط-آیکون → `mini-btn`، نه `btn secondary sm` با یک ✕ متنی */}
<button
type="button"
className="btn secondary sm"
className="mini-btn danger"
onClick={() => setAttributes((a) => a.filter((_, i) => i !== index))}
aria-label="حذف ویژگی"
aria-label={`حذف ویژگی ${formatNumber(index + 1)}`}
>
<XMarkIcon style={{ width: 16 }} />
</button>
</div>
))}
@@ -199,10 +213,13 @@ export default function ResourceFormModal({
</datalist>
</div>
<label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13 }}>
<input type="checkbox" checked={active} onChange={(e) => setActive(e.target.checked)} />
منبع فعال است
</label>
<Switch
id="resource-active"
checked={active}
onChange={setActive}
label="منبع فعال است"
hint="منبع غیرفعال در جستجوی وقت و رزرو نوبت نمی‌آید."
/>
{/* غیرفعال‌کردن نوبت‌های ثبت‌شده را لغو نمی‌کند؛ فقط از جستجوی وقتِ بعدی حذف
می‌شود. پس این هشدار است نه مانع — ولی اپراتور باید بداند چند بیمار روی
@@ -233,12 +250,3 @@ export default function ResourceFormModal({
</Modal>
);
}
function Field({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div style={{ display: 'grid', gap: 6 }}>
<label style={{ fontSize: 12, color: 'var(--text-2)' }}>{label}</label>
{children}
</div>
);
}
@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import SearchableSelect from '../ui/SearchableSelect';
import Switch from '../ui/Switch';
import { formatRial } from '../../lib/utils';
import type { ClinicResource, ResourceServiceOffering, ServiceItemOption } from '../../types';
@@ -106,14 +107,13 @@ export default function ResourceServicesPanel({
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ flex: 1, fontSize: 13, fontWeight: 600 }}>{line.service_name}</span>
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'var(--text-2)' }}>
<input
type="checkbox"
checked={line.active}
onChange={(e) => patch(index, { active: e.target.checked })}
/>
فعال
</label>
<Switch
inline
checked={line.active}
onChange={(v) => patch(index, { active: v })}
label="فعال"
ariaLabel={`فعال بودن سرویس ${line.service_name}`}
/>
<button
type="button"
@@ -1,6 +1,7 @@
import React, { useEffect, useMemo, useState } from 'react';
import { ExclamationTriangleIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
import { useResourceCalendar } from '../../hooks/useResourceCalendar';
import Switch from '../ui/Switch';
import { formatNumber } from '../../lib/utils';
/** ۰ = شنبه — همان قرارداد بک‌اند برای روزهای هفته. */
@@ -197,15 +198,16 @@ export default function ResourceWorkingHoursPanel({ resourceUuid, canUpdate }: {
)}
</label>
<label className="wh-eod">
<input
type="checkbox"
<span className="wh-eod">
<Switch
inline
checked={row.endOfDay}
disabled={!canUpdate}
onChange={(e) => editRange(day, index, { endOfDay: e.target.checked })}
onChange={(v) => editRange(day, index, { endOfDay: v })}
label="تا پایان روز"
ariaLabel={`تا پایان روز برای شیفت ${formatNumber(index + 1)} روز ${label}`}
/>
تا پایان روز
</label>
</span>
{canUpdate && (
<button