feat(resources): one tabbed page per resource
In the resource-first model a resource is the unit of capacity, so its
working hours, holidays, services, skills and categories belong to it — not
scattered across a list page's modals plus a separate calendar page.
/admin/resources/{uuid} now carries six tabs and the active tab lives in the
query string, so back and refresh land on the same view. The old
/calendar URL redirects to ?tab=hours instead of 404ing.
The skills and services modal bodies became panels the tab renders directly;
the modals are now thin wrappers, so the list page keeps working unchanged
and there is still one implementation of each editor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,104 +1,27 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import Modal from '../ui/Modal';
|
||||
import SearchableSelect from '../ui/SearchableSelect';
|
||||
import ResourceSkillsPanel, { type SkillLine } from './ResourceSkillsPanel';
|
||||
import type { ClinicResource, Skill } from '../../types';
|
||||
|
||||
type Line = { skill_uuid: string; level: number };
|
||||
|
||||
interface Props {
|
||||
resource: ClinicResource | null;
|
||||
skills: Skill[];
|
||||
saving: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (lines: Line[]) => void;
|
||||
onSave: (lines: SkillLine[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* مهارتهای یک منبع. ذخیره یک PUT است و **جایگزینی کامل**: مهارتی که اینجا نباشد،
|
||||
* از منبع برداشته میشود.
|
||||
*/
|
||||
/** همان پنل مهارتها، در قاب مودالِ فهرست منابع. */
|
||||
export default function ResourceSkillsModal({ resource, skills, saving, onClose, onSave }: Props) {
|
||||
const [lines, setLines] = useState<Line[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!resource) return;
|
||||
setLines(resource.skills.map((s) => ({ skill_uuid: s.skill_uuid, level: s.level })));
|
||||
}, [resource]);
|
||||
|
||||
const chosen = new Set(lines.map((l) => l.skill_uuid));
|
||||
const available = skills.filter((s) => !chosen.has(s.uuid));
|
||||
|
||||
const nameOf = (uuid: string) => skills.find((s) => s.uuid === uuid)?.name ?? uuid;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={resource !== null}
|
||||
onClose={onClose}
|
||||
title={`مهارتهای ${resource?.name ?? 'منبع'}`}
|
||||
>
|
||||
<div style={{ display: 'grid', gap: 14 }}>
|
||||
{skills.length === 0 && (
|
||||
<p style={{ fontSize: 13, color: 'var(--text-3)', margin: 0 }}>
|
||||
هنوز هیچ مهارتی تعریف نشده است. اول از صفحهٔ «مهارتها» یکی بسازید.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{lines.length === 0 ? (
|
||||
<p style={{ fontSize: 13, color: 'var(--text-3)', margin: 0 }}>این منبع هیچ مهارتی ندارد.</p>
|
||||
) : (
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
{lines.map((line, index) => (
|
||||
<div key={line.skill_uuid} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<span style={{ flex: 1, fontSize: 13, fontWeight: 600 }}>{nameOf(line.skill_uuid)}</span>
|
||||
<label style={{ fontSize: 12, color: 'var(--text-3)' }}>سطح</label>
|
||||
<div style={{ width: 92 }}>
|
||||
<SearchableSelect
|
||||
options={[1, 2, 3, 4, 5].map((lv) => ({ value: String(lv), label: String(lv) }))}
|
||||
value={String(line.level)}
|
||||
onChange={(v) =>
|
||||
setLines((l) => l.map((x, i) => (i === index ? { ...x, level: Number(v) || 1 } : x)))
|
||||
}
|
||||
placeholder="سطح"
|
||||
height={36}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn secondary sm"
|
||||
onClick={() => setLines((l) => l.filter((_, i) => i !== index))}
|
||||
aria-label={`حذف ${nameOf(line.skill_uuid)}`}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{available.length > 0 && (
|
||||
<div style={{ display: 'grid', gap: 6 }}>
|
||||
<label style={{ fontSize: 12, color: 'var(--text-2)' }}>افزودن مهارت</label>
|
||||
<SearchableSelect
|
||||
options={available.map((s) => ({ value: s.uuid, label: s.name }))}
|
||||
value={null}
|
||||
onChange={(v) => v && setLines((l) => [...l, { skill_uuid: String(v), level: 1 }])}
|
||||
placeholder="یک مهارت انتخاب کنید"
|
||||
height={38}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p style={{ fontSize: 12, color: 'var(--text-3)', margin: 0 }}>
|
||||
ذخیره کل فهرست را جایگزین میکند؛ مهارتی که اینجا نباشد از منبع برداشته میشود.
|
||||
</p>
|
||||
|
||||
<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} onClick={() => onSave(lines)}>
|
||||
{saving ? 'در حال ذخیره...' : 'ذخیره'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Modal open={resource !== null} onClose={onClose} title={`مهارتهای ${resource?.name ?? 'منبع'}`}>
|
||||
<ResourceSkillsPanel
|
||||
resource={resource}
|
||||
skills={skills}
|
||||
saving={saving}
|
||||
onCancel={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user