The visual pass over the older screens found one inconsistency worth fixing: the service catalog opened with a hand-rolled bold line and a button. No back button, no description — the one page in the panel that does not say what it is or how to leave it. It uses PageHeader now, like every other page. Everything else on the task 01-04 screens held up under dark mode and compact density: branches, resource types, skills, pools and the catalog all read from tokens and none of them broke. The only remaining mobile findings are the shared shell's Latin phone number and one small tap target, both of which predate this work and appear on the dashboard too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
400 lines
23 KiB
TypeScript
400 lines
23 KiB
TypeScript
import React, { useState } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||
import {
|
||
PlusIcon, PencilIcon, WrenchScrewdriverIcon, BanknotesIcon,
|
||
ShieldCheckIcon, MagnifyingGlassIcon, EyeIcon, EyeSlashIcon,
|
||
EllipsisHorizontalIcon, CheckCircleIcon, XCircleIcon, ChevronRightIcon,
|
||
XMarkIcon, UsersIcon, ClockIcon,
|
||
} from '@heroicons/react/24/outline';
|
||
import { useForm } from 'react-hook-form';
|
||
import { zodResolver } from '@hookform/resolvers/zod';
|
||
import { z } from 'zod';
|
||
import { toast } from 'sonner';
|
||
import { api } from '../lib/api';
|
||
import type { ApiResponse } from '../lib/api';
|
||
import type { ServiceSection, ServiceItem } from '../types';
|
||
import { formatRial, formatNumber } from '../lib/utils';
|
||
import { usePermissions } from '../hooks/usePermissions';
|
||
import PageHeader from '../components/ui/PageHeader';
|
||
import Modal from '../components/ui/Modal';
|
||
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
||
import ServiceTariffModal from '../components/ServiceTariffModal';
|
||
import ServiceInsuranceModal from '../components/ServiceInsuranceModal';
|
||
import ServiceItemFormModal from '../components/ServiceItemFormModal';
|
||
import FeatureGate from '../components/ui/FeatureGate';
|
||
|
||
const sectionSchema = z.object({ name: z.string().min(1, 'نام بخش الزامی است') });
|
||
type SectionForm = z.infer<typeof sectionSchema>;
|
||
|
||
const EMPTY_SECTIONS: ServiceSection[] = [];
|
||
const EMPTY_ITEMS: ServiceItem[] = [];
|
||
|
||
function Avatar({ name }: { name: string }) {
|
||
return (
|
||
<div style={{
|
||
width: 26, height: 26, borderRadius: '50%',
|
||
background: 'linear-gradient(145deg, oklch(0.62 0.15 162), oklch(0.48 0.16 162))',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
fontSize: 11, fontWeight: 700, color: 'var(--on-primary)', flexShrink: 0,
|
||
}}>
|
||
{name.charAt(0)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function ClinicServicesPageInner() {
|
||
const qc = useQueryClient();
|
||
// مجوزهای منشی؛ برای owner/پزشک همیشه true (usePermissions بدون context آزاد است).
|
||
const { can } = usePermissions();
|
||
const canCreate = can('services', 'create');
|
||
const canUpdate = can('services', 'update');
|
||
const navigate = useNavigate();
|
||
|
||
const [selectedSection, setSelectedSection] = useState<ServiceSection | null>(null);
|
||
const [sectionModal, setSectionModal] = useState<'create' | ServiceSection | null>(null);
|
||
const [itemModal, setItemModal] = useState<'create' | ServiceItem | null>(null);
|
||
const [toggleItem, setToggleItem] = useState<ServiceItem | null>(null);
|
||
const [tariffItem, setTariffItem] = useState<ServiceItem | null>(null);
|
||
const [insuranceItem, setInsuranceItem] = useState<ServiceItem | null>(null);
|
||
const [search, setSearch] = useState('');
|
||
const [showInactive, setShowInactive] = useState(true);
|
||
const [menuOpen, setMenuOpen] = useState<string | null>(null);
|
||
|
||
const { data: sectionsData, isLoading: sectionsLoading } = useQuery<ApiResponse<ServiceSection[]>>({
|
||
queryKey: ['service-sections'],
|
||
queryFn: () => api.get('/api/v1/service-sections'),
|
||
});
|
||
|
||
const { data: itemsData, isLoading: itemsLoading } = useQuery<ApiResponse<ServiceItem[]>>({
|
||
queryKey: ['service-items', selectedSection?.uuid],
|
||
queryFn: () => api.get(`/api/v1/service-items/${selectedSection!.uuid}`),
|
||
enabled: !!selectedSection,
|
||
});
|
||
|
||
const sections = sectionsData?.data ?? EMPTY_SECTIONS;
|
||
const allItems = itemsData?.data ?? EMPTY_ITEMS;
|
||
|
||
const sectionForm = useForm<SectionForm>({ resolver: zodResolver(sectionSchema) });
|
||
|
||
const items = allItems.filter((it) => {
|
||
if (!showInactive && !it.active) return false;
|
||
if (search.trim() && !it.name.includes(search.trim())) return false;
|
||
return true;
|
||
});
|
||
const activeCount = allItems.filter((i) => i.active).length;
|
||
|
||
const createSection = useMutation({
|
||
mutationFn: (body: SectionForm) => api.post('/api/v1/service-section', body),
|
||
onSuccess: () => { qc.invalidateQueries({ queryKey: ['service-sections'] }); setSectionModal(null); sectionForm.reset(); toast.success('بخش ایجاد شد'); },
|
||
onError: (e: any) => toast.error(e.message),
|
||
});
|
||
|
||
const editSection = useMutation({
|
||
mutationFn: ({ uuid, body }: { uuid: string; body: SectionForm }) =>
|
||
api.patch(`/api/v1/service-section/${uuid}`, body),
|
||
onSuccess: () => { qc.invalidateQueries({ queryKey: ['service-sections'] }); setSectionModal(null); toast.success('بخش ویرایش شد'); },
|
||
onError: (e: any) => toast.error(e.message),
|
||
});
|
||
|
||
const toggleSection = useMutation({
|
||
mutationFn: ({ uuid, active }: { uuid: string; active: boolean }) =>
|
||
api.patch(`/api/v1/service-section/${uuid}`, { active }),
|
||
onSuccess: (_d, v) => {
|
||
qc.invalidateQueries({ queryKey: ['service-sections'] });
|
||
toast.success(v.active ? 'بخش فعال شد' : 'بخش غیرفعال شد');
|
||
},
|
||
onError: (e: any) => toast.error(e.message),
|
||
});
|
||
|
||
const toggleActive = useMutation({
|
||
mutationFn: ({ uuid, active }: { uuid: string; active: boolean }) =>
|
||
api.patch(`/api/v1/service-item/${uuid}`, { active }),
|
||
onSuccess: (_d, v) => {
|
||
qc.invalidateQueries({ queryKey: ['service-items', selectedSection?.uuid] });
|
||
setToggleItem(null);
|
||
toast.success(v.active ? 'سرویس فعال شد' : 'سرویس غیرفعال شد');
|
||
},
|
||
onError: (e: any) => { toast.error(e.message); setToggleItem(null); },
|
||
});
|
||
|
||
const openEditSection = (s: ServiceSection) => {
|
||
sectionForm.reset({ name: s.name });
|
||
setSectionModal(s);
|
||
};
|
||
|
||
const openCreateItem = () => setItemModal('create');
|
||
|
||
return (
|
||
<>
|
||
{!selectedSection ? (
|
||
/* ═════════ نمای بخشها ═════════ */
|
||
<>
|
||
{/* تیتر دستساز جای `PageHeader` بود: بدون آن این صفحه تنها فهرستِ پنل است
|
||
که نه دکمهٔ بازگشت دارد و نه میگوید چیست. */}
|
||
<PageHeader
|
||
title="سرویسها"
|
||
description="بخشها و سرویسهای قابل رزرو. مدت و قیمت هر سرویس از همینجا میآید."
|
||
backTo="/admin/settings-menu"
|
||
action={
|
||
canCreate ? (
|
||
<button className="cp-btn-primary" onClick={() => { sectionForm.reset({ name: '' }); setSectionModal('create'); }}>
|
||
<PlusIcon style={{ width: 16 }} /> بخش جدید
|
||
</button>
|
||
) : undefined
|
||
}
|
||
/>
|
||
|
||
{sectionsLoading ? (
|
||
<div style={{ color: 'var(--text-3)', fontSize: 13, padding: 16 }}>در حال بارگذاری...</div>
|
||
) : sections.length === 0 ? (
|
||
<div className="card" style={{ padding: '60px 0', textAlign: 'center', color: 'var(--text-3)' }}>
|
||
<WrenchScrewdriverIcon style={{ width: 56, margin: '0 auto 16px', display: 'block', opacity: 0.3 }} />
|
||
<div style={{ fontSize: 14, color: 'var(--text-2)' }}>برای اضافه کردن بخش روی دکمه بخش جدید کلیک نمایید.</div>
|
||
</div>
|
||
) : (
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: 14 }}>
|
||
{sections.map((s) => (
|
||
<div
|
||
key={s.uuid}
|
||
onClick={() => setSelectedSection(s)}
|
||
style={{
|
||
cursor: 'pointer', background: 'var(--surface)', borderRadius: 'var(--r)',
|
||
boxShadow: '0 1px 24.8px rgba(204,204,204,0.18)',
|
||
border: '1px solid var(--border)', padding: '16px 16px 12px',
|
||
opacity: s.active ? 1 : 0.62,
|
||
}}
|
||
>
|
||
<div style={{ fontWeight: 700, fontSize: 15, color: 'var(--text)', marginBottom: 12 }}>{s.name}</div>
|
||
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '4px 0' }}>
|
||
<span style={{ fontSize: 12.5, color: 'var(--text-3)' }}>تعداد:</span>
|
||
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>{formatNumber(s.items_count ?? 0)} سرویس</span>
|
||
</div>
|
||
<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>
|
||
<span style={{ fontSize: 12.5, color: s.active ? 'var(--success)' : 'var(--text-3)' }}>{s.active ? 'فعال' : 'غیرفعال'}</span>
|
||
</span>
|
||
</div>
|
||
|
||
{canUpdate && (
|
||
<div style={{ display: 'flex', gap: 6, borderTop: '1px solid var(--border)', paddingTop: 10, marginTop: 8 }} onClick={(e) => e.stopPropagation()}>
|
||
<button className="btn sm ghost" aria-label="ویرایش" style={{ display: 'flex', alignItems: 'center', gap: 4, color: 'var(--text-2)' }} onClick={() => openEditSection(s)}>
|
||
<PencilIcon style={{ width: 15 }} />
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</>
|
||
) : (
|
||
/* ═════════ نمای سرویسهای یک بخش ═════════ */
|
||
<>
|
||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16, flexWrap: 'wrap', gap: 12 }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
|
||
<button className="cp-btn-secondary" style={{ height: 36 }} onClick={() => setSelectedSection(null)}>
|
||
<ChevronRightIcon style={{ width: 16 }} /> بازگشت
|
||
</button>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 14, color: 'var(--text-3)' }}>
|
||
<span style={{ cursor: 'pointer' }} onClick={() => setSelectedSection(null)}>بخشها</span>
|
||
<span>‹</span>
|
||
<b style={{ color: 'var(--text)' }}>{selectedSection.name}</b>
|
||
</div>
|
||
</div>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||
<div style={{ position: 'relative' }}>
|
||
<MagnifyingGlassIcon style={{ width: 15, position: 'absolute', insetInlineStart: 9, top: '50%', transform: 'translateY(-50%)', color: 'var(--text-3)', pointerEvents: 'none' }} />
|
||
<input className="cp-input" placeholder="جستجوی سرویس" value={search} onChange={(e) => setSearch(e.target.value)} style={{ width: 170, paddingInlineStart: 30, height: 36 }} />
|
||
</div>
|
||
<button className="btn sm ghost" onClick={() => setShowInactive((v) => !v)} title={showInactive ? 'پنهانکردن غیرفعالها' : 'نمایش غیرفعالها'}>
|
||
{showInactive ? <EyeIcon style={{ width: 16 }} /> : <EyeSlashIcon style={{ width: 16 }} />}
|
||
</button>
|
||
{canCreate && (
|
||
<button className="cp-btn-primary" onClick={openCreateItem}>
|
||
<PlusIcon style={{ width: 16 }} /> سرویس جدید
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{itemsLoading ? (
|
||
<div style={{ color: 'var(--text-3)', fontSize: 13, padding: 16 }}>در حال بارگذاری...</div>
|
||
) : items.length === 0 ? (
|
||
<div className="card" style={{ padding: '52px 0', textAlign: 'center', color: 'var(--text-3)' }}>
|
||
<PlusIcon style={{ width: 32, margin: '0 auto 12px', display: 'block', opacity: 0.35 }} />
|
||
<div style={{ fontWeight: 600, color: 'var(--text-2)', marginBottom: 4 }}>
|
||
{allItems.length === 0 ? 'سرویسی در این بخش وجود ندارد' : 'سرویسی با این فیلتر یافت نشد'}
|
||
</div>
|
||
{allItems.length === 0 && canCreate && (
|
||
<button className="cp-btn-primary" style={{ marginTop: 12 }} onClick={openCreateItem}>افزودن سرویس</button>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: 16 }}>
|
||
{items.map((item) => {
|
||
const menuItemStyle: React.CSSProperties = {
|
||
display: 'flex', alignItems: 'center', gap: 8, width: '100%',
|
||
padding: '8px 10px', borderRadius: 8, border: 'none', cursor: 'pointer',
|
||
background: 'transparent', color: 'var(--text-2)', fontSize: 13,
|
||
fontFamily: 'inherit', textAlign: 'start', whiteSpace: 'nowrap',
|
||
};
|
||
const chipStyle: React.CSSProperties = {
|
||
fontSize: 12, color: 'var(--text-2)', background: 'var(--surface-2)',
|
||
border: '1px solid var(--border)', borderRadius: 999, padding: '3px 10px', whiteSpace: 'nowrap',
|
||
};
|
||
return (
|
||
<div
|
||
key={item.uuid}
|
||
role="link"
|
||
tabIndex={0}
|
||
onClick={() => navigate(`/admin/clinic-services/${item.uuid}`)}
|
||
onKeyDown={(e) => { if (e.key === 'Enter') navigate(`/admin/clinic-services/${item.uuid}`); }}
|
||
style={{
|
||
position: 'relative', background: 'var(--surface)', borderRadius: 8,
|
||
boxShadow: '0 1px 24.8px rgba(204,204,204,0.18)',
|
||
border: '1px solid var(--border)', padding: 14, cursor: 'pointer',
|
||
opacity: item.active ? 1 : 0.7,
|
||
}}
|
||
>
|
||
{/* نوار بالا: ⋮ (چپ) + وضعیت و نام (راست) */}
|
||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 8, marginBottom: 14 }}>
|
||
{canUpdate ? (
|
||
<button className="btn sm ghost" style={{ padding: 4 }} onClick={(e) => { e.stopPropagation(); setMenuOpen(menuOpen === item.uuid ? null : item.uuid); }} title="عملیات">
|
||
<EllipsisHorizontalIcon style={{ width: 20 }} />
|
||
</button>
|
||
) : <span />}
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
|
||
<span className={`badge ${item.active ? 'green' : 'gray'}`} style={{ fontSize: 11 }}>
|
||
<span className="bdot" />{item.active ? 'فعال' : 'غیرفعال'}
|
||
</span>
|
||
<span style={{ fontWeight: 700, fontSize: 14, color: 'var(--text)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{item.name}</span>
|
||
</div>
|
||
</div>
|
||
{menuOpen === item.uuid && (
|
||
<>
|
||
<div style={{ position: 'fixed', inset: 0, zIndex: 40 }} onClick={(e) => { e.stopPropagation(); setMenuOpen(null); }} />
|
||
<div onClick={(e) => e.stopPropagation()} style={{ position: 'absolute', top: 40, insetInlineStart: 8, zIndex: 41, background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--r-sm)', boxShadow: 'var(--shadow)', padding: 6, minWidth: 176, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||
<button style={menuItemStyle} onClick={() => { setMenuOpen(null); setItemModal(item); }}><PencilIcon style={{ width: 15 }} /> ویرایش</button>
|
||
<button style={menuItemStyle} onClick={() => { setMenuOpen(null); setTariffItem(item); }}><BanknotesIcon style={{ width: 15 }} /> تعرفههای سالانه</button>
|
||
<button style={menuItemStyle} onClick={() => { setMenuOpen(null); setInsuranceItem(item); }}><ShieldCheckIcon style={{ width: 15 }} /> پوشش بیمه</button>
|
||
<button style={menuItemStyle} onClick={() => { setMenuOpen(null); setToggleItem(item); }}>{item.active ? <EyeSlashIcon style={{ width: 15 }} /> : <EyeIcon style={{ width: 15 }} />}{item.active ? ' غیرفعالکردن' : ' فعالکردن'}</button>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8, padding: '3px 0' }}>
|
||
<span style={{ fontSize: 12, color: 'var(--text-3)', display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
||
<BanknotesIcon style={{ width: 14, color: 'var(--text-3)' }} /> قیمت پایه:
|
||
</span>
|
||
<span style={{ fontSize: 14, fontWeight: 800, color: 'var(--primary)' }}>{formatRial(item.price_rials)}</span>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8, padding: '3px 0' }}>
|
||
<span style={{ fontSize: 12, color: 'var(--text-3)', display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
||
<ClockIcon style={{ width: 14, color: 'var(--text-3)' }} /> زمان متوسط:
|
||
</span>
|
||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
|
||
{item.bookable && <span className="badge green" style={{ fontSize: 11 }}>در نوبتدهی</span>}
|
||
{item.duration_minutes
|
||
? <span className="badge blue" style={{ fontSize: 11 }}>{formatNumber(Number(item.duration_minutes))} دقیقه</span>
|
||
: <span style={{ fontSize: 12, color: 'var(--text-3)' }}>—</span>}
|
||
</span>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8, padding: '3px 0' }}>
|
||
<span style={{ fontSize: 12, color: 'var(--text-3)', display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
||
<ShieldCheckIcon style={{ width: 14, color: 'var(--text-3)' }} /> بیمه:
|
||
</span>
|
||
{item.insurance_covered
|
||
? <span className="badge green" style={{ fontSize: 11 }}>تحت پوشش</span>
|
||
: <span style={{ fontSize: 12, color: 'var(--text-3)' }}>—</span>}
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 8, padding: '3px 0' }}>
|
||
<span style={{ fontSize: 12, color: 'var(--text-3)', flexShrink: 0, display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
||
<UsersIcon style={{ width: 14, color: 'var(--text-3)' }} /> پرسنل:
|
||
</span>
|
||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
|
||
{(item.staff_members && item.staff_members.length > 0)
|
||
? item.staff_members.map((m) => <span key={m.uuid} style={chipStyle}>{m.full_name}</span>)
|
||
: item.staff
|
||
? <span style={chipStyle}>{item.staff.full_name}</span>
|
||
: <span style={{ fontSize: 12, color: 'var(--text-3)' }}>—</span>}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
{/* ───────── Modal بخش ───────── */}
|
||
<Modal
|
||
open={sectionModal !== null}
|
||
onClose={() => setSectionModal(null)}
|
||
title={sectionModal === 'create' ? 'بخش جدید' : 'ویرایش بخش'}
|
||
>
|
||
<form onSubmit={sectionForm.handleSubmit((d) => {
|
||
if (sectionModal === 'create') createSection.mutate(d);
|
||
else if (sectionModal !== null && typeof sectionModal === 'object') editSection.mutate({ uuid: sectionModal.uuid, body: d });
|
||
})}>
|
||
<div className="field">
|
||
<label>نام بخش *</label>
|
||
<input {...sectionForm.register('name')} placeholder="مثلاً: تزریقات" autoFocus />
|
||
{sectionForm.formState.errors.name && (
|
||
<span className="field-error">{sectionForm.formState.errors.name.message}</span>
|
||
)}
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 8, marginTop: 18 }}>
|
||
<button type="submit" className="btn primary" disabled={createSection.isPending || editSection.isPending}>ذخیره</button>
|
||
<button type="button" className="btn" onClick={() => setSectionModal(null)}>انصراف</button>
|
||
</div>
|
||
</form>
|
||
</Modal>
|
||
|
||
{/* ───────── Modal سرویس ───────── */}
|
||
<ServiceItemFormModal
|
||
item={itemModal}
|
||
sectionUuid={selectedSection?.uuid ?? null}
|
||
onClose={() => setItemModal(null)}
|
||
onManageInsurance={setInsuranceItem}
|
||
/>
|
||
<ServiceTariffModal item={tariffItem} onClose={() => setTariffItem(null)} />
|
||
|
||
<ServiceInsuranceModal item={insuranceItem} onClose={() => setInsuranceItem(null)} />
|
||
|
||
{/* Confirm فعال/غیرفعال سرویس */}
|
||
<ConfirmDialog
|
||
open={!!toggleItem}
|
||
title={toggleItem?.active ? 'غیرفعالکردن سرویس' : 'فعالکردن سرویس'}
|
||
message={
|
||
toggleItem?.active
|
||
? `سرویس «${toggleItem?.name}» غیرفعال میشود و در پذیرش جدید نمایش داده نمیشود. سوابق قبلی حفظ میمانند.`
|
||
: `سرویس «${toggleItem?.name}» دوباره فعال و قابل انتخاب میشود.`
|
||
}
|
||
confirmLabel={toggleItem?.active ? 'غیرفعال کن' : 'فعال کن'}
|
||
onConfirm={() => toggleItem && toggleActive.mutate({ uuid: toggleItem.uuid, active: !toggleItem.active })}
|
||
onCancel={() => setToggleItem(null)}
|
||
loading={toggleActive.isPending}
|
||
/>
|
||
</>
|
||
);
|
||
}
|
||
|
||
export default function ClinicServicesPage() {
|
||
return (
|
||
<FeatureGate feature="services">
|
||
<ClinicServicesPageInner />
|
||
</FeatureGate>
|
||
);
|
||
}
|