refactor(resource): drop the branch domain from resources

Resources never needed a branch: devices and rooms belong to the clinic
itself, and the picker always had exactly one option — a mandatory click
that decided nothing.

- `address_uuid` is now optional on resource and pool creation; when it is
  missing the environment's own address is used. Clients still sending it
  keep working.
- The panel no longer asks for or displays a branch anywhere: resource
  form, list column and filter, pool form and column, detail row, and the
  resource-first booking page.
- Availability no longer gates on `doctor_addresses.active`. That gate shut
  down every device of a clinic whose address row happened to be inactive,
  with a message no page in the panel could act on — no endpoint writes
  that column at all.

`address_id` stays on the resource: the timezone and the tenant pair are
derived from it. It is simply no longer the user's decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-03 14:57:47 +03:30
co-authored by Claude Opus 5
parent 05bb4a4ade
commit 581a553516
19 changed files with 246 additions and 117 deletions
+10 -30
View File
@@ -8,7 +8,6 @@ import SearchableSelect from '../components/ui/SearchableSelect';
import { ActiveBadge } from '../components/ui/StatusBadge';
import { useUrlState } from '../hooks/useUrlState';
import { usePermissions } from '../hooks/usePermissions';
import { useAddresses } from '../hooks/useAddresses';
import { useResourcePools, useResources, useResourceTypes } from '../hooks/useResources';
import type { ResourcePool } from '../types';
import ResourcesSubNav from '../components/resources/ResourcesSubNav';
@@ -16,12 +15,11 @@ import ResourcesSubNav from '../components/resources/ResourcesSubNav';
/**
* استخر منابع — گروهی از منابع که جایگزین کامل یکدیگرند.
*
* اعضا باید هم‌شعبه و هم‌نوعِ خودِ استخر باشند؛ سرور این را اجبار می‌کند و فرم هم
* اعضا باید هم‌نوعِ خودِ استخر باشند؛ سرور این را اجبار می‌کند و فرم هم
* فهرست انتخاب را به همان‌ها محدود می‌کند تا کاربر به خطای ۴۲۲ نخورد.
*/
export default function ResourcePoolsPage() {
const { pools, loading, create, update, remove, setMembers } = useResourcePools();
const { addresses } = useAddresses();
const { types } = useResourceTypes();
const { can } = usePermissions();
const canUpdate = can('appointment_settings', 'update');
@@ -47,7 +45,6 @@ export default function ResourcePoolsPage() {
</div>
),
},
{ key: 'address_name', header: 'شعبه', render: (p) => <span style={{ fontSize: 13 }}>{p.address_name || '—'}</span> },
{
key: 'members',
header: 'اعضا',
@@ -69,7 +66,7 @@ export default function ResourcePoolsPage() {
<div className="fade-in">
<PageHeader
title="استخر منابع"
description="منابعی که جایگزین کامل یکدیگرند — مثل «لیزرهای آلکساندرایت». همهٔ اعضا باید در یک شعبه و از یک نوع باشند."
description="منابعی که جایگزین کامل یکدیگرند — مثل «لیزرهای آلکساندرایت». همهٔ اعضا باید از یک نوع باشند."
backTo="/admin/resources"
breadcrumbs={[{ label: 'منابع', to: '/admin/resources' }, { label: 'استخر منابع' }]}
action={
@@ -116,7 +113,6 @@ export default function ResourcePoolsPage() {
<CreatePoolModal
open={creating}
addresses={addresses}
types={types}
saving={create.isPending}
onClose={() => setCreating(false)}
@@ -147,27 +143,24 @@ export default function ResourcePoolsPage() {
}
function CreatePoolModal({
open, addresses, types, saving, onClose, onSave,
open, types, saving, onClose, onSave,
}: {
open: boolean;
addresses: ReturnType<typeof useAddresses>['addresses'];
types: ReturnType<typeof useResourceTypes>['types'];
saving: boolean;
onClose: () => void;
onSave: (payload: { address_uuid: string; type_uuid: string; name: string }) => void;
onSave: (payload: { type_uuid: string; name: string }) => void;
}) {
const [name, setName] = useState('');
const [addressUuid, setAddressUuid] = useState<string | null>(null);
const [typeUuid, setTypeUuid] = useState<string | null>(null);
useEffect(() => {
if (!open) return;
setName('');
setAddressUuid(null);
setTypeUuid(null);
}, [open]);
const invalid = name.trim() === '' || !addressUuid || !typeUuid;
const invalid = name.trim() === '' || !typeUuid;
return (
<Modal open={open} onClose={onClose} title="افزودن استخر منابع">
@@ -177,17 +170,6 @@ function CreatePoolModal({
<input className="field" value={name} onChange={(e) => setName(e.target.value)} placeholder="لیزرهای آلکساندرایت" />
</div>
<div style={{ display: 'grid', gap: 6 }}>
<label style={{ fontSize: 12, color: 'var(--text-2)' }}>شعبه</label>
<SearchableSelect
options={addresses.map((b) => ({ value: b.uuid, label: b.name || 'بدون نام' }))}
value={addressUuid}
onChange={(v) => setAddressUuid(v ? String(v) : null)}
placeholder="شعبه را انتخاب کنید"
height={38}
/>
</div>
<div style={{ display: 'grid', gap: 6 }}>
<label style={{ fontSize: 12, color: 'var(--text-2)' }}>نوع منبع</label>
<SearchableSelect
@@ -200,7 +182,7 @@ function CreatePoolModal({
</div>
<p style={{ fontSize: 12, color: 'var(--text-3)', margin: 0 }}>
شعبه و نوع پس از ساخت تغییر نمیکنند اعضا باید با هر دو بخوانند.
نوع پس از ساخت تغییر نمیکند اعضا باید با آن بخوانند.
</p>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
@@ -209,7 +191,7 @@ function CreatePoolModal({
type="button"
className="btn primary"
disabled={saving || invalid}
onClick={() => onSave({ address_uuid: addressUuid!, type_uuid: typeUuid!, name: name.trim() })}
onClick={() => onSave({ type_uuid: typeUuid!, name: name.trim() })}
>
{saving ? 'در حال ذخیره...' : 'ذخیره'}
</button>
@@ -229,11 +211,9 @@ function PoolMembersModal({
}) {
const [chosen, setChosen] = useState<string[]>([]);
// فهرست انتخاب به همان شعبه و نوعِ استخر محدود است — همان قاعده‌ای که سرور با ۴۲۲
// اجبار می‌کند، پس کاربر اصلاً به آن خطا نمی‌خورد.
const { resources } = useResources(
pool ? { address_uuid: pool.address_uuid, type_uuid: pool.type_uuid } : {},
);
// فهرست انتخاب به نوعِ استخر محدود است — همان قاعده‌ای که سرور با ۴۲۲ اجبار می‌کند،
// پس کاربر اصلاً به آن خطا نمی‌خورد.
const { resources } = useResources(pool ? { type_uuid: pool.type_uuid } : {});
useEffect(() => {
if (!pool) return;