2import { useEffect, useState } from "react";
3import { Icon } from '@/contexts/IconContext';
4import { apiClient } from "@/lib/client/apiClient";
12 partcode: string | null;
20export default function StockLevelsPage() {
21 const [stockLevels, setStockLevels] = useState<StockLevel[]>([]);
22 const [locations, setLocations] = useState<Location[]>([]);
23 const [selectedLocation, setSelectedLocation] = useState<string>("all");
24 const [includeZero, setIncludeZero] = useState<boolean>(false);
25 const [loading, setLoading] = useState(false);
26 const [error, setError] = useState<string | null>(null);
27 const [sortColumn, setSortColumn] = useState<keyof StockLevel | null>("qoh");
28 const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
37 }, [selectedLocation, includeZero]);
39 const loadLocations = async () => {
41 const result = await apiClient.get('/v1/locations');
42 if (result.DATS && Array.isArray(result.DATS)) {
43 const locs = result.DATS.map((loc: any) => ({
45 name: loc.f101 || loc.f500 || `Location ${loc.f100}`
50 console.error('Failed to load locations:', error);
54 const loadStockLevels = async () => {
60 includeZero: includeZero ? 'true' : 'false'
63 if (selectedLocation !== 'all') {
64 params.locationId = selectedLocation;
67 const queryString = new URLSearchParams(params as any).toString();
68 const result = await apiClient.get(`/v1/inventory/stock-levels?${queryString}`);
70 if (result.success && result.data) {
71 setStockLevels(result.data.rows || []);
73 setError('Failed to load stock levels');
76 } catch (error: any) {
77 console.error('Failed to load stock levels:', error);
78 setError(error.message || 'Network error while fetching stock levels');
85 const handleSort = (column: keyof StockLevel) => {
86 if (sortColumn === column) {
87 setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
89 setSortColumn(column);
90 setSortDirection(column === 'qoh' ? 'desc' : 'asc');
94 const sortedStockLevels = [...stockLevels].sort((a, b) => {
95 if (!sortColumn) return 0;
97 const aVal = a[sortColumn];
98 const bVal = b[sortColumn];
100 if (aVal === null && bVal === null) return 0;
101 if (aVal === null) return 1;
102 if (bVal === null) return -1;
105 if (typeof aVal === 'number' && typeof bVal === 'number') {
106 comparison = aVal - bVal;
108 comparison = String(aVal).localeCompare(String(bVal));
111 return sortDirection === 'asc' ? comparison : -comparison;
114 const totalQoh = stockLevels.reduce((sum, item) => sum + (item.qoh || 0), 0);
117 <div className="p-6 bg-bg min-h-screen">
118 <div className="mb-6">
119 <div className="bg-warn/10 p-4 rounded-lg border-l-4 border-warn mb-4">
120 <div className="text-text">
121 <h1 className="text-2xl font-bold flex items-center gap-2"><Icon name="inventory_2" size={28} /> Store Stock Levels</h1>
122 <p className="text-sm mt-1 text-muted">View current inventory levels across all locations</p>
128 <div className="bg-surface p-4 rounded-lg shadow border border-border mb-6">
129 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
131 <label className="block text-sm font-medium text-text mb-1">Location</label>
133 className="w-full p-3 border border-border rounded-lg bg-surface text-text focus:ring-2 focus:ring-brand"
134 value={selectedLocation}
135 onChange={(e) => setSelectedLocation(e.target.value)}
137 <option value="all">All Locations</option>
138 {locations.map(loc => (
139 <option key={loc.id} value={loc.id}>
146 <label className="block text-sm font-medium text-text mb-1">Display Options</label>
147 <div className="flex items-center h-full">
148 <label className="flex items-center cursor-pointer">
151 checked={includeZero}
152 onChange={(e) => setIncludeZero(e.target.checked)}
153 className="mr-2 h-4 w-4"
155 <span className="text-sm text-text">Include zero stock items</span>
159 <div className="flex items-end">
161 onClick={loadStockLevels}
162 className="px-4 py-3 bg-brand text-white rounded-lg hover:opacity-90 font-medium flex items-center gap-2"
164 <Icon name="refresh" size={20} /> Refresh
171 <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
172 <div className="bg-brand-2 p-4 rounded-lg border border-border">
173 <div className="text-2xl font-bold text-brand">{stockLevels.length}</div>
174 <div className="text-sm text-text">Total Items</div>
176 <div className="bg-surface-2 p-4 rounded-lg border border-border">
177 <div className="text-2xl font-bold text-brand">{totalQoh}</div>
178 <div className="text-sm text-text">Total Quantity</div>
180 <div className="bg-warn/10 p-4 rounded-lg border border-warn">
181 <div className="text-2xl font-bold text-warn">
182 {stockLevels.filter(s => s.qoh < 0).length}
184 <div className="text-sm text-text">Negative Stock</div>
188 {/* Error Display */}
190 <div className="bg-danger/10 border-l-4 border-danger p-4 mb-6">
191 <div className="flex">
192 <div className="flex-shrink-0">
193 <Icon name="warning" size={24} className="text-danger" />
195 <div className="ml-3">
196 <p className="text-sm text-danger">{error}</p>
202 {/* Loading Indicator */}
204 <div className="flex justify-center items-center py-12">
205 <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-brand"></div>
206 <span className="ml-3 text-muted">Loading stock levels...</span>
210 {/* Stock Levels Table */}
212 <div className="bg-surface rounded-lg shadow overflow-auto border border-border">
213 <table className="min-w-full divide-y divide-border">
214 <thead className="bg-[var(--brand)] text-surface">
217 className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider cursor-pointer hover:opacity-90"
218 onClick={() => handleSort('locname')}
220 Store {sortColumn === 'locname' && (sortDirection === 'asc' ? '▲' : '▼')}
223 className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider cursor-pointer hover:opacity-90"
224 onClick={() => handleSort('pid')}
226 PID {sortColumn === 'pid' && (sortDirection === 'asc' ? '▲' : '▼')}
229 className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider cursor-pointer hover:opacity-90"
230 onClick={() => handleSort('qoh')}
232 On Hand {sortColumn === 'qoh' && (sortDirection === 'asc' ? '▲' : '▼')}
235 className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider cursor-pointer hover:opacity-90"
236 onClick={() => handleSort('descrip')}
238 Description {sortColumn === 'descrip' && (sortDirection === 'asc' ? '▲' : '▼')}
241 className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider cursor-pointer hover:opacity-90"
242 onClick={() => handleSort('sku')}
244 SKU/PLU {sortColumn === 'sku' && (sortDirection === 'asc' ? '▲' : '▼')}
247 className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider cursor-pointer hover:opacity-90"
248 onClick={() => handleSort('partcode')}
250 Part Code {sortColumn === 'partcode' && (sortDirection === 'asc' ? '▲' : '▼')}
254 <tbody className="bg-surface divide-y divide-border">
255 {sortedStockLevels.length === 0 ? (
257 <td colSpan={6} className="px-6 py-4 text-center text-muted">
258 No stock levels found
262 sortedStockLevels.map((stock, index) => (
264 key={`${stock.pid}-${stock.locname}-${index}`}
266 ${stock.qoh < 0 ? 'bg-danger/10' : stock.qoh === 0 ? 'bg-surface-2' : ''}
270 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
273 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
276 <td className={`px-6 py-4 whitespace-nowrap text-sm font-medium ${
277 stock.qoh < 0 ? 'text-danger' :
278 stock.qoh === 0 ? 'text-muted' :
283 <td className="px-6 py-4 text-sm text-text">
286 <td className="px-6 py-4 whitespace-nowrap text-sm text-muted">
289 <td className="px-6 py-4 whitespace-nowrap text-sm text-muted">
290 {stock.partcode || '-'}
296 {sortedStockLevels.length > 0 && (
297 <tfoot className="bg-surface-2">
299 <td className="px-6 py-3 text-sm font-bold text-text">Totals</td>
300 <td className="px-6 py-3 text-sm"> </td>
301 <td className="px-6 py-3 text-sm font-bold text-text">{totalQoh}</td>
302 <td className="px-6 py-3 text-sm"> </td>
303 <td className="px-6 py-3 text-sm"> </td>
304 <td className="px-6 py-3 text-sm"> </td>