feat: port inventory (انبارداری) page from tauri to admin dashboard

Add a per-tenant (doctor/clinic) Inventory domain and admin page, ported
from clinic-pro-tauri /inventory (which was static/mock) into a real feature.

Backend (src/Inventory/):
- Entities InventoryItem, InventoryPackage, InventoryPackageItem, scoped via
  entity_type/entity_id like TenantTag. Item status is derived, package total
  and availability derived at read time.
- InventoryService (stats, package assembly, availability), thin
  InventoryController with CRUD for items and packages + categories endpoint.
- Migration + docs/api/inventory.md + functional tests (10 tests, 42 assertions).

Frontend (assets/admin/):
- InventoryPage with two tabs (کالاهای مصرفی / پکیج), stat cards, items table
  (desktop + mobile cards), packages accordion, add/edit item and package
  modals, search + category filter — pixel-matched to the tauri source.
- useInventory hook (TanStack Query), route + sidebar link for doctor/clinic.
- Vitest coverage (real data, empty state, modal, packages tab).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-15 13:48:29 +03:30
co-authored by Claude Opus 4.8
parent f1e9129a2c
commit 519bc8d7f6
23 changed files with 2124 additions and 0 deletions
@@ -0,0 +1,103 @@
import React, { useEffect, useState } from 'react';
import Modal from '../ui/Modal';
import { rialToToman, tomanToRial, toEnglishDigits } from '../../lib/utils';
import type { InventoryItem, ItemPayload } from '../../hooks/useInventory';
interface Props {
open: boolean;
editing: InventoryItem | null;
saving: boolean;
onClose: () => void;
onSave: (payload: ItemPayload, uuid?: string) => void;
}
interface FormState {
name: string;
consumable: string;
unit: string;
price: string; // Toman, as typed
stock: string;
alertThreshold: string;
}
const BLANK: FormState = { name: '', consumable: '', unit: '', price: '', stock: '', alertThreshold: '' };
const digits = (v: string) => toEnglishDigits(v).replace(/\D/g, '');
/** «افزودن/ویرایش کالای جدید» — mirrors tauri ModalAddInventory field-for-field. */
export default function AddItemModal({ open, editing, saving, onClose, onSave }: Props) {
const [form, setForm] = useState<FormState>(BLANK);
const [error, setError] = useState('');
useEffect(() => {
if (!open) return;
setError('');
setForm(editing
? {
name: editing.name,
consumable: editing.consumable ?? '',
unit: editing.unit,
price: editing.price ? String(rialToToman(editing.price)) : '',
stock: String(editing.stock),
alertThreshold: String(editing.alertThreshold),
}
: BLANK);
}, [open, editing]);
const set = (k: keyof FormState) => (e: React.ChangeEvent<HTMLInputElement>) =>
setForm((f) => ({ ...f, [k]: e.target.value }));
const setNum = (k: keyof FormState) => (e: React.ChangeEvent<HTMLInputElement>) =>
setForm((f) => ({ ...f, [k]: digits(e.target.value) }));
const submit = () => {
if (form.name.trim() === '') { setError('نام کالا الزامی است'); return; }
const payload: ItemPayload = {
name: form.name.trim(),
consumable: form.consumable.trim() || null,
unit: form.unit.trim() || 'عدد',
price: form.price ? tomanToRial(Number(form.price)) : 0,
stock: form.stock ? Number(form.stock) : 0,
alertThreshold: form.alertThreshold ? Number(form.alertThreshold) : 0,
};
onSave(payload, editing?.uuid);
};
const fields: { key: keyof FormState; label: string; placeholder: string; numeric?: boolean }[] = [
{ key: 'name', label: 'نام کالا', placeholder: 'نام کالا' },
{ key: 'consumable', label: 'مصرفی', placeholder: 'مصرفی' },
{ key: 'unit', label: 'واحد', placeholder: 'عدد' },
{ key: 'price', label: 'قیمت (تومان)', placeholder: 'قیمت', numeric: true },
{ key: 'stock', label: 'موجودی', placeholder: 'موجودی', numeric: true },
{ key: 'alertThreshold', label: 'هشدار اتمام', placeholder: 'هشدار اتمام', numeric: true },
];
return (
<Modal open={open} onClose={onClose} title={editing ? 'ویرایش کالا' : 'افزودن کالای جدید'} size="lg">
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
{fields.map((f) => (
<div key={f.key}>
<label className="field-label">{f.label}</label>
<div className="field">
<input
value={form[f.key]}
onChange={f.numeric ? setNum(f.key) : set(f.key)}
placeholder={f.placeholder}
inputMode={f.numeric ? 'numeric' : undefined}
autoFocus={f.key === 'name'}
/>
</div>
</div>
))}
{error && <span className="field-error">{error}</span>}
<button
className="btn primary"
style={{ width: '100%', justifyContent: 'center', height: 46 }}
onClick={submit}
disabled={saving}
>
{editing ? 'ذخیره تغییرات' : 'اضافه کردن کالا'}
</button>
</div>
</Modal>
);
}
@@ -0,0 +1,136 @@
import React, { useEffect, useMemo, useState } from 'react';
import { TrashIcon } from '@heroicons/react/24/outline';
import Modal from '../ui/Modal';
import { formatRial } from '../../lib/utils';
import type { InventoryItem, InventoryPackage, PackagePayload } from '../../hooks/useInventory';
interface Props {
open: boolean;
editing: InventoryPackage | null;
items: InventoryItem[]; // the tenant's available items to pick from
saving: boolean;
onClose: () => void;
onSave: (payload: PackagePayload, uuid?: string) => void;
}
interface Line { itemUuid: string; name: string; unit: string; price: number; amount: number }
/** «افزودن/ویرایش پکیج» — mirrors tauri ModalAddPackage (two-column builder). */
export default function AddPackageModal({ open, editing, items, saving, onClose, onSave }: Props) {
const [title, setTitle] = useState('');
const [pickUuid, setPickUuid] = useState('');
const [amount, setAmount] = useState('1');
const [lines, setLines] = useState<Line[]>([]);
const [error, setError] = useState('');
useEffect(() => {
if (!open) return;
setError('');
setPickUuid(items[0]?.uuid ?? '');
setAmount('1');
if (editing) {
setTitle(editing.title);
setLines(editing.items.map((l) => ({ itemUuid: l.itemUuid, name: l.name, unit: l.unit, price: l.price, amount: l.amount })));
} else {
setTitle('');
setLines([]);
}
}, [open, editing, items]);
const total = useMemo(() => lines.reduce((s, l) => s + l.price * l.amount, 0), [lines]);
const addLine = () => {
const item = items.find((i) => i.uuid === pickUuid);
if (!item) return;
const qty = Math.max(1, Number(amount) || 1);
setLines((prev) => [...prev, { itemUuid: item.uuid, name: item.name, unit: item.unit, price: item.price, amount: qty }]);
setAmount('1');
};
const changeAmount = (idx: number, delta: number) =>
setLines((prev) => prev.map((l, i) => (i === idx ? { ...l, amount: Math.max(1, l.amount + delta) } : l)));
const removeLine = (idx: number) => setLines((prev) => prev.filter((_, i) => i !== idx));
const submit = () => {
if (title.trim() === '') { setError('نام پکیج الزامی است'); return; }
onSave({ title: title.trim(), items: lines.map((l) => ({ itemUuid: l.itemUuid, amount: l.amount })) }, editing?.uuid);
};
const footer = (
<div style={{ display: 'flex', gap: 16, justifyContent: 'flex-end', width: '100%' }}>
<button className="btn ghost" style={{ height: 48, width: 160, justifyContent: 'center' }} onClick={onClose}>انصراف</button>
<button className="btn primary" style={{ height: 48, width: 160, justifyContent: 'center' }} onClick={submit} disabled={saving}>
{editing ? 'ذخیره تغییرات' : 'افزودن پکیج'}
</button>
</div>
);
return (
<Modal open={open} onClose={onClose} title={editing ? 'ویرایش پکیج' : 'افزودن پکیج'} size="xl" footer={footer}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 32 }}>
{/* Left — builder */}
<div style={{ flex: '1 1 300px', display: 'flex', flexDirection: 'column', gap: 20 }}>
<div>
<label className="field-label">نام پکیج</label>
<div className="field"><input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="نام پکیج" /></div>
</div>
<div>
<label className="field-label">اجزای پکیج</label>
<div className="field">
<select value={pickUuid} onChange={(e) => setPickUuid(e.target.value)} disabled={items.length === 0}>
{items.length === 0 && <option value="">ابتدا کالا اضافه کنید</option>}
{items.map((i) => <option key={i.uuid} value={i.uuid}>{i.name}</option>)}
</select>
</div>
</div>
<div>
<label className="field-label">مقدار</label>
<div className="field">
<input value={amount} inputMode="numeric" onChange={(e) => setAmount(e.target.value.replace(/[^0-9]/g, ''))} placeholder="1" />
</div>
</div>
<button className="btn ghost" style={{ justifyContent: 'center', height: 46 }} onClick={addLine} disabled={items.length === 0}>
افزودن کالا
</button>
</div>
{/* Right — selected items */}
<div
style={{
flex: '1 1 300px', minHeight: 320, borderRadius: 8, padding: 24,
border: '1px dashed #636ed0', display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
}}
>
<div>
<div style={{ fontSize: 16, fontWeight: 600, color: 'var(--text)', marginBottom: 16 }}>کالاهای انتخاب شده:</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{lines.length === 0 ? (
<span style={{ fontSize: 14, color: 'var(--text-3)' }}>هنوز کالایی به پکیج اضافه نشده است.</span>
) : (
lines.map((l, idx) => (
<div key={idx} style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<span style={{ flex: 1, minWidth: 0, fontSize: 14, fontWeight: 600, color: 'var(--text-2)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{l.name}</span>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: 97, height: 40, borderRadius: 4, border: '1px solid var(--border-2)', padding: '0 8px' }}>
<button type="button" onClick={() => changeAmount(idx, 1)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#f0753b', fontSize: 18, fontWeight: 600 }}>+</button>
<span style={{ color: '#f0753b', fontSize: 14, fontWeight: 500 }}>{l.amount}</span>
<button type="button" onClick={() => changeAmount(idx, -1)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#f0753b', fontSize: 18, fontWeight: 600 }}>-</button>
</div>
<span style={{ fontSize: 14, color: 'var(--text-2)', whiteSpace: 'nowrap' }}>{formatRial(l.price)}</span>
<button type="button" onClick={() => removeLine(idx)} aria-label="حذف" style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--danger)' }}>
<TrashIcon style={{ width: 18 }} />
</button>
</div>
))
)}
</div>
</div>
<div style={{ marginTop: 24, paddingTop: 16, borderTop: '1px solid var(--border)' }}>
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>قیمت کل پکیج: {formatRial(total)}</span>
</div>
</div>
</div>
{error && <span className="field-error">{error}</span>}
</div>
</Modal>
);
}
@@ -0,0 +1,75 @@
import React, { useState } from 'react';
import { EllipsisHorizontalCircleIcon, PencilSquareIcon, TrashIcon } from '@heroicons/react/24/outline';
import type { InventoryItem } from '../../hooks/useInventory';
interface Props {
item: InventoryItem;
onEdit: (item: InventoryItem) => void;
onDelete: (item: InventoryItem) => void;
}
/** «عملیات» trigger + dropdown — mirrors the tauri InventoryActionsPopover. */
export default function InventoryActionsMenu({ item, onEdit, onDelete }: Props) {
const [open, setOpen] = useState(false);
return (
<div style={{ position: 'relative', display: 'inline-block' }}>
<button
type="button"
onClick={() => setOpen((v) => !v)}
style={{
display: 'flex', alignItems: 'center', gap: 4, cursor: 'pointer',
borderRadius: 6, padding: '4px 8px', background: 'none',
color: '#5559ce', fontSize: 12, fontWeight: 500,
}}
>
<EllipsisHorizontalCircleIcon style={{ width: 18, height: 18 }} />
عملیات
</button>
{open && (
<>
<div
onClick={() => setOpen(false)}
style={{ position: 'fixed', inset: 0, zIndex: 30 }}
/>
<div
role="menu"
style={{
position: 'absolute', top: '100%', insetInlineStart: 0, marginTop: 4, zIndex: 31,
width: 160, background: 'var(--surface)', border: '1px solid var(--border)',
borderRadius: 12, boxShadow: 'var(--shadow-lg)', overflow: 'hidden',
}}
>
<button
type="button"
role="menuitem"
onClick={() => { setOpen(false); onEdit(item); }}
style={{
display: 'flex', alignItems: 'center', gap: 8, width: '100%',
padding: '12px 16px', background: 'var(--primary-soft)', border: 'none',
cursor: 'pointer', fontSize: 14, fontWeight: 600, color: 'var(--text)',
}}
>
<PencilSquareIcon style={{ width: 20, height: 20 }} />
ویرایش
</button>
<button
type="button"
role="menuitem"
onClick={() => { setOpen(false); onDelete(item); }}
style={{
display: 'flex', alignItems: 'center', gap: 8, width: '100%',
padding: '12px 16px', background: 'transparent', border: 'none',
cursor: 'pointer', fontSize: 14, color: 'var(--danger)',
}}
>
<TrashIcon style={{ width: 20, height: 20 }} />
حذف
</button>
</div>
</>
)}
</div>
);
}
@@ -0,0 +1,89 @@
import React from 'react';
import { PencilSquareIcon, TrashIcon } from '@heroicons/react/24/outline';
import { formatRial, formatNumber } from '../../lib/utils';
import type { InventoryItem } from '../../hooks/useInventory';
import InventoryStatusBadge from './InventoryStatusBadge';
import InventoryActionsMenu from './InventoryActionsMenu';
interface Props {
items: InventoryItem[];
onEdit: (item: InventoryItem) => void;
onDelete: (item: InventoryItem) => void;
}
const HEAD = ['نام کالا', 'موجودی', 'واحد', 'قیمت', 'وضعیت', 'عملیات'];
/** Consumable-items list: desktop table + mobile card grid (tauri InventoryList). */
export default function InventoryItemsTable({ items, onEdit, onDelete }: Props) {
return (
<div style={{ width: '100%' }}>
{/* Desktop table */}
<div
className="inv-desktop"
style={{ border: '1px solid #E7E7E7', borderRadius: 8, overflow: 'hidden' }}
>
<table className="inv-table">
<thead>
<tr>{HEAD.map((h) => <th key={h}>{h}</th>)}</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.uuid}>
<td style={{ color: 'var(--text-2)' }}>{item.name}</td>
<td>{formatNumber(item.stock)}</td>
<td>{item.unit}</td>
<td>{formatRial(item.price)}</td>
<td><InventoryStatusBadge status={item.status} /></td>
<td><InventoryActionsMenu item={item} onEdit={onEdit} onDelete={onDelete} /></td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile cards */}
<ul
className="inv-mobile"
style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))', gap: 16, margin: 0, padding: 0, listStyle: 'none' }}
>
{items.map((item) => (
<li
key={item.uuid}
style={{
padding: 12, background: 'var(--surface)', border: '1px solid var(--border)',
borderRadius: 8, boxShadow: 'var(--shadow-sm)',
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
<span style={{ color: 'var(--text)', fontSize: 14, fontWeight: 600 }}>{item.name}</span>
<InventoryStatusBadge status={item.status} />
</div>
{[
['موجودی:', formatNumber(item.stock)],
['واحد:', item.unit],
['قیمت:', formatRial(item.price)],
].map(([label, value], i, arr) => (
<div key={label}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
<span style={{ color: 'var(--text-3)', fontSize: 14 }}>{label}</span>
<span style={{ color: 'var(--text-2)', fontSize: 14, fontWeight: 500 }}>{value}</span>
</div>
{i < arr.length - 1 && (
<div style={{ height: 1, background: 'var(--border)', margin: '8px auto', width: 'calc(100% - 20px)' }} />
)}
</div>
))}
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 12 }}>
<button className="btn sm ghost" aria-label="ویرایش" onClick={() => onEdit(item)} style={{ color: 'var(--text-2)' }}>
<PencilSquareIcon style={{ width: 16 }} />
</button>
<button className="btn sm ghost" aria-label="حذف" onClick={() => onDelete(item)} style={{ color: 'var(--danger)' }}>
<TrashIcon style={{ width: 16 }} />
</button>
</div>
</li>
))}
</ul>
</div>
);
}
@@ -0,0 +1,26 @@
import React from 'react';
import { ArchiveBoxIcon } from '@heroicons/react/24/outline';
import StatCard from '../ui/StatCard';
import { formatNumber } from '../../lib/utils';
import type { InventoryStats } from '../../hooks/useInventory';
const ICON = <ArchiveBoxIcon style={{ width: 20, height: 20 }} />;
/** Four headline counters — order/colors mirror the tauri InventoryCards. */
export default function InventoryStatCards({ stats }: { stats: InventoryStats }) {
return (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
gap: 20,
padding: '20px 0',
}}
>
<StatCard tone="violet" icon={ICON} label="تعداد کل کالاها" value={formatNumber(stats.total)} />
<StatCard tone="amber" icon={ICON} label="کالاهای کم موجود" value={formatNumber(stats.low)} />
<StatCard tone="green" icon={ICON} label="کالاهای موجود" value={formatNumber(stats.inStock)} />
<StatCard tone="pink" icon={ICON} label="کالاهای اتمام یافته" value={formatNumber(stats.outOfStock)} />
</div>
);
}
@@ -0,0 +1,14 @@
import React from 'react';
import type { InventoryStatus } from '../../hooks/useInventory';
const CONFIG: Record<InventoryStatus, { label: string; cls: string }> = {
in_stock: { label: 'موجود', cls: 'in-stock' },
low_stock: { label: 'کم موجود', cls: 'low-stock' },
out_of_stock: { label: 'اتمام یافته', cls: 'out-of-stock' },
};
/** Colored availability pill — colors mirror the tauri InventoryStatus component. */
export default function InventoryStatusBadge({ status }: { status: InventoryStatus }) {
const c = CONFIG[status] ?? CONFIG.out_of_stock;
return <span className={`inv-badge ${c.cls}`}>{c.label}</span>;
}
@@ -0,0 +1,112 @@
import React, { useState } from 'react';
import { ChevronDownIcon, PencilSquareIcon, TrashIcon } from '@heroicons/react/24/outline';
import { formatRial } from '../../lib/utils';
import type { InventoryPackage } from '../../hooks/useInventory';
interface Props {
packages: InventoryPackage[];
onEdit: (pkg: InventoryPackage) => void;
onDelete: (pkg: InventoryPackage) => void;
}
/** Package cards with an expandable component list — mirrors tauri AddStockView. */
export default function PackagesView({ packages, onEdit, onDelete }: Props) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{packages.map((pkg) => (
<PackageCard key={pkg.uuid} pkg={pkg} onEdit={onEdit} onDelete={onDelete} />
))}
</div>
);
}
function PackageCard({ pkg, onEdit, onDelete }: { pkg: InventoryPackage } & Pick<Props, 'onEdit' | 'onDelete'>) {
const [expanded, setExpanded] = useState(false);
const divider = <div style={{ height: 1, background: '#d7d7d7', margin: '8px 0' }} className="inv-divider" />;
return (
<div style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 8, padding: 16 }}>
{/* Header */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ width: 20, height: 20, background: '#f0753b', borderRadius: '50%', flexShrink: 0 }} />
<span style={{ fontWeight: 500, fontSize: 16, color: 'var(--text)' }}>{pkg.title}</span>
</div>
<span
style={{
width: 122, height: 34, display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
borderRadius: 4, fontWeight: 600, fontSize: 16,
color: pkg.available ? '#3f9954' : '#d32f2f',
background: pkg.available ? '#f0faf2' : '#fdebed',
}}
>
{pkg.available ? 'موجودی کافی' : 'موجودی ناکافی'}
</span>
</div>
{divider}
{/* Accordion */}
<button
type="button"
onClick={() => setExpanded((v) => !v)}
style={{
display: 'flex', alignItems: 'center', gap: 12, width: '100%', padding: '8px 0',
background: 'none', border: 'none', cursor: 'pointer',
}}
>
<ChevronDownIcon
style={{ width: 20, height: 20, color: 'var(--text-2)', transition: 'transform .2s', transform: expanded ? 'rotate(180deg)' : 'none' }}
/>
<span style={{ fontWeight: 500, fontSize: 14, color: 'var(--text)' }}>اجزای پکیج:</span>
</button>
{expanded && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 40, padding: '4px 0 16px' }}>
{pkg.items.length === 0 ? (
<span style={{ fontSize: 14, color: 'var(--text-3)' }}>این پکیج کالایی ندارد.</span>
) : (
pkg.items.map((it) => (
<div key={it.itemUuid} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
<span style={{ color: 'var(--text)', fontWeight: 700, fontSize: 16 }}>{it.name}</span>
<span
style={{
height: 24, display: 'inline-flex', alignItems: 'center', padding: '0 10px',
background: 'var(--surface-3)', color: 'var(--text-2)', borderRadius: 6, fontWeight: 500, fontSize: 14,
}}
>
{it.amount} {it.unit}
</span>
</div>
))
)}
</div>
)}
{divider}
{/* Footer */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontWeight: 700, fontSize: 14, color: 'var(--text)' }}>
قیمت پکیج: {formatRial(pkg.total)}
</span>
<div style={{ display: 'flex', gap: 8 }}>
<button
className="btn sm ghost"
onClick={() => onDelete(pkg)}
style={{ height: 40, width: 78, border: '1px solid var(--border)', color: 'var(--danger)', gap: 6 }}
>
<TrashIcon style={{ width: 16 }} /> حذف
</button>
<button
className="btn sm ghost"
onClick={() => onEdit(pkg)}
style={{ height: 40, width: 78, border: '1px solid var(--border)', color: 'var(--text-2)', gap: 6 }}
>
<PencilSquareIcon style={{ width: 16 }} /> ویرایش
</button>
</div>
</div>
</div>
);
}
@@ -1,4 +1,5 @@
import {
ArchiveBoxIcon,
ArrowLeftOnRectangleIcon,
ArrowsRightLeftIcon,
BanknotesIcon,
@@ -222,6 +223,11 @@ function buildSections(
label: "مطالبات بیمه",
feature: "insurance",
},
{
to: "/admin/inventory",
icon: ArchiveBoxIcon,
label: "انبارداری",
},
],
},
{
@@ -286,6 +292,11 @@ function buildSections(
label: "مطالبات بیمه",
feature: "insurance",
},
{
to: "/admin/inventory",
icon: ArchiveBoxIcon,
label: "انبارداری",
},
],
},
{