Manage a resource's services from the panel

A "سرویس‌ها" action on each resource row opens a modal listing what that
resource performs, with its own duration and price. It follows the skills modal
exactly — same PUT-replaces-everything contract, same components, no new page
and no new route.

Leaving a cell empty means inherit, so the effective value is shown as the
placeholder along with where it came from: "40 — service default", "1,800,000
toman — branch". Without that the user cannot tell an unset field from a zero,
which is the one thing this screen has to communicate.

Two backend adjustments came out of wiring it up:

- the offering filter in findEligible is now scoped to the requirement's
  resource type. Registering lasers for a service was making the room in the
  same plan ineligible and breaking the whole booking — "who performs this" is
  about the performing role, not about rooms and support resources. The seeder
  caught this immediately.
- the scenario seeder now creates offerings and resource categories, so the
  demo data exercises this model instead of leaving every resource empty.

Three vitest tests: inherited value with its source, saving an override, and
clearing back to inheritance. Verified in the browser at 1440 dark, 1440
compact and 390 mobile — the last with no horizontal scroll.

Panel suite 648 green across 98 files, tsc clean, encore build succeeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-01 22:57:55 +03:30
co-authored by Claude Opus 5
parent a6acf3bfe2
commit 26425bec31
10 changed files with 463 additions and 35 deletions
+25 -1
View File
@@ -10,9 +10,11 @@ import { ActiveBadge } from '../components/ui/StatusBadge';
import { useUrlState } from '../hooks/useUrlState';
import { usePermissions } from '../hooks/usePermissions';
import { useBranches } from '../hooks/useBranches';
import { useResources, useResourceTypes, useSkills } from '../hooks/useResources';
import { useResourceServices, useResources, useResourceTypes, useSkills } from '../hooks/useResources';
import ResourceFormModal from '../components/resources/ResourceFormModal';
import ResourceSkillsModal from '../components/resources/ResourceSkillsModal';
import ResourceServicesModal from '../components/resources/ResourceServicesModal';
import { useAllServiceItems } from '../hooks/useServiceCatalog';
import type { ClinicResource } from '../types';
/** برچسب فارسی پلِ هر منبع؛ `null` یعنی تجهیزاتی که پشتش موجودیت دیگری نیست. */
@@ -49,9 +51,13 @@ export default function ResourcesPage() {
const [editing, setEditing] = useState<{ open: boolean; resource: ClinicResource | null }>({ open: false, resource: null });
const [skillsFor, setSkillsFor] = useState<ClinicResource | null>(null);
const [servicesFor, setServicesFor] = useState<ClinicResource | null>(null);
const [blocksFor, setBlocksFor] = useState<ClinicResource | null>(null);
const [toDelete, setToDelete] = useState<ClinicResource | null>(null);
const { offerings, save: saveServices } = useResourceServices(servicesFor?.uuid);
const { items: serviceOptions } = useAllServiceItems();
const rows = useMemo(() => {
const q = urlState.search.trim();
return q === '' ? resources : resources.filter((r) => `${r.name} ${r.type_name}`.includes(q));
@@ -185,6 +191,9 @@ export default function ResourcesPage() {
<button type="button" className="btn secondary sm" onClick={() => setSkillsFor(r)}>
مهارتها
</button>
<button type="button" className="btn secondary sm" onClick={() => setServicesFor(r)}>
سرویسها
</button>
<Link className="btn secondary sm" to={`/admin/resources/${r.uuid}/calendar`}>
تقویم
</Link>
@@ -233,6 +242,21 @@ export default function ResourcesPage() {
}
/>
<ResourceServicesModal
resource={servicesFor}
offerings={offerings}
services={serviceOptions}
saving={saveServices.isPending}
onClose={() => setServicesFor(null)}
onSave={(lines) =>
servicesFor &&
saveServices.mutate(
{ uuid: servicesFor.uuid, services: lines },
{ onSuccess: () => setServicesFor(null) },
)
}
/>
<ConfirmDialog
open={!!toDelete}
title="حذف منبع"