EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
page.tsx
Go to the documentation of this file.
1"use client";
2import { useEffect, useState } from "react";
3import { Icon } from '@/contexts/IconContext';
4import { apiClient } from "@/lib/client/apiClient";
5
6interface StockLevel {
7 locname: string;
8 pid: number;
9 qoh: number;
10 descrip: string;
11 sku: string | null;
12 partcode: string | null;
13}
14
15interface Location {
16 id: number;
17 name: string;
18}
19
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");
29
30 useEffect(() => {
31 loadLocations();
32 loadStockLevels();
33 }, []);
34
35 useEffect(() => {
36 loadStockLevels();
37 }, [selectedLocation, includeZero]);
38
39 const loadLocations = async () => {
40 try {
41 const result = await apiClient.get('/v1/locations');
42 if (result.DATS && Array.isArray(result.DATS)) {
43 const locs = result.DATS.map((loc: any) => ({
44 id: loc.f100,
45 name: loc.f101 || loc.f500 || `Location ${loc.f100}`
46 }));
47 setLocations(locs);
48 }
49 } catch (error) {
50 console.error('Failed to load locations:', error);
51 }
52 };
53
54 const loadStockLevels = async () => {
55 try {
56 setLoading(true);
57 setError(null);
58
59 const params: any = {
60 includeZero: includeZero ? 'true' : 'false'
61 };
62
63 if (selectedLocation !== 'all') {
64 params.locationId = selectedLocation;
65 }
66
67 const queryString = new URLSearchParams(params as any).toString();
68 const result = await apiClient.get(`/v1/inventory/stock-levels?${queryString}`);
69
70 if (result.success && result.data) {
71 setStockLevels(result.data.rows || []);
72 } else {
73 setError('Failed to load stock levels');
74 setStockLevels([]);
75 }
76 } catch (error: any) {
77 console.error('Failed to load stock levels:', error);
78 setError(error.message || 'Network error while fetching stock levels');
79 setStockLevels([]);
80 } finally {
81 setLoading(false);
82 }
83 };
84
85 const handleSort = (column: keyof StockLevel) => {
86 if (sortColumn === column) {
87 setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
88 } else {
89 setSortColumn(column);
90 setSortDirection(column === 'qoh' ? 'desc' : 'asc');
91 }
92 };
93
94 const sortedStockLevels = [...stockLevels].sort((a, b) => {
95 if (!sortColumn) return 0;
96
97 const aVal = a[sortColumn];
98 const bVal = b[sortColumn];
99
100 if (aVal === null && bVal === null) return 0;
101 if (aVal === null) return 1;
102 if (bVal === null) return -1;
103
104 let comparison = 0;
105 if (typeof aVal === 'number' && typeof bVal === 'number') {
106 comparison = aVal - bVal;
107 } else {
108 comparison = String(aVal).localeCompare(String(bVal));
109 }
110
111 return sortDirection === 'asc' ? comparison : -comparison;
112 });
113
114 const totalQoh = stockLevels.reduce((sum, item) => sum + (item.qoh || 0), 0);
115
116 return (
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>
123 </div>
124 </div>
125 </div>
126
127 {/* Filters */}
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">
130 <div>
131 <label className="block text-sm font-medium text-text mb-1">Location</label>
132 <select
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)}
136 >
137 <option value="all">All Locations</option>
138 {locations.map(loc => (
139 <option key={loc.id} value={loc.id}>
140 {loc.name}
141 </option>
142 ))}
143 </select>
144 </div>
145 <div>
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">
149 <input
150 type="checkbox"
151 checked={includeZero}
152 onChange={(e) => setIncludeZero(e.target.checked)}
153 className="mr-2 h-4 w-4"
154 />
155 <span className="text-sm text-text">Include zero stock items</span>
156 </label>
157 </div>
158 </div>
159 <div className="flex items-end">
160 <button
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"
163 >
164 <Icon name="refresh" size={20} /> Refresh
165 </button>
166 </div>
167 </div>
168 </div>
169
170 {/* Stats */}
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>
175 </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>
179 </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}
183 </div>
184 <div className="text-sm text-text">Negative Stock</div>
185 </div>
186 </div>
187
188 {/* Error Display */}
189 {error && (
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" />
194 </div>
195 <div className="ml-3">
196 <p className="text-sm text-danger">{error}</p>
197 </div>
198 </div>
199 </div>
200 )}
201
202 {/* Loading Indicator */}
203 {loading && (
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>
207 </div>
208 )}
209
210 {/* Stock Levels Table */}
211 {!loading && (
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">
215 <tr>
216 <th
217 className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider cursor-pointer hover:opacity-90"
218 onClick={() => handleSort('locname')}
219 >
220 Store {sortColumn === 'locname' && (sortDirection === 'asc' ? '▲' : '▼')}
221 </th>
222 <th
223 className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider cursor-pointer hover:opacity-90"
224 onClick={() => handleSort('pid')}
225 >
226 PID {sortColumn === 'pid' && (sortDirection === 'asc' ? '▲' : '▼')}
227 </th>
228 <th
229 className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider cursor-pointer hover:opacity-90"
230 onClick={() => handleSort('qoh')}
231 >
232 On Hand {sortColumn === 'qoh' && (sortDirection === 'asc' ? '▲' : '▼')}
233 </th>
234 <th
235 className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider cursor-pointer hover:opacity-90"
236 onClick={() => handleSort('descrip')}
237 >
238 Description {sortColumn === 'descrip' && (sortDirection === 'asc' ? '▲' : '▼')}
239 </th>
240 <th
241 className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider cursor-pointer hover:opacity-90"
242 onClick={() => handleSort('sku')}
243 >
244 SKU/PLU {sortColumn === 'sku' && (sortDirection === 'asc' ? '▲' : '▼')}
245 </th>
246 <th
247 className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider cursor-pointer hover:opacity-90"
248 onClick={() => handleSort('partcode')}
249 >
250 Part Code {sortColumn === 'partcode' && (sortDirection === 'asc' ? '▲' : '▼')}
251 </th>
252 </tr>
253 </thead>
254 <tbody className="bg-surface divide-y divide-border">
255 {sortedStockLevels.length === 0 ? (
256 <tr>
257 <td colSpan={6} className="px-6 py-4 text-center text-muted">
258 No stock levels found
259 </td>
260 </tr>
261 ) : (
262 sortedStockLevels.map((stock, index) => (
263 <tr
264 key={`${stock.pid}-${stock.locname}-${index}`}
265 className={`
266 ${stock.qoh < 0 ? 'bg-danger/10' : stock.qoh === 0 ? 'bg-surface-2' : ''}
267 hover:bg-surface-2
268 `}
269 >
270 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
271 {stock.locname}
272 </td>
273 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
274 {stock.pid}
275 </td>
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' :
279 'text-brand'
280 }`}>
281 {stock.qoh}
282 </td>
283 <td className="px-6 py-4 text-sm text-text">
284 {stock.descrip}
285 </td>
286 <td className="px-6 py-4 whitespace-nowrap text-sm text-muted">
287 {stock.sku || '-'}
288 </td>
289 <td className="px-6 py-4 whitespace-nowrap text-sm text-muted">
290 {stock.partcode || '-'}
291 </td>
292 </tr>
293 ))
294 )}
295 </tbody>
296 {sortedStockLevels.length > 0 && (
297 <tfoot className="bg-surface-2">
298 <tr>
299 <td className="px-6 py-3 text-sm font-bold text-text">Totals</td>
300 <td className="px-6 py-3 text-sm">&nbsp;</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">&nbsp;</td>
303 <td className="px-6 py-3 text-sm">&nbsp;</td>
304 <td className="px-6 py-3 text-sm">&nbsp;</td>
305 </tr>
306 </tfoot>
307 )}
308 </table>
309 </div>
310 )}
311 </div>
312 );
313}