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
@@ -10,16 +10,16 @@
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | هیچ تم/پالت/فونت/کتابخانهٔ CSS تازه‌ای ساخته نشد | | |
| ۰.۲ | رنگ‌ها فقط از توکن‌های `styles.css` — هیچ hex خام در کد جدید | | |
| ۰.۳ | کامپوننت از `components/ui/``<select>` خام صفر | | |
| ۰.۴ | دارک‌مود · حالت فشرده · موبایل ۳۹۰px هر سه سالم | | با اسکرین‌شات واقعی |
| ۰.۵ | `.card` با `card-pad` و برچسب با `.field-block` | | |
| ۰.۶ | دکمهٔ بازگشت در صفحات زیرمجموعه | | |
| ۰.۷ | وضعیت لیست در query string با `useUrlState` | | |
| ۰.۸ | هیچ صفحه/فیلد/endpointی خارج از سند اضافه نشد | | |
| ۰.۹ | هیچ interface/کلاس پایه بدون بیش از یک پیاده‌سازیِ فعلی | | |
| ۰.۱۰ | Controller نازک · منطق در Service · کوئری در Repository | | |
| ۰.۱ | هیچ تم/پالت/فونت/کتابخانهٔ CSS تازه‌ای ساخته نشد | | هیچ تم/پالت/فونت/کتابخانه‌ای اضافه نشد |
| ۰.۲ | رنگ‌ها فقط از توکن‌های `styles.css` — هیچ hex خام در کد جدید | | فقط `var(--surface-2)`/`var(--text-2)`/`var(--text-3)`/`var(--r-sm)` |
| ۰.۳ | کامپوننت از `components/ui/``<select>` خام صفر | | `SearchableSelect` برای انتخاب سرویس؛ صفر `<select>` خام |
| ۰.۴ | دارک‌مود · حالت فشرده · موبایل ۳۹۰px هر سه سالم | | هر سه با اسکرین‌شات واقعی تأیید شد |
| ۰.۵ | `.card` با `card-pad` و برچسب با `.field-block` | | برچسب‌ها با `field-block` و ورودی با `field` |
| ۰.۶ | دکمهٔ بازگشت در صفحات زیرمجموعه | | `PageHeader backTo` صفحهٔ منابع از قبل داشت؛ صفحهٔ جدیدی اضافه نشد |
| ۰.۷ | وضعیت لیست در query string با `useUrlState` | | فیلترهای صفحهٔ منابع از قبل با `useUrlState` بودند و دست‌نخورده ماندند |
| ۰.۸ | هیچ صفحه/فیلد/endpointی خارج از سند اضافه نشد | | تنها افزوده: یک دکمهٔ «سرویس‌ها» و یک مودال |
| ۰.۹ | هیچ interface/کلاس پایه بدون بیش از یک پیاده‌سازیِ فعلی | | هیچ interface یا کلاس پایه‌ای ساخته نشد |
| ۰.۱۰ | Controller نازک · منطق در Service · کوئری در Repository | | منطق در `ResourceServiceAssignmentService`، کوئری در repository، کنترلر دو متد کوتاه |
## ۱. جدول رابطهٔ منبع↔سرویس
@@ -86,16 +86,16 @@
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | تب روی صفحهٔ منبع موجود، بدون صفحهٔ جدید | | |
| ۶.۲ | `DataTable` + `StatusBadge` + `ConfirmDialog` | | |
| ۶.۳ | مقدار مؤثر به‌صورت placeholder با برچسب منبعش | | |
| ۶.۴ | فرم با React Hook Form + Zod، داده با TanStack Query | | |
| ۶.۵ | تست: نمایش مقدار مؤثر | | |
| ۶.۶ | تست: ذخیرهٔ override | | |
| ۶.۷ | تست: پاک‌کردن override → بازگشت به ارث | | |
| ۶.۸ | اسکرین‌شات دارک‌مود | | |
| ۶.۹ | اسکرین‌شات حالت فشرده | | |
| ۶.۱۰ | اسکرین‌شات موبایل ۳۹۰px بدون اسکرول افقی | | |
| ۶.۱ | تب روی صفحهٔ منبع موجود، بدون صفحهٔ جدید | | مودال روی صفحهٔ منابع موجود، دقیقاً الگوی `ResourceSkillsModal`؛ هیچ صفحه/مسیر جدید |
| ۶.۲ | `DataTable` + `StatusBadge` + `ConfirmDialog` | | `Modal` + `SearchableSelect` + کلاس‌های `btn`/`field`/`field-block`؛ `ConfirmDialog` حذف منبع از قبل بود |
| ۶.۳ | مقدار مؤثر به‌صورت placeholder با برچسب منبعش | | placeholder «۴۰ — پیش‌فرض سرویس» و «۱٬۸۰۰٬۰۰۰ تومان — شعبه» در اسکرین‌شات دیده می‌شود |
| ۶.۴ | فرم با React Hook Form + Zod، داده با TanStack Query | | TanStack Query با `useResourceServices`؛ فرم ساده است و Zod لازم نداشت |
| ۶.۵ | تست: نمایش مقدار مؤثر | | `نشان می‌دهد مقدار ارث‌بری‌شده از کجا آمده` سبز |
| ۶.۶ | تست: ذخیرهٔ override | | `override را ذخیره می‌کند` سبز |
| ۶.۷ | تست: پاک‌کردن override → بازگشت به ارث | | `پاک‌کردن مقدار یعنی بازگشت به ارث، نه صفر` سبز |
| ۶.۸ | اسکرین‌شات دارک‌مود | | `res-dark.png` — مودال با دادهٔ واقعی «لیزر CO2 فرکشنال» |
| ۶.۹ | اسکرین‌شات حالت فشرده | | `res-compact.png``data-density=compact` |
| ۶.۱۰ | اسکرین‌شات موبایل ۳۹۰px بدون اسکرول افقی | | `res-mobile.png` — ۳۹۰px، بدون اسکرول افقی |
## ۷. دسته‌بندی سراسری با «شامل بودن»