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:
@@ -85,6 +85,13 @@ const PLAN_DISPLAY: Record<string, string> = {
|
||||
|
||||
// ── Switch toggle ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* نسخهٔ uncontrolled سوییچ برای `react-hook-form`.
|
||||
*
|
||||
* `components/ui/Switch` کنترلشده است (`checked` + `onChange`) و با اسپردِ
|
||||
* `register()` — که `ref` و `onChange` نیتیو میدهد — جور در نمیآید. ظاهر هر دو از
|
||||
* یک کلاس `.switch` میآید، پس تفاوت دیداری ندارند.
|
||||
*/
|
||||
const SwitchToggle = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
|
||||
function SwitchToggle(props, ref) {
|
||||
return (
|
||||
|
||||
@@ -20,6 +20,7 @@ import SlotPicker, { type PickedSlot } from '../components/appointments/SlotPick
|
||||
import { tehranWallClockToUnix, rialToToman, tomanToRial } from '../lib/utils';
|
||||
import BackButton from '../components/ui/BackButton';
|
||||
import { digitsOnly, todayIso } from '../lib/utils';
|
||||
import Switch from '../components/ui/Switch';
|
||||
|
||||
/**
|
||||
* افزودن نوبت — صفحهٔ کامل (بازسازی `CreateTurn.jsx` طرح tauri). از همان endpointهای
|
||||
@@ -611,20 +612,12 @@ export default function AppointmentCreatePage() {
|
||||
|
||||
{/* بیعانه */}
|
||||
<div style={sectionTitle}>بیعانه</div>
|
||||
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 10, fontSize: 14, cursor: 'pointer' }}>
|
||||
<span style={{
|
||||
position: 'relative', width: 42, height: 22, borderRadius: 999, flexShrink: 0,
|
||||
background: depositRequired ? 'var(--primary)' : 'var(--border-2)', transition: 'background .2s',
|
||||
}}>
|
||||
<input type="checkbox" checked={depositRequired} onChange={e => setDepositRequired(e.target.checked)}
|
||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', margin: 0, opacity: 0, cursor: 'pointer' }} />
|
||||
<span style={{
|
||||
position: 'absolute', top: 2, insetInlineStart: depositRequired ? 22 : 2, width: 18, height: 18,
|
||||
borderRadius: 999, background: 'var(--surface)', transition: 'inset-inline-start .2s', boxShadow: '0 1px 2px rgba(0,0,0,.2)',
|
||||
}} />
|
||||
</span>
|
||||
بیعانه مورد نیاز است.
|
||||
</label>
|
||||
<Switch
|
||||
inline
|
||||
checked={depositRequired}
|
||||
onChange={setDepositRequired}
|
||||
label="بیعانه مورد نیاز است."
|
||||
/>
|
||||
{depositRequired && (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'flex-end', gap: 12, margin: '12px 0' }}>
|
||||
<div style={{ width: 300, maxWidth: '100%' }}>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useAppointmentInsurance } from '../hooks/useAppointmentInsurance';
|
||||
import { useDoctorBookingServices } from '../hooks/useDoctorBookingServices';
|
||||
import ServiceSlotPicker from '../components/appointments/ServiceSlotPicker';
|
||||
import type { PickedService, ServicePick } from '../components/appointments/ServiceSlotPicker';
|
||||
import Switch from '../components/ui/Switch';
|
||||
|
||||
interface Option { uuid: string; name?: string; full_name?: string }
|
||||
|
||||
@@ -358,10 +359,12 @@ export default function AppointmentEditPage() {
|
||||
|
||||
<div style={{ fontSize: 14, fontWeight: 700, marginBottom: 12 }}>بیعانه:</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap', marginBottom: 18 }}>
|
||||
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 13, cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={depositRequired} onChange={e => setDepositRequired(e.target.checked)} />
|
||||
بیعانه مورد نیاز است.
|
||||
</label>
|
||||
<Switch
|
||||
inline
|
||||
checked={depositRequired}
|
||||
onChange={setDepositRequired}
|
||||
label="بیعانه مورد نیاز است."
|
||||
/>
|
||||
{depositRequired && (
|
||||
<>
|
||||
<div style={{ minWidth: 220 }}>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { usePermissions } from '../hooks/usePermissions';
|
||||
import { useCatalogCategories, useCategoryIncludes } from '../hooks/useCatalogCategories';
|
||||
import type { CatalogCategory } from '../types';
|
||||
import SettingsLayout from '../components/layout/SettingsLayout';
|
||||
import Switch from '../components/ui/Switch';
|
||||
|
||||
/** یک ردیف از درخت، صافشده — با عمق، تا تورفتگی نشان دهد کجای درخت است. */
|
||||
type Row = CatalogCategory & { depth: number };
|
||||
@@ -204,10 +205,7 @@ function CategoryFormModal({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, color: 'var(--text-2)' }}>
|
||||
<input type="checkbox" checked={active} onChange={(e) => setActive(e.target.checked)} />
|
||||
فعال
|
||||
</label>
|
||||
<Switch inline checked={active} onChange={setActive} label="فعال" />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
||||
|
||||
@@ -26,6 +26,7 @@ import NotificationMobileCard from '../components/ui/NotificationMobileCard';
|
||||
import ClinicDoctorsManager from '../components/ClinicDoctorsManager';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import { latinDigitsField } from '../lib/forms';
|
||||
import Switch from '../components/ui/Switch';
|
||||
|
||||
// Fix leaflet icons
|
||||
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
||||
@@ -107,10 +108,9 @@ function MultiCheckList({ options, selected, onChange, placeholder }: {
|
||||
{filtered.length === 0
|
||||
? <p style={{ textAlign: 'center', padding: '10px 0', fontSize: 12, color: 'var(--text-3)' }}>نتیجهای یافت نشد</p>
|
||||
: filtered.map(o => (
|
||||
<label key={o.id} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '7px 12px', cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={selected.includes(o.id)} onChange={() => toggle(o.id)} />
|
||||
<span style={{ fontSize: 13, color: 'var(--text)' }}>{o.name}</span>
|
||||
</label>
|
||||
<div key={o.id} style={{ padding: '7px 12px' }}>
|
||||
<Switch inline checked={selected.includes(o.id)} onChange={() => toggle(o.id)} label={o.name} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -281,10 +281,12 @@ function EditModal({ clinic, onClose, onSaved }: {
|
||||
<label style={{ fontSize: 13, fontWeight: 600, display: 'block', marginBottom: 6 }}>توضیحات</label>
|
||||
<textarea className="input" rows={3} style={{ resize: 'vertical' }} {...register('info')} />
|
||||
</div>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer', userSelect: 'none' }}>
|
||||
<input type="checkbox" {...register('is_247')} />
|
||||
<span style={{ fontSize: 13, fontWeight: 600 }}>کلینیک ۲۴ ساعته (۷ روز هفته)</span>
|
||||
</label>
|
||||
<Switch
|
||||
inline
|
||||
checked={watch('is_247') ?? false}
|
||||
onChange={(v) => setValue('is_247', v, { shouldDirty: true })}
|
||||
label="کلینیک ۲۴ ساعته (۷ روز هفته)"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import ConfirmDialog from '../components/ui/ConfirmDialog';
|
||||
import ServiceInsuranceModal from '../components/ServiceInsuranceModal';
|
||||
import ServiceItemFormModal from '../components/ServiceItemFormModal';
|
||||
import FeatureGate from '../components/ui/FeatureGate';
|
||||
import Switch from '../components/ui/Switch';
|
||||
|
||||
const sectionSchema = z.object({ name: z.string().min(1, 'نام بخش الزامی است') });
|
||||
type SectionForm = z.infer<typeof sectionSchema>;
|
||||
@@ -172,10 +173,11 @@ function ClinicServicesPageInner() {
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '4px 0' }} onClick={(e) => e.stopPropagation()}>
|
||||
<span style={{ fontSize: 12.5, color: 'var(--text-3)' }}>وضعیت:</span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<label className="switch" title={s.active ? 'فعال' : 'غیرفعال'}>
|
||||
<input type="checkbox" checked={s.active} onChange={() => toggleSection.mutate({ uuid: s.uuid, active: !s.active })} />
|
||||
<span className="switch-track"><span className="switch-thumb" /></span>
|
||||
</label>
|
||||
<Switch
|
||||
checked={s.active}
|
||||
onChange={() => toggleSection.mutate({ uuid: s.uuid, active: !s.active })}
|
||||
ariaLabel={`وضعیت ${s.name}`}
|
||||
/>
|
||||
<span style={{ fontSize: 12.5, color: s.active ? 'var(--success)' : 'var(--text-3)' }}>{s.active ? 'فعال' : 'غیرفعال'}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,7 @@ import type { AddressData } from '../components/schedule/ScheduleSection';
|
||||
import { latinDigitsField } from '../lib/forms';
|
||||
import BackButton from '../components/ui/BackButton';
|
||||
import { toggleSpecialtyChild, toggleSpecialtyRoot, removeSpecialtyEntry } from '../lib/specialtySelection';
|
||||
import Switch from '../components/ui/Switch';
|
||||
|
||||
// Fix leaflet default marker icons
|
||||
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
||||
@@ -396,8 +397,7 @@ function HierarchicalSpecialtyPicker({ selected, onChange, specialties }: {
|
||||
filteredFlat.length > 0
|
||||
? filteredFlat.map(s => (
|
||||
<label key={s.id} className="flex items-center gap-2.5 px-3 py-2 hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] cursor-pointer">
|
||||
<input type="checkbox" checked={selected.includes(s.id)} onChange={() => toggleSelect(s.id)}
|
||||
className="rounded border-[var(--border-2)] text-[var(--primary)] shrink-0" />
|
||||
<Switch checked={selected.includes(s.id)} onChange={() => toggleSelect(s.id)} ariaLabel={s.name} />
|
||||
<span className="text-sm text-[var(--text)]">{s.name}</span>
|
||||
{s.parent_id !== null && (
|
||||
<span className="text-[10px] text-[var(--text-3)] mr-auto truncate max-w-[100px]">
|
||||
@@ -420,8 +420,9 @@ function HierarchicalSpecialtyPicker({ selected, onChange, specialties }: {
|
||||
? (isOpen
|
||||
? <ChevronDownIcon className="w-4 h-4 text-[var(--text-3)] shrink-0" />
|
||||
: <ChevronRightIcon className="w-4 h-4 text-[var(--text-3)] shrink-0" />)
|
||||
: <input type="checkbox" checked={selected.includes(parent.id)} readOnly
|
||||
className="rounded border-[var(--border-2)] text-[var(--primary)] pointer-events-none shrink-0" />
|
||||
: <span className="pointer-events-none shrink-0">
|
||||
<Switch checked={selected.includes(parent.id)} onChange={() => {}} disabled ariaLabel={parent.name} />
|
||||
</span>
|
||||
}
|
||||
<span className={`text-sm font-medium ${numSel > 0 ? 'text-[var(--primary)] dark:text-[var(--primary)]' : 'text-[var(--text)]'}`}>
|
||||
{parent.name}
|
||||
@@ -434,8 +435,7 @@ function HierarchicalSpecialtyPicker({ selected, onChange, specialties }: {
|
||||
</div>
|
||||
{isOpen && children.map(child => (
|
||||
<label key={child.id} className="flex items-center gap-2.5 px-3 py-2 pr-9 hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] cursor-pointer">
|
||||
<input type="checkbox" checked={selected.includes(child.id)} onChange={() => toggleSelect(child.id)}
|
||||
className="rounded border-[var(--border-2)] text-[var(--primary)] shrink-0" />
|
||||
<Switch checked={selected.includes(child.id)} onChange={() => toggleSelect(child.id)} ariaLabel={child.name} />
|
||||
<span className="text-sm text-[var(--text-2)]">{child.name}</span>
|
||||
</label>
|
||||
))}
|
||||
@@ -528,8 +528,7 @@ function ServicesPicker({ selected, onChange, services, specialties, selectedSpe
|
||||
<div className="max-h-44 overflow-y-auto">
|
||||
{visibleServices.map(svc => (
|
||||
<label key={svc.id} className="flex items-center gap-2.5 px-3 py-2 hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] cursor-pointer">
|
||||
<input type="checkbox" checked={selected.includes(svc.id)} onChange={() => toggle(svc.id)}
|
||||
className="rounded border-[var(--border-2)] text-[var(--success)] shrink-0" />
|
||||
<Switch checked={selected.includes(svc.id)} onChange={() => toggle(svc.id)} ariaLabel={svc.name} />
|
||||
<span className="text-sm text-[var(--text)]">{svc.name}</span>
|
||||
</label>
|
||||
))}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { formatDate, digitsOnly, IRAN_MOBILE_RE, IRAN_NATIONAL_CODE_RE } from ".
|
||||
import { useSubscription } from "../hooks/useSubscription";
|
||||
import { useAuthStore } from "../stores/authStore";
|
||||
import type { Secretary, SecretaryPermissions } from "../types";
|
||||
import Switch from '../components/ui/Switch';
|
||||
|
||||
// ── SVG icons (copied verbatim from clinic-pro-tauri) ───────────────────────
|
||||
|
||||
@@ -298,15 +299,11 @@ function PermissionAccordions({
|
||||
key={item.key}
|
||||
className="flex items-center gap-[11px] cursor-pointer py-[9px] min-h-[42px]"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
<Switch
|
||||
checked={sectionPerm?.[item.key] ?? false}
|
||||
disabled={disabled}
|
||||
onChange={(e) =>
|
||||
onChange(section.key, item.key, e.target.checked)
|
||||
}
|
||||
className="w-[20px] h-[20px] shrink-0"
|
||||
style={{ accentColor: "var(--primary)", cursor: "pointer" }}
|
||||
onChange={(v) => onChange(section.key, item.key, v)}
|
||||
ariaLabel={item.label}
|
||||
/>
|
||||
<span className="text-[var(--text-2)] text-[14px]">
|
||||
{item.label}
|
||||
@@ -872,12 +869,7 @@ function DoctorMultiSelect({
|
||||
(checked ? "bg-[var(--primary-soft)] dark:bg-[var(--surface-3)]" : "hover:bg-[var(--surface)] dark:hover:bg-[var(--surface-2)]")
|
||||
}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="accent-[var(--primary)] w-[16px] h-[16px]"
|
||||
checked={checked}
|
||||
onChange={() => toggle(d.uuid)}
|
||||
/>
|
||||
<Switch checked={checked} onChange={() => toggle(d.uuid)} ariaLabel={d.name} />
|
||||
<Avatar name={d.name} size={26} />
|
||||
<span className="text-[13px] text-[var(--text)]">{d.name}</span>
|
||||
</label>
|
||||
|
||||
@@ -3,6 +3,7 @@ import SettingsLayout from '../components/layout/SettingsLayout';
|
||||
import SearchableSelect from '../components/ui/SearchableSelect';
|
||||
import { useRecordNumberSettings } from '../hooks/useRecordNumberSettings';
|
||||
import type { RecordNumberResetPolicy } from '../hooks/useRecordNumberSettings';
|
||||
import Switch from '../components/ui/Switch';
|
||||
|
||||
const RESET_OPTIONS: { value: RecordNumberResetPolicy; label: string }[] = [
|
||||
{ value: 'none', label: 'هرگز — شمارنده پیوسته جلو میرود' },
|
||||
@@ -71,29 +72,13 @@ export default function RecordNumberSettingsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 10, fontSize: 14, cursor: readOnly ? 'not-allowed' : 'pointer' }}>
|
||||
<span style={{
|
||||
position: 'relative', width: 42, height: 22, borderRadius: 999, flexShrink: 0,
|
||||
background: enabled ? 'var(--primary)' : 'var(--border-2)', transition: 'background .2s',
|
||||
opacity: readOnly ? 0.6 : 1,
|
||||
}}>
|
||||
<input
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
aria-label="شمارهگذاری خودکار پرونده"
|
||||
checked={enabled}
|
||||
disabled={readOnly}
|
||||
onChange={(e) => setEnabled(e.target.checked)}
|
||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', margin: 0, opacity: 0, cursor: 'inherit' }}
|
||||
/>
|
||||
<span style={{
|
||||
position: 'absolute', top: 2, insetInlineStart: enabled ? 22 : 2, width: 18, height: 18,
|
||||
borderRadius: 999, background: 'var(--surface)', transition: 'inset-inline-start .2s',
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,.2)',
|
||||
}} />
|
||||
</span>
|
||||
شمارهگذاری خودکار پرونده
|
||||
</label>
|
||||
<Switch
|
||||
inline
|
||||
checked={enabled}
|
||||
disabled={readOnly}
|
||||
onChange={setEnabled}
|
||||
label="شمارهگذاری خودکار پرونده"
|
||||
/>
|
||||
|
||||
<div style={{ maxWidth: 420, marginTop: 18 }}>
|
||||
<label className="field-label" htmlFor="record-number-pattern">الگوی شماره</label>
|
||||
|
||||
@@ -20,6 +20,7 @@ import ConfirmDialog from '../components/ui/ConfirmDialog';
|
||||
import Modal from '../components/ui/Modal';
|
||||
import SearchableSelect from '../components/ui/SearchableSelect';
|
||||
import { numericField } from '../lib/forms';
|
||||
import Switch from '../components/ui/Switch';
|
||||
|
||||
const schema = z.object({
|
||||
full_name: z.string().min(2, 'نام الزامی است'),
|
||||
@@ -64,7 +65,7 @@ export default function RepresentationsPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const { register, handleSubmit, reset, control, watch, formState: { errors, isSubmitting } } = useForm<FormData>({
|
||||
const { register, handleSubmit, reset, control, watch, setValue, formState: { errors, isSubmitting } } = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: { commission_percent: 10, city_ids: [], is_global: false },
|
||||
});
|
||||
@@ -203,10 +204,12 @@ export default function RepresentationsPage() {
|
||||
{errors.mobile_number && <p className="err-text">{errors.mobile_number.message}</p>}
|
||||
</div>
|
||||
<div className="form-row" style={{ marginTop: 12 }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<input type="checkbox" {...register('is_global')} style={{ width: 16, height: 16 }} />
|
||||
نماینده سراسری (دامنه اختصاصی — فقط پزشکان/کلینیکهای خودش نمایش داده میشوند)
|
||||
</label>
|
||||
<Switch
|
||||
inline
|
||||
checked={watch('is_global') ?? false}
|
||||
onChange={(v) => setValue('is_global', v, { shouldDirty: true })}
|
||||
label="نماینده سراسری (دامنه اختصاصی — فقط پزشکان/کلینیکهای خودش نمایش داده میشوند)"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row" style={{ marginTop: 12 }}>
|
||||
<label>دامنه</label>
|
||||
|
||||
@@ -102,7 +102,7 @@ export default function ResourcePoolsPage() {
|
||||
>
|
||||
{p.active ? 'غیرفعال کردن' : 'فعال کردن'}
|
||||
</button>
|
||||
<button type="button" className="btn secondary sm" onClick={() => setToDelete(p)}>
|
||||
<button type="button" className="btn danger sm" onClick={() => setToDelete(p)}>
|
||||
حذف
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { renderWithProviders } from '../test/utils';
|
||||
|
||||
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
||||
vi.mock('../lib/api', () => ({
|
||||
api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() },
|
||||
ApiError: class extends Error {},
|
||||
}));
|
||||
vi.mock('../hooks/usePermissions', () => ({
|
||||
usePermissions: () => ({ can: () => true }),
|
||||
}));
|
||||
|
||||
import { api } from '../lib/api';
|
||||
import ResourceTypesPage from './ResourceTypesPage';
|
||||
|
||||
const get = api.get as ReturnType<typeof vi.fn>;
|
||||
const post = api.post as ReturnType<typeof vi.fn>;
|
||||
|
||||
const LASER = { uuid: 't1', code: 'laser', name: 'دستگاه لیزر', is_system: false, active: true, resources_count: 2, created_at: 0, updated_at: 0 };
|
||||
const SYSTEM = { uuid: 't2', code: 'doctor', name: 'پزشک', is_system: true, active: true, resources_count: 5, created_at: 0, updated_at: 0 };
|
||||
|
||||
function mockApi() {
|
||||
get.mockResolvedValue({ success: true, data: [LASER, SYSTEM] });
|
||||
post.mockResolvedValue({ success: true, data: LASER });
|
||||
}
|
||||
|
||||
const openCreate = async () => {
|
||||
fireEvent.click(await screen.findByRole('button', { name: /افزودن نوع/ }));
|
||||
return screen.findByText('افزودن نوع منبع');
|
||||
};
|
||||
|
||||
describe('ResourceTypesPage', () => {
|
||||
beforeEach(() => { vi.clearAllMocks(); mockApi(); });
|
||||
|
||||
it('نوعها را با کد و نشان سیستمی فهرست میکند', async () => {
|
||||
renderWithProviders(<ResourceTypesPage />, { route: '/admin/resources/types' });
|
||||
|
||||
expect(await screen.findByText('دستگاه لیزر')).toBeInTheDocument();
|
||||
expect(screen.getByText('laser')).toBeInTheDocument();
|
||||
expect(screen.getByText('سیستمی')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
/** نوع سیستمی پلِ خودکار منابع است؛ حذفش باید بسته باشد، نه صرفاً پشیمانکننده. */
|
||||
it('حذف نوع سیستمی غیرفعال است', async () => {
|
||||
renderWithProviders(<ResourceTypesPage />, { route: '/admin/resources/types' });
|
||||
|
||||
await screen.findByText('پزشک');
|
||||
const deletes = screen.getAllByRole('button', { name: 'حذف' });
|
||||
expect(deletes[1]).toBeDisabled();
|
||||
expect(deletes[0]).toBeEnabled();
|
||||
});
|
||||
|
||||
/**
|
||||
* مودال پیش از این فیلدهایش را دستی میساخت: `field-block` روی خودِ `<input>`
|
||||
* (که رَپر است، نه اینپوت) اینپوت را بیکادر میکرد، و چکباکس نیتیو بود.
|
||||
* فیلدها باید از `Input`/`Field` دیزاینسیستم بیایند و سوییچ از کلاس `.switch`.
|
||||
*/
|
||||
describe('مودال افزودن', () => {
|
||||
it('فیلدها کلاس اینپوت دیزاینسیستم را دارند، نه کلاس رَپر', async () => {
|
||||
const { container } = renderWithProviders(<ResourceTypesPage />, { route: '/admin/resources/types' });
|
||||
await openCreate();
|
||||
|
||||
const code = screen.getByPlaceholderText('laser_device');
|
||||
const name = screen.getByPlaceholderText('دستگاه لیزر');
|
||||
|
||||
expect(code).toHaveClass('cp-input');
|
||||
expect(name).toHaveClass('cp-input');
|
||||
expect(container.querySelector('input.field-block')).toBeNull();
|
||||
});
|
||||
|
||||
it('وضعیت فعال با سوییچ نمایش داده میشود نه چکباکس خام', async () => {
|
||||
renderWithProviders(<ResourceTypesPage />, { route: '/admin/resources/types' });
|
||||
await openCreate();
|
||||
|
||||
const toggle = screen.getByLabelText(/فعال است/) as HTMLInputElement;
|
||||
expect(toggle.type).toBe('checkbox');
|
||||
expect(toggle.closest('.switch')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('هر فیلد لیبل متصل دارد', async () => {
|
||||
renderWithProviders(<ResourceTypesPage />, { route: '/admin/resources/types' });
|
||||
await openCreate();
|
||||
|
||||
expect(screen.getByLabelText('کد (انگلیسی)')).toBe(screen.getByPlaceholderText('laser_device'));
|
||||
expect(screen.getByLabelText('نام نمایشی')).toBe(screen.getByPlaceholderText('دستگاه لیزر'));
|
||||
});
|
||||
|
||||
it('کد نامعتبر پیام خطا میدهد و ذخیره را میبندد', async () => {
|
||||
renderWithProviders(<ResourceTypesPage />, { route: '/admin/resources/types' });
|
||||
await openCreate();
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('laser_device'), { target: { value: 'Laser Device' } });
|
||||
|
||||
expect(await screen.findByText('فقط حروف کوچک انگلیسی، عدد و زیرخط.')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'ذخیره' })).toBeDisabled();
|
||||
});
|
||||
|
||||
/** سه فیلد ساده؛ Enter باید ثبت کند و کاربر را سراغ ماوس نفرستد. */
|
||||
it('Enter فرم را ثبت میکند', async () => {
|
||||
renderWithProviders(<ResourceTypesPage />, { route: '/admin/resources/types' });
|
||||
await openCreate();
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('laser_device'), { target: { value: 'unit_chair' } });
|
||||
fireEvent.change(screen.getByPlaceholderText('دستگاه لیزر'), { target: { value: 'یونیت' } });
|
||||
// مودال در Portal روی document.body است، نه داخل container رندر.
|
||||
fireEvent.submit(document.querySelector('#resource-type-form')!);
|
||||
|
||||
await waitFor(() => expect(post).toHaveBeenCalledWith(
|
||||
'/api/v1/resource-types',
|
||||
{ code: 'unit_chair', name: 'یونیت' },
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,9 @@ import PageHeader from '../components/ui/PageHeader';
|
||||
import DataTable, { type Column } from '../components/ui/DataTable';
|
||||
import Modal from '../components/ui/Modal';
|
||||
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
||||
import Field from '../components/ui/Field';
|
||||
import Input from '../components/ui/Input';
|
||||
import Switch from '../components/ui/Switch';
|
||||
import { ActiveBadge } from '../components/ui/StatusBadge';
|
||||
import { useUrlState } from '../hooks/useUrlState';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
@@ -75,9 +78,10 @@ export default function ResourceTypesPage() {
|
||||
<button type="button" className="btn secondary sm" onClick={() => setEditing({ open: true, type: t })}>
|
||||
ویرایش
|
||||
</button>
|
||||
{/* کنش مخرب باید رنگ هشدار داشته باشد، نه ظاهرِ «ویرایش» */}
|
||||
<button
|
||||
type="button"
|
||||
className="btn secondary sm"
|
||||
className="btn danger sm"
|
||||
disabled={t.is_system}
|
||||
title={t.is_system ? 'نوع سیستمی حذف نمیشود' : undefined}
|
||||
onClick={() => setToDelete(t)}
|
||||
@@ -139,53 +143,81 @@ function TypeModal({
|
||||
const codeValid = /^[a-z0-9_]{1,40}$/.test(code);
|
||||
const invalid = name.trim() === '' || (!isEdit && !codeValid);
|
||||
|
||||
const submit = () => {
|
||||
if (saving || invalid) return;
|
||||
onSave({ code: code.trim(), name: name.trim(), active });
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} title={isEdit ? 'ویرایش نوع منبع' : 'افزودن نوع منبع'}>
|
||||
<div style={{ display: 'grid', gap: 14 }}>
|
||||
<div style={{ display: 'grid', gap: 6 }}>
|
||||
<label style={{ fontSize: 12, color: 'var(--text-2)' }}>کد (انگلیسی)</label>
|
||||
<input
|
||||
className="field"
|
||||
value={code}
|
||||
disabled={isEdit}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
placeholder="laser_device"
|
||||
style={{ direction: 'ltr' }}
|
||||
/>
|
||||
{!isEdit && code !== '' && !codeValid && (
|
||||
<span style={{ fontSize: 12, color: 'var(--danger)' }}>
|
||||
فقط حروف کوچک انگلیسی، عدد و زیرخط.
|
||||
</span>
|
||||
)}
|
||||
{isEdit && (
|
||||
<span style={{ fontSize: 12, color: 'var(--text-3)' }}>
|
||||
کد پس از ساخت تغییر نمیکند؛ منابع موجود با همین کد پیدا میشوند.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: 6 }}>
|
||||
<label style={{ fontSize: 12, color: 'var(--text-2)' }}>نام نمایشی</label>
|
||||
<input className="field-block" value={name} onChange={(e) => setName(e.target.value)} placeholder="دستگاه لیزر" />
|
||||
</div>
|
||||
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13 }}>
|
||||
<input type="checkbox" checked={active} onChange={(e) => setActive(e.target.checked)} />
|
||||
فعال است
|
||||
</label>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title={isEdit ? 'ویرایش نوع منبع' : 'افزودن نوع منبع'}
|
||||
/* دکمهها در `footer` مینشینند نه در بدنه: `.modal-foot` خطِ جداکننده و
|
||||
پدینگ خودش را دارد، و ردیف دستیِ قبلی چسبیده به آخرین فیلد بود. */
|
||||
footer={
|
||||
<div className="row-actions">
|
||||
<button type="button" className="btn secondary" onClick={onClose}>انصراف</button>
|
||||
<button
|
||||
type="button"
|
||||
type="submit"
|
||||
form="resource-type-form"
|
||||
className="btn primary"
|
||||
disabled={saving || invalid}
|
||||
onClick={() => onSave({ code: code.trim(), name: name.trim(), active })}
|
||||
>
|
||||
{saving ? 'در حال ذخیره...' : 'ذخیره'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{/* `<form>` است تا Enter هم ذخیره کند — سه فیلد ساده، رفتن سراغ ماوس اضافه است. */}
|
||||
<form
|
||||
id="resource-type-form"
|
||||
style={{ display: 'grid', gap: 16 }}
|
||||
onSubmit={(e) => { e.preventDefault(); submit(); }}
|
||||
>
|
||||
<Field
|
||||
label="کد (انگلیسی)"
|
||||
htmlFor="resource-type-code"
|
||||
error={!isEdit && code !== '' && !codeValid ? 'فقط حروف کوچک انگلیسی، عدد و زیرخط.' : undefined}
|
||||
>
|
||||
<Input
|
||||
id="resource-type-code"
|
||||
value={code}
|
||||
disabled={isEdit}
|
||||
autoFocus={!isEdit}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
placeholder="laser_device"
|
||||
dir="ltr"
|
||||
hasError={!isEdit && code !== '' && !codeValid}
|
||||
/* فیلد قفل باید قفل به نظر برسد؛ پیش از این با فیلد فعال یکسان بود. */
|
||||
style={isEdit ? { opacity: 0.6, cursor: 'not-allowed' } : undefined}
|
||||
/>
|
||||
{isEdit && (
|
||||
<span className="field-hint">
|
||||
کد پس از ساخت تغییر نمیکند؛ منابع موجود با همین کد پیدا میشوند.
|
||||
</span>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field label="نام نمایشی" htmlFor="resource-type-name">
|
||||
<Input
|
||||
id="resource-type-name"
|
||||
value={name}
|
||||
autoFocus={isEdit}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="دستگاه لیزر"
|
||||
/>
|
||||
<span className="field-hint">همین نام در فهرست منابع و انتخابگرها دیده میشود.</span>
|
||||
</Field>
|
||||
|
||||
<Switch
|
||||
id="resource-type-active"
|
||||
checked={active}
|
||||
onChange={setActive}
|
||||
label="فعال است"
|
||||
hint="نوع غیرفعال در ساخت منبع تازه پیشنهاد نمیشود."
|
||||
/>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ActiveBadge } from '../components/ui/StatusBadge';
|
||||
import Pagination from '../components/ui/Pagination';
|
||||
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
||||
import Modal from '../components/ui/Modal';
|
||||
import Switch from '../components/ui/Switch';
|
||||
|
||||
const DEFAULT_PERMISSIONS: SecretaryPermissions = {
|
||||
appointments: { view: true, create: false, cancel: false, update_status: false },
|
||||
@@ -208,13 +209,14 @@ function PermissionsMatrix({
|
||||
return <td key={action} style={{ textAlign: 'center', color: 'var(--border)' }}>—</td>;
|
||||
}
|
||||
return (
|
||||
<td key={action} style={{ textAlign: 'center' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={sectionPerms[action] ?? false}
|
||||
onChange={() => toggle(section, action)}
|
||||
style={{ width: 16, height: 16, accentColor: 'var(--primary)', cursor: 'pointer' }}
|
||||
/>
|
||||
<td key={action}>
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Switch
|
||||
checked={sectionPerms[action] ?? false}
|
||||
onChange={() => toggle(section, action)}
|
||||
ariaLabel={`${section} — ${action}`}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { ApiResponse } from '../lib/api';
|
||||
import type { Secretary } from '../types';
|
||||
import { digitsOnly, formatDate, formatNumber, formatRial } from '../lib/utils';
|
||||
import PageHeader from '../components/ui/PageHeader';
|
||||
import Switch from '../components/ui/Switch';
|
||||
|
||||
/** یک ردیف label:value با همان تم کارتهای موجود. */
|
||||
function Row({ label, value }: { label: string; value: React.ReactNode }) {
|
||||
@@ -95,15 +96,11 @@ export default function SecretaryDetailPage() {
|
||||
</p>
|
||||
|
||||
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 10, cursor: 'pointer', marginBottom: 14 }}>
|
||||
<span className="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label="محاسبه درآمد منشی از نوبتهای آنلاین"
|
||||
checked={enabled}
|
||||
onChange={(e) => setEnabled(e.target.checked)}
|
||||
/>
|
||||
<span className="switch-track"><span className="switch-thumb" /></span>
|
||||
</span>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={setEnabled}
|
||||
ariaLabel="محاسبه درآمد منشی از نوبتهای آنلاین"
|
||||
/>
|
||||
<span style={{ fontSize: 13 }}>محاسبه درآمد از نوبتهای آنلاین فعال باشد</span>
|
||||
</label>
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
WrenchScrewdriverIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
||||
import Switch from '../components/ui/Switch';
|
||||
|
||||
interface TaxHistoryRow {
|
||||
tax_percent: number;
|
||||
@@ -131,10 +132,9 @@ function SectionHead({ s }: { s: SectionDef }) {
|
||||
|
||||
function Toggle({ checked, onChange, label }: { checked: boolean; onChange: () => void; label: string }) {
|
||||
return (
|
||||
<label className="switch" title={label}>
|
||||
<input type="checkbox" checked={checked} onChange={onChange} aria-label={label} />
|
||||
<span className="switch-track"><span className="switch-thumb" /></span>
|
||||
</label>
|
||||
<span title={label}>
|
||||
<Switch checked={checked} onChange={onChange} ariaLabel={label} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ import PageHeader from '../components/ui/PageHeader';
|
||||
import DataTable, { type Column } from '../components/ui/DataTable';
|
||||
import Modal from '../components/ui/Modal';
|
||||
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
||||
import Field from '../components/ui/Field';
|
||||
import Input from '../components/ui/Input';
|
||||
import Switch from '../components/ui/Switch';
|
||||
import { ActiveBadge } from '../components/ui/StatusBadge';
|
||||
import { useUrlState } from '../hooks/useUrlState';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
@@ -74,7 +77,7 @@ export default function SkillsPage() {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn secondary sm"
|
||||
className="btn danger sm"
|
||||
disabled={(s.resources_count ?? 0) > 0}
|
||||
title={(s.resources_count ?? 0) > 0 ? 'اول از منابع برداشته شود' : undefined}
|
||||
onClick={() => setToDelete(s)}
|
||||
@@ -131,30 +134,47 @@ function SkillModal({
|
||||
}, [open, skill]);
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} title={skill ? 'ویرایش مهارت' : 'افزودن مهارت'}>
|
||||
<div style={{ display: 'grid', gap: 14 }}>
|
||||
<div style={{ display: 'grid', gap: 6 }}>
|
||||
<label style={{ fontSize: 12, color: 'var(--text-2)' }}>نام مهارت</label>
|
||||
<input className="field" value={name} onChange={(e) => setName(e.target.value)} placeholder="لیزر آلکساندرایت" />
|
||||
</div>
|
||||
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13 }}>
|
||||
<input type="checkbox" checked={active} onChange={(e) => setActive(e.target.checked)} />
|
||||
فعال است
|
||||
</label>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title={skill ? 'ویرایش مهارت' : 'افزودن مهارت'}
|
||||
footer={
|
||||
<div className="row-actions">
|
||||
<button type="button" className="btn secondary" onClick={onClose}>انصراف</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn primary"
|
||||
disabled={saving || name.trim() === ''}
|
||||
onClick={() => onSave({ name: name.trim(), active })}
|
||||
>
|
||||
<button type="submit" form="skill-form" className="btn primary" disabled={saving || name.trim() === ''}>
|
||||
{saving ? 'در حال ذخیره...' : 'ذخیره'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<form
|
||||
id="skill-form"
|
||||
style={{ display: 'grid', gap: 16 }}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (saving || name.trim() === '') return;
|
||||
onSave({ name: name.trim(), active });
|
||||
}}
|
||||
>
|
||||
<Field label="نام مهارت" htmlFor="skill-name">
|
||||
<Input
|
||||
id="skill-name"
|
||||
value={name}
|
||||
autoFocus
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="لیزر آلکساندرایت"
|
||||
/>
|
||||
<span className="field-hint">مهارت به منبع داده میشود و در جستجوی وقت شرط میگذارد.</span>
|
||||
</Field>
|
||||
|
||||
<Switch
|
||||
id="skill-active"
|
||||
checked={active}
|
||||
onChange={setActive}
|
||||
label="فعال است"
|
||||
hint="مهارت غیرفعال در انتخابگرها پیشنهاد نمیشود."
|
||||
/>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import PageHeader from '../components/ui/PageHeader';
|
||||
import FeatureGate from '../components/ui/FeatureGate';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { numericField } from '../lib/forms';
|
||||
import Switch from '../components/ui/Switch';
|
||||
|
||||
const chargeSchema = z.object({
|
||||
amount_rials: z.coerce.number().min(1000, 'حداقل مبلغ ۱٬۰۰۰ تومان است'),
|
||||
@@ -262,14 +263,13 @@ function SmsWalletPageInner() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<label className="switch" style={{ marginTop: 4, flexShrink: 0 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
<span style={{ marginTop: 4, flexShrink: 0 }}>
|
||||
<Switch
|
||||
checked={currentSettings.reminder_enabled}
|
||||
onChange={() => setLocalSettings({ ...currentSettings, reminder_enabled: !currentSettings.reminder_enabled })}
|
||||
onChange={(v) => setLocalSettings({ ...currentSettings, reminder_enabled: v })}
|
||||
ariaLabel="یادآوری نوبت"
|
||||
/>
|
||||
<span className="switch-track"><span className="switch-thumb" /></span>
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* ردیف: پیامک بعد از ویزیت */}
|
||||
@@ -285,14 +285,13 @@ function SmsWalletPageInner() {
|
||||
<div style={{ color: 'var(--text-3)', fontSize: 12.5, marginTop: 3 }}>متن تشکر پس از پایان مراجعه برای بیمار ارسال میشود</div>
|
||||
</div>
|
||||
</div>
|
||||
<label className="switch" style={{ flexShrink: 0 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
<span style={{ flexShrink: 0 }}>
|
||||
<Switch
|
||||
checked={currentSettings.post_visit_enabled}
|
||||
onChange={() => setLocalSettings({ ...currentSettings, post_visit_enabled: !currentSettings.post_visit_enabled })}
|
||||
onChange={(v) => setLocalSettings({ ...currentSettings, post_visit_enabled: v })}
|
||||
ariaLabel="پیام پس از مراجعه"
|
||||
/>
|
||||
<span className="switch-track"><span className="switch-thumb" /></span>
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* محتوای بازشونده */}
|
||||
|
||||
@@ -51,7 +51,8 @@ describe('TagsSettingsPage', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /برچسب جدید/ }));
|
||||
fireEvent.change(screen.getByPlaceholderText('نام برچسب را وارد کنید'), { target: { value: 'بدهکار' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'رنگ #0088ff' }));
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: 'وضعیت برچسب' })); // toggle off (default active)
|
||||
// `role="switch"` است نه checkbox — ورودی boolean در پنل همیشه سوییچ است.
|
||||
fireEvent.click(screen.getByRole('switch', { name: 'وضعیت برچسب' })); // toggle off (default active)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'ثبت برچسب' }));
|
||||
|
||||
await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/tenant-tag', expect.objectContaining({
|
||||
|
||||
@@ -11,6 +11,7 @@ import Modal from '../components/ui/Modal';
|
||||
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
||||
import SettingsLayout from '../components/layout/SettingsLayout';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import Switch from '../components/ui/Switch';
|
||||
|
||||
interface TenantTag { uuid: string; name: string; color: string; active: boolean }
|
||||
|
||||
@@ -142,10 +143,11 @@ export default function TagsSettingsPage() {
|
||||
<div>
|
||||
<label className="field-label" style={{ fontWeight: 700 }}>وضعیت برچسب</label>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<label className="switch" title="وضعیت برچسب">
|
||||
<input type="checkbox" checked={form.watch('active')} onChange={(e) => form.setValue('active', e.target.checked)} aria-label="وضعیت برچسب" />
|
||||
<span className="switch-track"><span className="switch-thumb" /></span>
|
||||
</label>
|
||||
<Switch
|
||||
checked={form.watch('active')}
|
||||
onChange={(v) => form.setValue('active', v, { shouldDirty: true })}
|
||||
ariaLabel="وضعیت برچسب"
|
||||
/>
|
||||
<span style={{ fontSize: 14, color: 'var(--text-2)' }}>{form.watch('active') ? 'فعال' : 'غیرفعال'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user