- 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.
182 lines
6.9 KiB
TypeScript
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>
|
|
);
|
|
}
|