fix inventory
This commit is contained in:
+60
-3
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
import { apiFetch } from './http'
|
||||
|
||||
const BASE = '/api/v1/administrator/inventory'
|
||||
const BASE = '/api/v1/inventory'
|
||||
|
||||
function q(searchParams: URLSearchParams): string {
|
||||
const s = searchParams.toString()
|
||||
@@ -18,7 +18,9 @@ export async function fetchInventoryIndex(
|
||||
kind: InventoryKind,
|
||||
searchParams: URLSearchParams,
|
||||
): Promise<unknown> {
|
||||
return apiFetch(`${BASE}/${kind}${q(searchParams)}`)
|
||||
const params = new URLSearchParams(searchParams)
|
||||
params.set('type', kind)
|
||||
return apiFetch(`${BASE}/items?${params.toString()}`)
|
||||
}
|
||||
|
||||
export async function postInventoryCategory(body: Record<string, unknown>): Promise<unknown> {
|
||||
@@ -92,7 +94,11 @@ export async function postClassroomAudit(
|
||||
}
|
||||
|
||||
export async function fetchInventorySummary(searchParams: URLSearchParams): Promise<unknown> {
|
||||
return apiFetch(`${BASE}/summary${q(searchParams)}`)
|
||||
const type = searchParams.get('type') || 'classroom'
|
||||
const params = new URLSearchParams(searchParams)
|
||||
params.delete('type')
|
||||
const qs = params.toString()
|
||||
return apiFetch(`${BASE}/summary/${type}${qs ? `?${qs}` : ''}`)
|
||||
}
|
||||
|
||||
export async function fetchMovements(searchParams: URLSearchParams): Promise<unknown> {
|
||||
@@ -156,3 +162,54 @@ export async function postTeacherBookDistribute(body: Record<string, unknown>):
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
}
|
||||
|
||||
/* ─── New Inventory Improvement API Calls ─── */
|
||||
|
||||
/** Fetch low-stock items (quantity <= reorder_point). */
|
||||
export async function fetchLowStock(): Promise<unknown> {
|
||||
return apiFetch(`${BASE}/low-stock`)
|
||||
}
|
||||
|
||||
/** Fetch reorder suggestions for low-stock items. */
|
||||
export async function fetchReorderSuggestions(): Promise<unknown> {
|
||||
return apiFetch(`${BASE}/reorder-suggestions`)
|
||||
}
|
||||
|
||||
/** Fetch stock status for all items (ok/low_stock/out_of_stock). */
|
||||
export async function fetchStockStatus(
|
||||
searchParams: URLSearchParams,
|
||||
): Promise<unknown> {
|
||||
return apiFetch(`${BASE}/stock-status${q(searchParams)}`)
|
||||
}
|
||||
|
||||
/** Fetch inventory dashboard summary counts. */
|
||||
export async function fetchInventoryDashboard(
|
||||
searchParams: URLSearchParams,
|
||||
): Promise<unknown> {
|
||||
return apiFetch(`${BASE}/dashboard${q(searchParams)}`)
|
||||
}
|
||||
|
||||
/** Void a movement (append-only correction with reverse entry). */
|
||||
export async function voidMovement(
|
||||
movementId: string | number,
|
||||
reason: string,
|
||||
): Promise<unknown> {
|
||||
return apiFetch(`${BASE}/movements/${movementId}/void`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ reason }),
|
||||
})
|
||||
}
|
||||
|
||||
/** Create a correction movement linked to an existing movement. */
|
||||
export async function correctMovement(
|
||||
movementId: string | number,
|
||||
qtyChange: number,
|
||||
reason: string,
|
||||
): Promise<unknown> {
|
||||
return apiFetch(`${BASE}/movements/${movementId}/correct`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ qty_change: qtyChange, reason }),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
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/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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ApiHttpError } from '../../api/http'
|
||||
import { fetchLowStock } from '../../api/inventory'
|
||||
import { INVENTORY_BOOK_BASE } from './inventoryPaths'
|
||||
|
||||
/**
|
||||
* Low Stock Items — items where quantity <= reorder_point.
|
||||
* Backend: GET /api/v1/inventory/low-stock
|
||||
*/
|
||||
export function InventoryLowStockPage() {
|
||||
const [items, setItems] = useState<Record<string, unknown>[]>([])
|
||||
const [count, setCount] = useState(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const load = useCallback(() => {
|
||||
setLoading(true)
|
||||
fetchLowStock()
|
||||
.then((res) => {
|
||||
if (res && typeof res === 'object') {
|
||||
const o = res as Record<string, unknown>
|
||||
const arr = Array.isArray(o.items) ? (o.items as Record<string, unknown>[]) : []
|
||||
setItems(arr)
|
||||
setCount(Number(o.count ?? arr.length))
|
||||
}
|
||||
setError(null)
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
setError(e instanceof ApiHttpError ? e.message : 'Unable to load low stock items.')
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [load])
|
||||
|
||||
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">Low Stock Items ({count})</h3>
|
||||
<div className="d-flex gap-2">
|
||||
<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>
|
||||
) : items.length === 0 ? (
|
||||
<div className="alert alert-success mx-3">
|
||||
<strong>All stocked up!</strong> No items are below their reorder point.
|
||||
</div>
|
||||
) : (
|
||||
<div className="card mx-3">
|
||||
<div className="card-body table-responsive">
|
||||
<table className="table table-striped align-middle" id="lowStockTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Category</th>
|
||||
<th className="text-end">Current Qty</th>
|
||||
<th className="text-end">Reorder Point</th>
|
||||
<th className="text-end">Reorder Qty</th>
|
||||
<th className="text-end">Lead Time (Days)</th>
|
||||
<th>Preferred Supplier</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item) => {
|
||||
const i = item as Record<string, unknown>
|
||||
const supplier = i.preferred_supplier as Record<string, unknown> | null
|
||||
return (
|
||||
<tr key={String(i.id)}>
|
||||
<td>{String(i.name ?? '')}</td>
|
||||
<td style={{ textTransform: 'capitalize' }}>{String(i.type ?? '')}</td>
|
||||
<td>{String((i.category as Record<string, unknown>)?.name ?? i.category ?? '')}</td>
|
||||
<td className="text-end text-danger fw-semibold">
|
||||
{Number(i.quantity ?? 0).toLocaleString()}
|
||||
</td>
|
||||
<td className="text-end">{Number(i.reorder_point ?? 0).toLocaleString()}</td>
|
||||
<td className="text-end">{Number(i.reorder_quantity ?? 0).toLocaleString() || '—'}</td>
|
||||
<td className="text-end">{Number(i.lead_time_days ?? 0) || '—'}</td>
|
||||
<td>{supplier ? String(supplier.name ?? '') : '—'}</td>
|
||||
<td>
|
||||
<Link
|
||||
className="btn btn-sm btn-warning"
|
||||
to={`/app/administrator/inventory/items/${String(i.id)}/adjust`}
|
||||
>
|
||||
Adjust Stock
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ApiHttpError } from '../../api/http'
|
||||
import { fetchReorderSuggestions } from '../../api/inventory'
|
||||
import { INVENTORY_BOOK_BASE } from './inventoryPaths'
|
||||
|
||||
/**
|
||||
* Reorder Suggestions — calculated suggested order quantities for low-stock items.
|
||||
* Backend: GET /api/v1/inventory/reorder-suggestions
|
||||
*/
|
||||
export function InventoryReorderSuggestionsPage() {
|
||||
const [suggestions, setSuggestions] = useState<Record<string, unknown>[]>([])
|
||||
const [count, setCount] = useState(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const load = useCallback(() => {
|
||||
setLoading(true)
|
||||
fetchReorderSuggestions()
|
||||
.then((res) => {
|
||||
if (res && typeof res === 'object') {
|
||||
const o = res as Record<string, unknown>
|
||||
const arr = Array.isArray(o.suggestions) ? (o.suggestions as Record<string, unknown>[]) : []
|
||||
setSuggestions(arr)
|
||||
setCount(Number(o.count ?? arr.length))
|
||||
}
|
||||
setError(null)
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
setError(e instanceof ApiHttpError ? e.message : 'Unable to load reorder suggestions.')
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [load])
|
||||
|
||||
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">Reorder Suggestions ({count})</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-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>
|
||||
) : suggestions.length === 0 ? (
|
||||
<div className="alert alert-success mx-3">
|
||||
<strong>All stocked up!</strong> No reorder suggestions at this time.
|
||||
</div>
|
||||
) : (
|
||||
<div className="card mx-3">
|
||||
<div className="card-body table-responsive">
|
||||
<table className="table table-striped align-middle" id="reorderSuggestionsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Category</th>
|
||||
<th className="text-end">Current Qty</th>
|
||||
<th className="text-end">Reorder Point</th>
|
||||
<th className="text-end">Suggested Order</th>
|
||||
<th>Preferred Supplier</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{suggestions.map((s) => {
|
||||
const supplier = s.preferred_supplier as Record<string, unknown> | null
|
||||
return (
|
||||
<tr key={String(s.id)}>
|
||||
<td>{String(s.name ?? '')}</td>
|
||||
<td>{String(s.category ?? '—')}</td>
|
||||
<td className="text-end text-danger fw-semibold">
|
||||
{Number(s.current_quantity ?? 0).toLocaleString()}
|
||||
</td>
|
||||
<td className="text-end">{Number(s.reorder_point ?? 0).toLocaleString()}</td>
|
||||
<td className="text-end fw-semibold">
|
||||
{Number(s.suggested_order_qty ?? 0).toLocaleString()}
|
||||
</td>
|
||||
<td>{supplier ? String(supplier.name ?? '') : '—'}</td>
|
||||
<td>
|
||||
<Link
|
||||
className="btn btn-sm btn-warning"
|
||||
to={`/app/administrator/inventory/items/${String(s.id)}/adjust`}
|
||||
>
|
||||
Adjust Stock
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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/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>
|
||||
)
|
||||
}
|
||||
@@ -5,12 +5,16 @@ export { InventoryBookIndexPage } from './InventoryBookIndexPage'
|
||||
export { InventoryClassroomAuditPage } from './InventoryClassroomAuditPage'
|
||||
export { InventoryClassroomFormPage } from './InventoryClassroomFormPage'
|
||||
export { InventoryClassroomIndexPage } from './InventoryClassroomIndexPage'
|
||||
export { InventoryDashboardPage } from './InventoryDashboardPage'
|
||||
export { InventoryEditEntryPage } from './InventoryEditEntryPage'
|
||||
export { InventoryKitchenFormPage } from './InventoryKitchenFormPage'
|
||||
export { InventoryKitchenIndexPage } from './InventoryKitchenIndexPage'
|
||||
export { InventoryLowStockPage } from './InventoryLowStockPage'
|
||||
export { InventoryMovementFormPage } from './InventoryMovementFormPage'
|
||||
export { InventoryMovementsIndexPage } from './InventoryMovementsIndexPage'
|
||||
export { InventoryOfficeFormPage } from './InventoryOfficeFormPage'
|
||||
export { InventoryOfficeIndexPage } from './InventoryOfficeIndexPage'
|
||||
export { InventoryReorderSuggestionsPage } from './InventoryReorderSuggestionsPage'
|
||||
export { InventoryStockStatusPage } from './InventoryStockStatusPage'
|
||||
export { InventorySummaryPage } from './InventorySummaryPage'
|
||||
export { TeacherBookDistributePage } from './TeacherBookDistributePage'
|
||||
|
||||
Reference in New Issue
Block a user