fix inventory
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Link, useSearchParams } from 'react-router-dom'
|
||||
import { ApiHttpError } from '../../api/http'
|
||||
import { fetchStockStatus } from '../../api/inventory'
|
||||
import { INVENTORY_BOOK_BASE } from './inventoryPaths'
|
||||
|
||||
/**
|
||||
* Stock Status — all items with status: ok/low_stock/out_of_stock.
|
||||
* Backend: GET /api/v1/administrator/inventory/stock-status
|
||||
*/
|
||||
export function InventoryStockStatusPage() {
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const [items, setItems] = useState<Record<string, unknown>[]>([])
|
||||
const [counts, setCounts] = useState<Record<string, number>>({})
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const load = useCallback(() => {
|
||||
setLoading(true)
|
||||
fetchStockStatus(searchParams)
|
||||
.then((res) => {
|
||||
if (res && typeof res === 'object') {
|
||||
const o = res as Record<string, unknown>
|
||||
setItems(Array.isArray(o.items) ? (o.items as Record<string, unknown>[]) : [])
|
||||
setCounts((o.counts as Record<string, number>) ?? {})
|
||||
}
|
||||
setError(null)
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
setError(e instanceof ApiHttpError ? e.message : 'Unable to load stock status.')
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}, [searchParams])
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [load])
|
||||
|
||||
function onTypeFilter(v: string) {
|
||||
const next = new URLSearchParams(searchParams)
|
||||
if (v) next.set('type', v)
|
||||
else next.delete('type')
|
||||
setSearchParams(next)
|
||||
}
|
||||
|
||||
const currentType = searchParams.get('type') ?? ''
|
||||
|
||||
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">Stock Status</h3>
|
||||
<div className="d-flex gap-2">
|
||||
<Link className="btn btn-outline-warning btn-sm" to="/app/administrator/inventory/low-stock">
|
||||
Low Stock
|
||||
</Link>
|
||||
<Link className="btn btn-outline-info btn-sm" to="/app/administrator/inventory/reorder-suggestions">
|
||||
Reorder Suggestions
|
||||
</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>
|
||||
) : (
|
||||
<>
|
||||
{/* Summary Badges */}
|
||||
<div className="d-flex gap-3 mb-3 px-3 flex-wrap">
|
||||
<span className="badge bg-success fs-6">
|
||||
OK: {counts.ok ?? 0}
|
||||
</span>
|
||||
<span className="badge bg-warning fs-6">
|
||||
Low Stock: {counts.low_stock ?? 0}
|
||||
</span>
|
||||
<span className="badge bg-danger fs-6">
|
||||
Out of Stock: {counts.out_of_stock ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="card mx-3">
|
||||
<div className="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<span>All Items</span>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
style={{ minWidth: 180 }}
|
||||
value={currentType}
|
||||
onChange={(e) => onTypeFilter(e.target.value)}
|
||||
>
|
||||
<option value="">All Types</option>
|
||||
<option value="book">Books</option>
|
||||
<option value="classroom">Classroom</option>
|
||||
<option value="office">Office</option>
|
||||
<option value="kitchen">Kitchen</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="card-body table-responsive">
|
||||
<table className="table table-striped align-middle" id="stockStatusTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Category</th>
|
||||
<th className="text-end">Quantity</th>
|
||||
<th className="text-end">Reorder Point</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="text-center text-muted py-4">
|
||||
No items found.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
items.map((item) => {
|
||||
const status = String(item.status ?? 'ok')
|
||||
const badgeClass =
|
||||
status === 'out_of_stock'
|
||||
? 'bg-danger'
|
||||
: status === 'low_stock'
|
||||
? 'bg-warning'
|
||||
: 'bg-success'
|
||||
const badgeLabel =
|
||||
status === 'out_of_stock'
|
||||
? 'Out of Stock'
|
||||
: status === 'low_stock'
|
||||
? 'Low Stock'
|
||||
: 'OK'
|
||||
return (
|
||||
<tr key={String(item.id)}>
|
||||
<td>{String(item.name ?? '')}</td>
|
||||
<td style={{ textTransform: 'capitalize' }}>{String(item.type ?? '')}</td>
|
||||
<td>{String(item.category ?? '—')}</td>
|
||||
<td className="text-end">{Number(item.quantity ?? 0).toLocaleString()}</td>
|
||||
<td className="text-end">
|
||||
{item.reorder_point != null ? Number(item.reorder_point).toLocaleString() : '—'}
|
||||
</td>
|
||||
<td>
|
||||
<span className={`badge ${badgeClass}`}>{badgeLabel}</span>
|
||||
</td>
|
||||
<td>
|
||||
<Link
|
||||
className="btn btn-sm btn-outline-primary me-1"
|
||||
to={`/app/administrator/inventory/items/${String(item.id)}/adjust`}
|
||||
>
|
||||
Adjust
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user