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
+53
View File
@@ -852,3 +852,56 @@ html, body { max-width: 100%; overflow-x: hidden; }
@media (max-width: 560px) {
.settings-grid { grid-template-columns: 1fr; }
}
/* ── Inventory (انبارداری) ─────────────────────────────────────────────────
Status pill colors mirror clinic-pro-tauri src/components/inventory/list/
InventoryStatus.jsx exactly, in both light and dark. */
.inv-badge {
display: inline-block; width: 100px; text-align: center;
padding: 4px 8px; border-radius: 4px; font-size: 14px; font-weight: 500;
}
.inv-badge.in-stock { background: #E8FADD; color: #3c9a4f; }
.inv-badge.low-stock { background: #FFF3DD; color: #FDBA35; }
.inv-badge.out-of-stock { background: #FFE3E2; color: #FF5450; }
.dark .inv-badge.in-stock { background: #324A32; color: #75E22E; }
.dark .inv-badge.low-stock { background: #4E4234; color: #FDBA35; }
.dark .inv-badge.out-of-stock { background: rgba(255,84,80,0.20); color: #FF5450; }
/* Tab row — mirrors inventory/TabsRow + TabItem. */
.inv-tabs {
display: flex; align-items: flex-end; gap: 24px;
border-bottom: 1px solid #EDEDED; margin: 4px 0 18px; width: 100%;
overflow-x: auto; scrollbar-width: none;
}
.inv-tabs::-webkit-scrollbar { display: none; }
.dark .inv-tabs { border-color: #35343D; }
.inv-tab {
display: flex; align-items: center; gap: 6px; padding: 0 8px 6px;
font-size: 16px; cursor: pointer; white-space: nowrap;
border-bottom: 2px solid transparent; color: #6B7280; background: none;
}
.dark .inv-tab { color: #A1A1A1; }
.inv-tab.active { border-bottom: 3px solid #5559ce; color: #5559ce; }
.dark .inv-tab.active { color: #5559ce; }
/* Inventory responsive table/card switch (md breakpoint = 768px, like tauri). */
.inv-desktop { display: block; }
.inv-mobile { display: none; }
@media (max-width: 767px) {
.inv-desktop { display: none; }
.inv-mobile { display: grid; }
}
.inv-table { width: 100%; border-collapse: collapse; }
.inv-table thead tr { background: #e1e1e1; }
.inv-table th {
color: #616161; font-size: 14px; font-weight: 400;
padding: 10px 18px; text-align: start; white-space: nowrap;
}
.inv-table td {
padding: 12px 18px; text-align: start; white-space: nowrap;
font-size: 16px; font-weight: 500; color: var(--text-2);
border-bottom: 1px solid #DBDBDB;
}
.dark .inv-table td { border-color: var(--border); }
.inv-table tbody tr:hover { background: #f4f5fd; }
.dark .inv-table tbody tr:hover { background: var(--surface-2); }