import React, { useEffect, useMemo, useState } from 'react'; import { PlusIcon } from '@heroicons/react/24/outline'; import PageHeader from '../components/ui/PageHeader'; import DataTable, { type Column } from '../components/ui/DataTable'; import Modal from '../components/ui/Modal'; import ConfirmDialog from '../components/ui/ConfirmDialog'; import SearchableSelect from '../components/ui/SearchableSelect'; import { ActiveBadge } from '../components/ui/StatusBadge'; import { useUrlState } from '../hooks/useUrlState'; import { usePermissions } from '../hooks/usePermissions'; import { useResourcePools, useResources, useResourceTypes } from '../hooks/useResources'; import type { ResourcePool } from '../types'; import ResourcesSubNav from '../components/resources/ResourcesSubNav'; /** * استخر منابع — گروهی از منابع که جایگزین کامل یکدیگرند. * * اعضا باید هم‌نوعِ خودِ استخر باشند؛ سرور این را اجبار می‌کند و فرم هم * فهرست انتخاب را به همان‌ها محدود می‌کند تا کاربر به خطای ۴۲۲ نخورد. */ export default function ResourcePoolsPage() { const { pools, loading, create, update, remove, setMembers } = useResourcePools(); const { types } = useResourceTypes(); const { can } = usePermissions(); const canUpdate = can('resources', 'update'); const [urlState, setUrlState] = useUrlState({ search: '' }); const [creating, setCreating] = useState(false); const [membersFor, setMembersFor] = useState(null); const [toDelete, setToDelete] = useState(null); const rows = useMemo(() => { const q = urlState.search.trim(); return q === '' ? pools : pools.filter((p) => `${p.name} ${p.type_name}`.includes(q)); }, [pools, urlState.search]); const columns: Column[] = [ { key: 'name', header: 'استخر', render: (p) => (
{p.name} {p.type_name}
), }, { key: 'members', header: 'اعضا', render: (p) => p.members.length === 0 ? ( بدون عضو ) : (
{p.members.map((m) => ( {m.resource_name} ))}
), }, { key: 'active', header: 'وضعیت', render: (p) => }, ]; return (
setCreating(true)}> افزودن استخر ) : undefined } /> setUrlState({ search: v })} searchPlaceholder="جستجو در استخرها..." emptyMessage="هنوز استخری تعریف نشده است" actions={ canUpdate ? (p) => (
) : undefined } /> setCreating(false)} onSave={(payload) => create.mutate(payload, { onSuccess: () => setCreating(false) })} /> setMembersFor(null)} onSave={(members) => membersFor && setMembers.mutate({ uuid: membersFor.uuid, members }, { onSuccess: () => setMembersFor(null) }) } /> toDelete && remove.mutate(toDelete.uuid, { onSuccess: () => setToDelete(null) })} onCancel={() => setToDelete(null)} />
); } function CreatePoolModal({ open, types, saving, onClose, onSave, }: { open: boolean; types: ReturnType['types']; saving: boolean; onClose: () => void; onSave: (payload: { type_uuid: string; name: string }) => void; }) { const [name, setName] = useState(''); const [typeUuid, setTypeUuid] = useState(null); useEffect(() => { if (!open) return; setName(''); setTypeUuid(null); }, [open]); const invalid = name.trim() === '' || !typeUuid; return (
setName(e.target.value)} placeholder="لیزرهای آلکساندرایت" />
({ value: t.uuid, label: t.name }))} value={typeUuid} onChange={(v) => setTypeUuid(v ? String(v) : null)} placeholder="نوع را انتخاب کنید" height={38} />

نوع پس از ساخت تغییر نمی‌کند — اعضا باید با آن بخوانند.

); } function PoolMembersModal({ pool, saving, onClose, onSave, }: { pool: ResourcePool | null; saving: boolean; onClose: () => void; onSave: (members: { resource_uuid: string; priority: number }[]) => void; }) { const [chosen, setChosen] = useState([]); // فهرست انتخاب به نوعِ استخر محدود است — همان قاعده‌ای که سرور با ۴۲۲ اجبار می‌کند، // پس کاربر اصلاً به آن خطا نمی‌خورد. const { resources } = useResources(pool ? { type_uuid: pool.type_uuid } : {}); useEffect(() => { if (!pool) return; setChosen(pool.members.map((m) => m.resource_uuid)); }, [pool]); const nameOf = (uuid: string) => resources.find((r) => r.uuid === uuid)?.name ?? pool?.members.find((m) => m.resource_uuid === uuid)?.resource_name ?? uuid; const available = resources.filter((r) => !chosen.includes(r.uuid)); return (
{chosen.length === 0 ? (

این استخر عضوی ندارد. استخر بدون عضو معتبر است، ولی در انتخاب منبع «هیچ منبعی» دیده می‌شود.

) : (
{chosen.map((uuid, index) => (
{index + 1} {nameOf(uuid)}
))}
)} {available.length > 0 && (
({ value: r.uuid, label: r.name }))} value={null} onChange={(v) => v && setChosen((c) => [...c, String(v)])} placeholder="یک منبع انتخاب کنید" height={38} />
)}

ترتیب همان اولویت انتخاب است. ذخیره کل فهرست را جایگزین می‌کند.

); }