Files
clinicpro/assets/admin/pages/SkillsPage.tsx
T
hamed ecdefa3c24 feat(tour): add onboarding tours for various admin pages
- 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.
2026-08-10 10:10:20 +03:30

182 lines
6.9 KiB
TypeScript

import React, { useMemo, useState } from 'react';
import { PlusIcon } from '@heroicons/react/24/outline';
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';
import { useSkills } from '../hooks/useResources';
import type { Skill } from '../types';
import ResourcesSubNav from '../components/resources/ResourcesSubNav';
/**
* مهارت‌ها — «کدام اپراتور مجاز است با کدام دستگاه کار کند» یک اطلاعات است، نه یک
* قانون. با «عنوان شغلی» پرسنل قاطی نشود: آن متن آزاد و فقط برای نمایش است.
*/
export default function SkillsPage() {
const { skills, loading, create, update, remove } = useSkills();
const { can } = usePermissions();
const canUpdate = can('resources', 'update');
const [urlState, setUrlState] = useUrlState({ search: '' });
const [editing, setEditing] = useState<{ open: boolean; skill: Skill | null }>({ open: false, skill: null });
const [toDelete, setToDelete] = useState<Skill | null>(null);
const rows = useMemo(() => {
const q = urlState.search.trim();
return q === '' ? skills : skills.filter((s) => s.name.includes(q));
}, [skills, urlState.search]);
const columns: Column<Skill>[] = [
{ key: 'name', header: 'مهارت', render: (s) => <span style={{ fontWeight: 600 }}>{s.name}</span> },
{
key: 'resources_count',
header: 'روی چند منبع',
render: (s) => <span style={{ fontSize: 13 }}>{s.resources_count ?? 0}</span>,
},
{ key: 'active', header: 'وضعیت', render: (s) => <ActiveBadge active={s.active} /> },
];
return (
<div className="fade-in">
<PageHeader
title="مهارت‌ها"
tourId="resource-skills"
description="مهارت روی منابع می‌نشیند و در انتخاب منبع مناسب استفاده می‌شود. مهارتی که به منبعی داده شده، تا برداشته نشود حذف نمی‌شود."
backTo="/admin/resources"
breadcrumbs={[{ label: 'منابع', to: '/admin/resources' }, { label: 'مهارت‌ها' }]}
action={
canUpdate ? (
<button type="button" className="btn primary" onClick={() => setEditing({ open: true, skill: null })}>
<PlusIcon style={{ width: 16 }} /> افزودن مهارت
</button>
) : undefined
}
/>
<ResourcesSubNav />
<DataTable
columns={columns}
data={rows}
loading={loading}
searchValue={urlState.search}
onSearchChange={(v) => setUrlState({ search: v })}
searchPlaceholder="جستجو در مهارت‌ها..."
emptyMessage="هنوز مهارتی تعریف نشده است"
actions={
canUpdate
? (s) => (
<div style={{ display: 'flex', gap: 6 }}>
<button type="button" className="btn secondary sm" onClick={() => setEditing({ open: true, skill: s })}>
ویرایش
</button>
<button
type="button"
className="btn danger sm"
disabled={(s.resources_count ?? 0) > 0}
title={(s.resources_count ?? 0) > 0 ? 'اول از منابع برداشته شود' : undefined}
onClick={() => setToDelete(s)}
>
حذف
</button>
</div>
)
: undefined
}
/>
<SkillModal
open={editing.open}
skill={editing.skill}
saving={create.isPending || update.isPending}
onClose={() => setEditing({ open: false, skill: null })}
onSave={({ name, active }) => {
const opts = { onSuccess: () => setEditing({ open: false, skill: null }) };
if (editing.skill) update.mutate({ uuid: editing.skill.uuid, d: { name, active } }, opts);
else create.mutate({ name }, opts);
}}
/>
<ConfirmDialog
open={!!toDelete}
title="حذف مهارت"
message={`آیا از حذف «${toDelete?.name}» مطمئن هستید؟`}
confirmLabel="حذف"
loading={remove.isPending}
onConfirm={() => toDelete && remove.mutate(toDelete.uuid, { onSuccess: () => setToDelete(null) })}
onCancel={() => setToDelete(null)}
/>
</div>
);
}
function SkillModal({
open, skill, saving, onClose, onSave,
}: {
open: boolean;
skill: Skill | null;
saving: boolean;
onClose: () => void;
onSave: (payload: { name: string; active: boolean }) => void;
}) {
const [name, setName] = useState('');
const [active, setActive] = useState(true);
React.useEffect(() => {
if (!open) return;
setName(skill?.name ?? '');
setActive(skill?.active ?? true);
}, [open, skill]);
return (
<Modal
open={open}
onClose={onClose}
title={skill ? 'ویرایش مهارت' : 'افزودن مهارت'}
footer={
<div className="row-actions">
<button type="button" className="btn secondary" onClick={onClose}>انصراف</button>
<button type="submit" form="skill-form" className="btn primary" disabled={saving || name.trim() === ''}>
{saving ? 'در حال ذخیره...' : 'ذخیره'}
</button>
</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>
);
}