Resources, branches, price lists, holidays and the new categories page sat in the settings menu but rendered bare, so clicking one made the settings sidebar disappear — the subscription page was the only one that kept it. Eleven pages now wrap in SettingsLayout with the key of the menu entry they belong to, and the four resource pages (list, types, skills, pools) share one menu entry plus a sub-nav between them, rather than four entries that would make the menu a third longer without making anything clearer. .seg accepts `a` as well as `button`, and treats `active` as an alias of `on`. Both were needed: cross-page tabs must be real links, and the pages already using `active` (service detail, clinic appointment settings) had no visible highlight at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
164 lines
6.6 KiB
TypeScript
164 lines
6.6 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 { 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 SettingsLayout from '../components/layout/SettingsLayout';
|
|
import ResourcesSubNav from '../components/resources/ResourcesSubNav';
|
|
|
|
/**
|
|
* مهارتها — «کدام اپراتور مجاز است با کدام دستگاه کار کند» یک اطلاعات است، نه یک
|
|
* قانون. با «عنوان شغلی» پرسنل قاطی نشود: آن متن آزاد و فقط برای نمایش است.
|
|
*/
|
|
export default function SkillsPage() {
|
|
const { skills, loading, create, update, remove } = useSkills();
|
|
const { can } = usePermissions();
|
|
const canUpdate = can('appointment_settings', '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 (
|
|
<SettingsLayout active="resources">
|
|
<div className="fade-in">
|
|
<PageHeader
|
|
title="مهارتها"
|
|
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 secondary 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>
|
|
</SettingsLayout>
|
|
);
|
|
}
|
|
|
|
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 ? 'ویرایش مهارت' : 'افزودن مهارت'}>
|
|
<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 }}>
|
|
<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 })}
|
|
>
|
|
{saving ? 'در حال ذخیره...' : 'ذخیره'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|