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
@@ -193,7 +193,6 @@ const EMPTY_REASON_TEXT: Record<string, { title: string; hint: string }> = {
tenant_holiday: { title: 'این روز تعطیل است', hint: 'در تقویم تعطیلات مجموعه، این روز تعطیل ثبت شده' },
exception: { title: 'استثنای تقویم', hint: 'کل ساعت کاری این روز با استثنای منبع پوشیده شده است' },
resource_inactive: { title: 'این منبع غیرفعال است', hint: 'برای نوبت‌دهی، منبع را از صفحهٔ «منابع» فعال کنید' },
address_inactive: { title: 'شعبهٔ این منبع غیرفعال است', hint: 'تا وقتی شعبه غیرفعال باشد، منابعش نوبت نمی‌گیرند' },
};
export default function TurnsTimeline({
@@ -3,7 +3,7 @@ import Modal from '../ui/Modal';
import { useQuery } from '@tanstack/react-query';
import SearchableSelect from '../ui/SearchableSelect';
import { api, type ApiResponse } from '../../lib/api';
import type { Branch, ClinicResource, ResourcePayload, ResourceType } from '../../types';
import type { ClinicResource, ResourcePayload, ResourceType } from '../../types';
import { useResourceDetail } from '../../hooks/useResources';
import { formatNumber } from '../../lib/utils';
@@ -15,7 +15,6 @@ type AttributeRow = { key: string; value: string };
interface Props {
open: boolean;
resource: ClinicResource | null;
addresses: Branch[];
types: ResourceType[];
saving: boolean;
onClose: () => void;
@@ -23,7 +22,7 @@ interface Props {
}
export default function ResourceFormModal({
open, resource, addresses, types, saving, onClose, onSave,
open, resource, types, saving, onClose, onSave,
}: Props) {
// شمار نوبت‌های آینده فقط برای منبعِ موجود معنا دارد و فقط وقتی مودال باز است.
const { upcomingAppointments: upcoming } = useResourceDetail(open ? resource?.uuid : undefined);
@@ -39,7 +38,6 @@ export default function ResourceFormModal({
const doctorsLoading = doctorsQuery.isLoading;
const [name, setName] = useState('');
const [addressUuid, setAddressUuid] = useState<string | null>(null);
const [typeUuid, setTypeUuid] = useState<string | null>(null);
const [supervisorUuid, setSupervisorUuid] = useState<string | null>(null);
const [capacity, setCapacity] = useState('1');
@@ -51,7 +49,6 @@ export default function ResourceFormModal({
useEffect(() => {
if (!open) return;
setName(resource?.name ?? '');
setAddressUuid(resource?.address_uuid ?? null);
setTypeUuid(resource?.type_uuid ?? null);
setSupervisorUuid(resource?.supervisor?.uuid ?? null);
setCapacity(String(resource?.capacity ?? 1));
@@ -68,7 +65,7 @@ export default function ResourceFormModal({
const invalid =
name.trim() === '' ||
!supervisorUuid ||
(!isEdit && (!addressUuid || !typeUuid)) ||
(!isEdit && !typeUuid) ||
!Number.isFinite(parsedCapacity) ||
parsedCapacity < 1;
@@ -88,10 +85,10 @@ export default function ResourceFormModal({
supervisor_doctor_uuid: supervisorUuid!,
};
// شعبه و نوع فقط هنگام ساخت فرستاده می‌شوند؛ جفت محیطِ منبع از آدرس مشتق شده و
// جابه‌جا کردنش یعنی همان منبع در محیط دیگری ظاهر شود.
// نوع فقط هنگام ساخت فرستاده می‌شود؛ جفت محیطِ منبع از آدرسِ محیط مشتق می‌شود و
// جابه‌جا کردنش یعنی همان منبع در محیط دیگری ظاهر شود. آدرس پرسیده نمی‌شود:
// منابع دامنهٔ شعبه ندارند و سرور آدرسِ خودِ کلینیک را برمی‌دارد.
if (!isEdit) {
payload.address_uuid = addressUuid!;
payload.type_uuid = typeUuid!;
}
@@ -106,16 +103,6 @@ export default function ResourceFormModal({
</Field>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 12 }}>
<Field label="شعبه">
<SearchableSelect
options={addresses.map((b) => ({ value: b.uuid, label: b.name || 'بدون نام' }))}
value={addressUuid}
onChange={(v) => setAddressUuid(v ? String(v) : null)}
placeholder="شعبه را انتخاب کنید"
isDisabled={isEdit}
height={38}
/>
</Field>
<Field label="نوع منبع">
<SearchableSelect
options={types.map((t) => ({ value: t.uuid, label: t.name }))}
@@ -126,8 +113,8 @@ export default function ResourceFormModal({
height={38}
/>
</Field>
{/* ناظر برخلاف شعبه و نوع در ویرایش هم قابل تغییر است: پزشکِ مسئولِ یک
دستگاه عوض می‌شود، ولی محیطِ منبع نه. */}
{/* ناظر برخلاف نوع در ویرایش هم قابل تغییر است: پزشکِ مسئولِ یک دستگاه
عوض می‌شود، ولی محیطِ منبع نه. */}
<Field label="پزشک ناظر">
<SearchableSelect
options={doctors.map((d) => ({ value: d.uuid, label: d.name }))}
@@ -142,7 +129,7 @@ export default function ResourceFormModal({
{isEdit && (
<p style={{ fontSize: 12, color: 'var(--text-3)', margin: 0 }}>
شعبه و نوع منبع پس از ساخت تغییر نمیکنند؛ برای جابهجایی، منبع تازه بسازید.
نوع منبع پس از ساخت تغییر نمیکند؛ برای تغییرش، منبع تازه بسازید.
</p>
)}
@@ -36,7 +36,7 @@ interface Props {
const SOURCE_LABELS: Record<string, string> = {
resource_option: 'همین منبع',
resource_service: 'منبع، روی سرویس والد',
branch: 'شعبه',
branch: 'تنظیم محل نوبت‌دهی',
service_default: 'پیش‌فرض سرویس',
};
+2 -2
View File
@@ -47,8 +47,8 @@ export interface HoldResult {
}
export const REASON_LABELS: Record<string, string> = {
no_capacity_in_range: 'در این بازه هیچ ظرفیتی نیست — بازه را بزرگ‌تر کنید یا شعبهٔ دیگری را امتحان کنید.',
no_working_hours: 'شعبه در این بازه ساعت کاری ندارد.',
no_capacity_in_range: 'در این بازه هیچ ظرفیتی نیست — بازه را بزرگ‌تر کنید یا سرویس دیگری را امتحان کنید.',
no_working_hours: 'در این بازه ساعت کاری تعریف نشده است.',
no_eligible_resource: 'هیچ منبعی شرایط بخش‌های این خدمت را ندارد.',
};
+3 -2
View File
@@ -6,7 +6,7 @@ import type {
} from '../types';
/**
* منابع: هر چیزی که ممکن است اشغال باشد. هر منبع مال یک شعبه است، و شعبه همان
* منابع: هر چیزی که ممکن است اشغال باشد. هر منبع مال محیط جاری است؛ لنگرش همان
* آدرس محل نوبت‌دهی است — پس فیلترها با `address_uuid` کار می‌کنند نه `branch_id`.
*/
const RESOURCES_KEY = 'resources';
@@ -203,7 +203,8 @@ export function useResourcePools() {
});
const create = useMutation({
mutationFn: (d: { address_uuid: string; type_uuid: string; name: string }) =>
// آدرس فرستاده نمی‌شود: منابع دامنهٔ شعبه ندارند و سرور آدرسِ خودِ محیط را برمی‌دارد.
mutationFn: (d: { type_uuid: string; name: string }) =>
api.post<ApiResponse<ResourcePool>>('/api/v1/resource-pools', d),
onSuccess: () => { toast.success('استخر افزوده شد'); invalidate(); },
onError: (e) => fail(e, 'افزودن استخر ناموفق بود'),
+8 -15
View File
@@ -33,6 +33,13 @@ export default function ResourceBookingPage() {
const navigate = useNavigate();
const [params] = useSearchParams();
const { addresses } = useAddresses();
/**
* محل نوبت‌دهی از خودِ محیط می‌آید، نه از یک انتخابگر.
*
* منابع دامنهٔ شعبه ندارند: دستگاه و اتاق مالِ همین کلینیک‌اند و آن select همیشه یک
* گزینه داشت — یک کلیک اجباری که هیچ تصمیمی نبود.
*/
const branchUuid = addresses[0]?.uuid ?? '';
const { items: services } = useAllServiceItems();
const { create, release, confirm, rebook } = useHold();
@@ -46,7 +53,6 @@ export default function ResourceBookingPage() {
const rebookUuid = params.get('rebook');
const [serviceUuid, setServiceUuid] = useState('');
const [branchUuid, setBranchUuid] = useState('');
const [days, setDays] = useState('7');
const [searching, setSearching] = useState(false);
@@ -96,7 +102,7 @@ export default function ResourceBookingPage() {
/**
* گزینه‌های جایگزین یک نقش: منابعی که در **همین زمان** پیشنهاد شده‌اند.
*
* فهرست کاملِ منابع شعبه اینجا غلط است — منبعی که موتور برای این زمان نداده، آزاد
* فهرست کاملِ منابع محیط اینجا غلط است — منبعی که موتور برای این زمان نداده، آزاد
* نبوده، و نشان دادنش یعنی اپراتور چیزی انتخاب کند که ۴۰۹ می‌گیرد.
*/
const optionsFor = (role: string): { value: string; label: string }[] =>
@@ -154,19 +160,6 @@ export default function ResourceBookingPage() {
/>
</div>
<div className="field-block" style={{ minWidth: 200, margin: 0 }}>
<label>شعبه</label>
<SearchableSelect
value={branchUuid}
onChange={(v) => {
setBranchUuid(String(v ?? ''));
setPickedSlot(null);
}}
options={addresses.map((b) => ({ value: b.uuid, label: b.name || 'بدون نام' }))}
placeholder="انتخاب شعبه"
/>
</div>
<div className="field-block" style={{ minWidth: 160, margin: 0 }}>
<label>بازه</label>
<SearchableSelect
@@ -78,7 +78,7 @@ describe('ResourceDetailPage', () => {
mockApi();
renderPage();
await waitFor(() => expect(screen.getByText('شعبهٔ مرکزی')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('ظرفیت هم‌زمان')).toBeInTheDocument());
expect(screen.getByText('ظرفیت هم‌زمان')).toBeInTheDocument();
expect(screen.getByText('کار با لیزر · 4')).toBeInTheDocument();
});
@@ -193,7 +193,7 @@ describe('ResourceDetailPage', () => {
const user = userEvent.setup();
renderPage();
await waitFor(() => expect(screen.getByText('شعبهٔ مرکزی')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('ظرفیت هم‌زمان')).toBeInTheDocument());
await user.click(screen.getByRole('button', { name: 'دسته‌بندی‌ها' }));
await waitFor(() => expect(screen.getByText(/فقط انتخاب می‌شود/)).toBeInTheDocument());
@@ -221,7 +221,7 @@ describe('ResourceDetailPage', () => {
const user = userEvent.setup();
renderPage();
await waitFor(() => expect(screen.getByText('شعبهٔ مرکزی')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('ظرفیت هم‌زمان')).toBeInTheDocument());
await user.click(screen.getByRole('button', { name: 'غیرفعال‌سازی موقت' }));
expect(await screen.findByText(/یک بازهٔ مشخص را می‌بندد/)).toBeInTheDocument();
@@ -238,7 +238,7 @@ describe('ResourceDetailPage', () => {
mockApi();
renderPage();
await waitFor(() => expect(screen.getByText('شعبهٔ مرکزی')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('ظرفیت هم‌زمان')).toBeInTheDocument());
// منبعِ نمونه فعال است، پس اقدام «غیرفعال کردن» پیشنهاد می‌شود.
expect(screen.getByRole('button', { name: 'غیرفعال کردن' })).toBeInTheDocument();
@@ -252,7 +252,7 @@ describe('ResourceDetailPage', () => {
const user = userEvent.setup();
renderPage();
await waitFor(() => expect(screen.getByText('شعبهٔ مرکزی')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('ظرفیت هم‌زمان')).toBeInTheDocument());
await user.click(screen.getByRole('button', { name: 'غیرفعال کردن' }));
expect(await screen.findByText(/در جستجوی وقت و رزرو نوبت ظاهر نمی‌شود/)).toBeInTheDocument();
+1 -5
View File
@@ -13,7 +13,6 @@ import ResourceCategoriesPanel from '../components/resources/ResourceCategoriesP
import ResourceBlocksPanel from '../components/resources/ResourceBlocksPanel';
import { useUrlState } from '../hooks/useUrlState';
import { usePermissions } from '../hooks/usePermissions';
import { useAddresses } from '../hooks/useAddresses';
import { useResourceDetail, useResourceServices, useResources, useResourceTypes, useSkills } from '../hooks/useResources';
import { useAllServiceItems } from '../hooks/useServiceCatalog';
import type { ClinicResource } from '../types';
@@ -48,7 +47,6 @@ export default function ResourceDetailPage() {
const tab = (TABS.some((t) => t.id === urlState.tab) ? urlState.tab : 'info') as TabId;
const { resource, loading } = useResourceDetail(resourceUuid);
const { addresses } = useAddresses();
const { types } = useResourceTypes();
const { skills } = useSkills();
const { update, setSkills, setCategories } = useResources();
@@ -74,7 +72,7 @@ export default function ResourceDetailPage() {
<div className="fade-in">
<PageHeader
title={resource.name}
description={`${resource.type_name}${resource.subject_kind ? ` · ${SUBJECT_LABEL[resource.subject_kind]}` : ' · تجهیزات'}${resource.address_name ? ` · ${resource.address_name}` : ''}`}
description={`${resource.type_name}${resource.subject_kind ? ` · ${SUBJECT_LABEL[resource.subject_kind]}` : ' · تجهیزات'}`}
backTo="/admin/resources"
breadcrumbs={[{ label: 'منابع', to: '/admin/resources' }, { label: resource.name }]}
action={
@@ -175,7 +173,6 @@ export default function ResourceDetailPage() {
<ResourceFormModal
open={editOpen}
resource={resource}
addresses={addresses}
types={types}
saving={update.isPending}
onClose={() => setEditOpen(false)}
@@ -216,7 +213,6 @@ function InfoTab({ resource, canUpdate, toggling, onToggleActive }: {
return (
<div style={{ display: 'grid', gap: 'var(--gap)' }}>
<div className="card card-pad">
<Row label="شعبه">{resource.address_name || '—'}</Row>
<Row label="نوع منبع">{resource.type_name}</Row>
<Row label="ظرفیت هم‌زمان">{resource.capacity} نفر</Row>
<Row label="آماده‌سازی / تمیزکاری">
+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;
+5 -4
View File
@@ -81,7 +81,7 @@ describe('ResourcesPage', () => {
it('passes the URL filters straight to the server', async () => {
mockApi();
renderWithProviders(<ResourcesPage />, {
route: '/admin/resources?address=b1&type=t1&skill=s1&status=1',
route: '/admin/resources?type=t1&skill=s1&status=1',
});
await waitFor(() => expect(get).toHaveBeenCalled());
@@ -89,13 +89,14 @@ describe('ResourcesPage', () => {
const called = get.mock.calls.map((c) => String(c[0]));
const listCall = called.find((p) => p.startsWith('/api/v1/resources'));
expect(listCall).toContain('address_uuid=b1');
// شعبه فیلتر نیست: منابع دامنهٔ شعبه ندارند.
expect(listCall).not.toContain('address_uuid');
expect(listCall).toContain('type_uuid=t1');
expect(listCall).toContain('skill_uuid=s1');
expect(listCall).toContain('active=1');
});
it('sends address and type only when creating, never when editing', async () => {
it('sends the type only when creating, never when editing', async () => {
mockApi();
renderWithProviders(<ResourcesPage />, { route: '/admin/resources' });
@@ -104,7 +105,7 @@ describe('ResourcesPage', () => {
fireEvent.click(screen.getByText('افزودن منبع'));
fireEvent.change(screen.getByPlaceholderText('لیزر آلکساندرایت ۱'), { target: { value: 'لیزر تازه' } });
// شعبه و نوع هنوز انتخاب نشده‌اند → ذخیره غیرفعال است.
// نوع هنوز انتخاب نشده است → ذخیره غیرفعال است.
expect(screen.getByText('ذخیره').closest('button')).toBeDisabled();
});
+1 -16
View File
@@ -7,7 +7,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 { useResources, useResourceTypes, useSkills } from '../hooks/useResources';
import ResourceFormModal from '../components/resources/ResourceFormModal';
import type { ClinicResource } from '../types';
@@ -29,17 +28,15 @@ const SUBJECT_LABEL: Record<string, string> = {
*/
export default function ResourcesPage() {
const [urlState, setUrlState] = useUrlState({
search: '', address: '', type: '', skill: '', status: '',
search: '', type: '', skill: '', status: '',
});
const { addresses } = useAddresses();
const { types } = useResourceTypes();
const { skills } = useSkills();
const { can } = usePermissions();
const canUpdate = can('appointment_settings', 'update');
const { resources, loading, create } = useResources({
address_uuid: urlState.address || undefined,
type_uuid: urlState.type || undefined,
skill_uuid: urlState.skill || undefined,
active: urlState.status || undefined,
@@ -67,7 +64,6 @@ export default function ResourcesPage() {
</div>
),
},
{ key: 'address_name', header: 'شعبه', render: (r) => <span style={{ fontSize: 13 }}>{r.address_name || '—'}</span> },
{
key: 'capacity',
header: 'ظرفیت هم‌زمان',
@@ -127,16 +123,6 @@ export default function ResourcesPage() {
emptyMessage="هیچ منبعی با این فیلترها یافت نشد"
headerExtra={
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginRight: 'auto' }}>
<div style={{ minWidth: 180 }}>
<SearchableSelect
options={addresses.map((b) => ({ value: b.uuid, label: b.name || 'بدون نام' }))}
value={urlState.address || null}
onChange={(v) => setUrlState({ address: v ? String(v) : '' })}
placeholder="همهٔ شعبه‌ها"
isClearable
height={36}
/>
</div>
<div style={{ minWidth: 170 }}>
<SearchableSelect
options={types.map((t) => ({ value: t.uuid, label: t.name }))}
@@ -185,7 +171,6 @@ export default function ResourcesPage() {
<ResourceFormModal
open={createOpen}
resource={null}
addresses={addresses}
types={types}
saving={create.isPending}
onClose={() => setCreateOpen(false)}