184 lines
7.1 KiB
TypeScript
184 lines
7.1 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react'
|
|
import { Link, useSearchParams } from 'react-router-dom'
|
|
import { ApiHttpError } from '../../api/http'
|
|
import { fetchInventoryDashboard } from '../../api/inventory'
|
|
import { INVENTORY_BOOK_BASE } from './inventoryPaths'
|
|
|
|
/**
|
|
* Inventory Dashboard — summary counts by type, low stock, out of stock, etc.
|
|
* Backend: GET /api/v1/administrator/inventory/dashboard
|
|
*/
|
|
export function InventoryDashboardPage() {
|
|
const [searchParams] = useSearchParams()
|
|
const [data, setData] = useState<Record<string, unknown>>({})
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
const load = useCallback(() => {
|
|
setLoading(true)
|
|
fetchInventoryDashboard(searchParams)
|
|
.then((res) => {
|
|
if (res && typeof res === 'object') {
|
|
setData(res as Record<string, unknown>)
|
|
}
|
|
setError(null)
|
|
})
|
|
.catch((e: unknown) => {
|
|
setError(e instanceof ApiHttpError ? e.message : 'Unable to load dashboard.')
|
|
})
|
|
.finally(() => setLoading(false))
|
|
}, [searchParams])
|
|
|
|
useEffect(() => {
|
|
load()
|
|
}, [load])
|
|
|
|
const byType = (data.by_type as Record<string, number>) ?? {}
|
|
const totalItems = Number(data.total_items ?? 0)
|
|
const lowStockCount = Number(data.low_stock_count ?? 0)
|
|
const outOfStockCount = Number(data.out_of_stock_count ?? 0)
|
|
const needsRepairCount = Number(data.needs_repair_count ?? 0)
|
|
const missingCount = Number(data.missing_count ?? 0)
|
|
|
|
return (
|
|
<div className="container-fluid px-0 mt-3">
|
|
<div className="d-flex justify-content-between align-items-center mb-3 px-3 flex-wrap gap-2">
|
|
<h3 className="mb-0">Inventory Dashboard</h3>
|
|
<div className="d-flex gap-2">
|
|
<Link className="btn btn-outline-info btn-sm" to="/app/administrator/inventory/stock-status">
|
|
Stock Status
|
|
</Link>
|
|
<Link className="btn btn-outline-secondary" to={INVENTORY_BOOK_BASE}>
|
|
Back to Items
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{error ? <div className="alert alert-danger mx-3">{error}</div> : null}
|
|
|
|
{loading ? (
|
|
<p className="text-muted px-3">Loading…</p>
|
|
) : (
|
|
<div className="row g-3 px-3">
|
|
{/* Summary Cards */}
|
|
<div className="col-sm-6 col-lg-4">
|
|
<div className="card border-primary h-100">
|
|
<div className="card-body text-center">
|
|
<h5 className="card-title text-primary">{totalItems}</h5>
|
|
<p className="card-text">Total Items</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="col-sm-6 col-lg-4">
|
|
<div className="card border-warning h-100">
|
|
<div className="card-body text-center">
|
|
<h5 className="card-title text-warning">{lowStockCount}</h5>
|
|
<p className="card-text">Low Stock Items</p>
|
|
{lowStockCount > 0 ? (
|
|
<Link className="btn btn-sm btn-warning" to="/app/administrator/inventory/low-stock">
|
|
View Low Stock
|
|
</Link>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="col-sm-6 col-lg-4">
|
|
<div className="card border-danger h-100">
|
|
<div className="card-body text-center">
|
|
<h5 className="card-title text-danger">{outOfStockCount}</h5>
|
|
<p className="card-text">Out of Stock</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="col-sm-6 col-lg-4">
|
|
<div className="card border-info h-100">
|
|
<div className="card-body text-center">
|
|
<h5 className="card-title text-info">{needsRepairCount}</h5>
|
|
<p className="card-text">Needs Repair</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="col-sm-6 col-lg-4">
|
|
<div className="card border-secondary h-100">
|
|
<div className="card-body text-center">
|
|
<h5 className="card-title text-secondary">{missingCount}</h5>
|
|
<p className="card-text">Missing / Cannot Find</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Items by Type */}
|
|
<div className="col-12">
|
|
<div className="card">
|
|
<div className="card-header">Items by Type</div>
|
|
<div className="card-body">
|
|
{Object.keys(byType).length === 0 ? (
|
|
<p className="text-muted mb-0">No items found.</p>
|
|
) : (
|
|
<div className="table-responsive">
|
|
<table className="table table-striped align-middle mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>Type</th>
|
|
<th className="text-end">Count</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{Object.entries(byType).map(([type, count]) => (
|
|
<tr key={type}>
|
|
<td style={{ textTransform: 'capitalize' }}>{type}</td>
|
|
<td className="text-end">{Number(count).toLocaleString()}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Links */}
|
|
<div className="col-12">
|
|
<div className="card">
|
|
<div className="card-header">Quick Actions</div>
|
|
<div className="card-body">
|
|
<div className="d-flex gap-2 flex-wrap">
|
|
<Link className="btn btn-outline-primary" to="/app/administrator/inventory/classroom">
|
|
Classroom Equipment
|
|
</Link>
|
|
<Link className="btn btn-outline-primary" to={INVENTORY_BOOK_BASE}>
|
|
Books
|
|
</Link>
|
|
<Link className="btn btn-outline-primary" to="/app/administrator/inventory/office">
|
|
Office Supplies
|
|
</Link>
|
|
<Link className="btn btn-outline-primary" to="/app/administrator/inventory/kitchen">
|
|
Kitchen Supplies
|
|
</Link>
|
|
<Link className="btn btn-outline-primary" to="/app/administrator/inventory/movements">
|
|
Movements
|
|
</Link>
|
|
<Link className="btn btn-outline-primary" to="/app/administrator/inventory/summary">
|
|
Summary
|
|
</Link>
|
|
<Link className="btn btn-outline-primary" to="/app/administrator/inventory/reorder-suggestions">
|
|
Reorder Suggestions
|
|
</Link>
|
|
<Link className="btn btn-outline-primary" to="/app/administrator/inventory/low-stock">
|
|
Low Stock
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|