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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user