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
+31 -1
View File
@@ -2,7 +2,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { api, ApiError, type ApiResponse } from '../lib/api';
import type {
ClinicResource, ResourcePool, ResourcePayload, ResourceType, Skill,
ClinicResource, ResourcePool, ResourcePayload, ResourceServiceOffering, ResourceType, Skill,
} from '../types';
/**
@@ -77,6 +77,36 @@ export function useResources(filters: ResourceFilters = {}) {
};
}
/**
* سرویس‌هایی که یک منبع ارائه می‌دهد.
*
* مقدار مؤثر و منبعِ هر عدد از سرور می‌آید، نه از محاسبهٔ فرانت: زنجیرهٔ حل چهار سطح
* دارد و بازسازی‌اش اینجا یعنی دو پیاده‌سازی که با هم واگرا می‌شوند.
*/
export function useResourceServices(resourceUuid?: string) {
const qc = useQueryClient();
const key = ['resource-services', resourceUuid];
const query = useQuery({
queryKey: key,
queryFn: () => api.get<ApiResponse<ResourceServiceOffering[]>>(`/api/v1/resource/${resourceUuid}/services`),
enabled: !!resourceUuid,
});
const save = useMutation({
mutationFn: ({ uuid, services }: { uuid: string; services: Array<Record<string, unknown>> }) =>
api.put<ApiResponse<ResourceServiceOffering[]>>(`/api/v1/resource/${uuid}/services`, { services }),
onSuccess: () => {
toast.success('سرویس‌های منبع ذخیره شد');
qc.invalidateQueries({ queryKey: ['resource-services'] });
qc.invalidateQueries({ queryKey: [RESOURCES_KEY] });
},
onError: (e) => fail(e, 'ذخیرهٔ سرویس‌ها ناموفق بود'),
});
return { offerings: query.data?.data ?? [], loading: query.isLoading, save };
}
export function useResourceTypes() {
const qc = useQueryClient();
const invalidate = () => qc.invalidateQueries({ queryKey: TYPES_KEY });