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";
2
3import React, { useEffect, useMemo, useState } from "react";
4import { apiClient } from "@/lib/client/apiClient";
5import { Icon } from '@/contexts/IconContext';
6
7interface StocktakeRow {
8 id: number;
9 name: string;
10 created?: string;
11 snapshot?: string;
12 finalised?: string;
13 status: "New" | "Complete" | "Cancelled";
14 store?: string;
15 counted?: number;
16 value?: number;
17 rawFlags?: number;
18}
19
20interface StoreOption {
21 id: number;
22 name: string;
23}
24
25export default function StocktakePage() {
26 const [rows, setRows] = useState<StocktakeRow[]>([]);
27 const [loading, setLoading] = useState(false);
28 const [error, setError] = useState<string>("");
29 const [showCurrent, setShowCurrent] = useState(true);
30 const [showCompleted, setShowCompleted] = useState(false);
31 const [showAll, setShowAll] = useState(false);
32 const [stores, setStores] = useState<StoreOption[]>([]);
33 const [newStock, setNewStock] = useState({ description: "", storeId: "0", comments: "" });
34 const [saving, setSaving] = useState(false);
35 const [actionMsg, setActionMsg] = useState<string>("");
36
37 useEffect(() => {
38 loadStores();
39 }, []);
40
41 useEffect(() => {
42 loadList();
43 // eslint-disable-next-line react-hooks/exhaustive-deps
44 }, [showCurrent, showCompleted, showAll]);
45
46 const filterParam = useMemo(() => {
47 if (showAll) return "all";
48 if (showCurrent && !showCompleted) return "current";
49 if (showCompleted && !showCurrent) return "complete";
50 return "all";
51 }, [showAll, showCurrent, showCompleted]);
52
53 const loadStores = async () => {
54 try {
55 // Use new OpenAPI locations endpoint
56 const result = await apiClient.getLocations({ source: 'openapi' });
57 if (result.success) {
58 const apiData = result.data as any;
59 const locationData = apiData?.data?.Location;
60 if (Array.isArray(locationData)) {
61 setStores(
62 locationData.map((loc: any) => ({
63 id: Number(loc.Locid) || 0,
64 name: loc.Name || ""
65 }))
66 );
67 }
68 }
69 } catch (err) {
70 console.error("Failed to load stores", err);
71 }
72 };
73
74 const loadList = async () => {
75 try {
76 setLoading(true);
77 setError("");
78
79 // Use BUCK eLink API for stocktake list
80 const filter = showAll ? 'all' : showCurrent ? 'current' : showCompleted ? 'complete' : 'all';
81 const result = await apiClient.getBuckStocktakes({ filter });
82
83 if (!result.success) {
84 setRows([]);
85 setLoading(false);
86 return;
87 }
88
89 const appData = result.data?.APPD;
90 if (!Array.isArray(appData)) {
91 setRows([]);
92 setLoading(false);
93 return;
94 }
95
96 const mapped: StocktakeRow[] = appData.map((item: any) => {
97 const flags = Number(item.f116) || 0;
98 let status: StocktakeRow["status"] = "New";
99 if (flags & 4) status = "Cancelled";
100 else if (flags & 2) status = "Complete";
101 return {
102 id: Number(item.f110) || 0,
103 name: item.f114 || "",
104 created: item.f113 || "",
105 snapshot: item.f112 || "",
106 finalised: item.f111 || "",
107 store: item.f1115 || "",
108 counted: Number(item.f200) || 0,
109 value: Number(item.f201) || 0,
110 status,
111 rawFlags: flags,
112 };
113 });
114 setRows(mapped);
115 setLoading(false);
116 } catch (err) {
117 console.error("Failed to load stocktake list", err);
118 setError("Failed to load stocktakes. Check API connection.");
119 setRows([]);
120 setLoading(false);
121 }
122 };
123
124 const formatDate = (value?: string) => {
125 if (!value) return "";
126 const d = new Date(value);
127 if (Number.isNaN(d.getTime())) return value;
128 return d.toLocaleDateString("en-GB", {
129 day: "2-digit",
130 month: "short",
131 year: "numeric",
132 });
133 };
134
135 const formatMoney = (value?: number) => {
136 return new Intl.NumberFormat("en-US", {
137 style: "currency",
138 currency: "USD",
139 minimumFractionDigits: 2,
140 }).format(value || 0);
141 };
142
143 const runControlAction = async (id: number, action: "cancel" | "snapshot" | "finalise") => {
144 const confirmText: Record<typeof action, string> = {
145 cancel: `Cancel stocktake #${id}? All counts will be discarded.`,
146 snapshot: `Snapshot stocktake #${id}? This records current levels for finalising later.`,
147 finalise: `Finalise stocktake #${id}? Counts will become current stock on hand.`,
148 };
149 if (!window.confirm(confirmText[action])) return;
150
151 const actionCode: Record<typeof action, number> = { cancel: 8, snapshot: 2, finalise: 4 };
152 try {
153 setActionMsg("");
154 const xml = [
155 "<DATI>",
156 "<f8_s>retailmax.elink.stocktake.control</f8_s>",
157 "<f11_B>E</f11_B>",
158 `<f180_E>${actionCode[action]}</f180_E>`,
159 `<f100_E>${id}</f100_E>`,
160 "</DATI>",
161 ].join("");
162
163 const resp = await fetch("/dati", {
164 method: "POST",
165 headers: { "Content-Type": "application/xml", Accept: "application/json" },
166 credentials: "include",
167 body: xml,
168 });
169 if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
170 setActionMsg(`${action.charAt(0).toUpperCase() + action.slice(1)} request sent.`);
171 loadList();
172 } catch (err) {
173 console.error(`Failed to ${action} stocktake`, err);
174 setActionMsg(`Failed to ${action} stocktake.`);
175 }
176 };
177
178 const createStocktake = async () => {
179 if (!newStock.description.trim()) {
180 setActionMsg("Description is required.");
181 return;
182 }
183 if (!newStock.storeId || newStock.storeId === "0") {
184 setActionMsg("Please select a store.");
185 return;
186 }
187 try {
188 setSaving(true);
189 setActionMsg("");
190 const xml = [
191 "<DATI>",
192 "<f8_s>retailmax.elink.stocktake.control</f8_s>",
193 "<f11_B>I</f11_B>",
194 "<f180_E>1</f180_E>",
195 `<f202_s>${escapeXml(newStock.description)}</f202_s>`,
196 `<f207_E>${newStock.storeId}</f207_E>`,
197 newStock.comments ? `<f209_s>${escapeXml(newStock.comments)}</f209_s>` : "",
198 "</DATI>",
199 ].join("");
200
201 const resp = await fetch("/dati", {
202 method: "POST",
203 headers: { "Content-Type": "application/xml", Accept: "application/json" },
204 credentials: "include",
205 body: xml,
206 });
207 if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
208 setActionMsg("Stocktake created.");
209 setNewStock({ description: "", storeId: "0", comments: "" });
210 loadList();
211 } catch (err) {
212 console.error("Failed to create stocktake", err);
213 setActionMsg("Failed to create stocktake.");
214 } finally {
215 setSaving(false);
216 }
217 };
218
219 const escapeXml = (v: string) =>
220 v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
221
222 return (
223 <div className="p-6 max-w-7xl mx-auto space-y-6">
224 <div className="flex items-center justify-between flex-wrap gap-3">
225 <div>
226 <h1 className="text-3xl font-bold text-text">Stocktake</h1>
227 <p className="text-sm text-muted">Manage stock counts and formal stocktakes.</p>
228 </div>
229 </div>
230
231 {actionMsg && <div className="text-sm text-text">{actionMsg}</div>}
232 {error && <div className="text-sm text-red-700">{error}</div>}
233
234 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
235 <div className="lg:col-span-2 bg-surface rounded-lg shadow p-4">
236 <div className="flex items-center justify-between mb-4 gap-3">
237 <h2 className="text-lg font-semibold text-text">Current Stocktakes</h2>
238 <div className="flex items-center gap-4 text-sm text-text">
239 <label className="flex items-center gap-2">
240 <input type="checkbox" checked={showCurrent} onChange={(e) => setShowCurrent(e.target.checked)} className="rounded" />
241 Show Current
242 </label>
243 <label className="flex items-center gap-2">
244 <input type="checkbox" checked={showCompleted} onChange={(e) => setShowCompleted(e.target.checked)} className="rounded" />
245 Show Completed
246 </label>
247 <label className="flex items-center gap-2">
248 <input type="checkbox" checked={showAll} onChange={(e) => setShowAll(e.target.checked)} className="rounded" />
249 Show All
250 </label>
251 {loading && <span className="text-xs text-muted">Loading…</span>}
252 </div>
253 </div>
254 <div className="overflow-x-auto">
255 <table className="min-w-full text-sm border border-border">
256 <thead className="bg-surface-2 text-text">
257 <tr>
258 <th className="px-3 py-2 border-b">Id</th>
259 <th className="px-3 py-2 border-b">Name</th>
260 <th className="px-3 py-2 border-b">Created</th>
261 <th className="px-3 py-2 border-b">Snapshot</th>
262 <th className="px-3 py-2 border-b">Finalised</th>
263 <th className="px-3 py-2 border-b">Store</th>
264 <th className="px-3 py-2 border-b text-right">Counted</th>
265 <th className="px-3 py-2 border-b text-right">Value</th>
266 <th className="px-3 py-2 border-b">Status</th>
267 <th className="px-3 py-2 border-b">Actions</th>
268 </tr>
269 </thead>
270 <tbody>
271 {rows.length === 0 ? (
272 <tr>
273 <td colSpan={10} className="text-center py-6 text-muted">
274 {loading ? "Loading stocktakes..." : "No stocktakes to show."}
275 </td>
276 </tr>
277 ) : (
278 rows.map((row, idx) => (
279 <tr key={row.id} className={idx % 2 === 0 ? "bg-surface" : "bg-surface-2"}>
280 <td className="px-3 py-2 border-b whitespace-nowrap">{row.id}</td>
281 <td className="px-3 py-2 border-b">{row.name}</td>
282 <td className="px-3 py-2 border-b whitespace-nowrap">{formatDate(row.created)}</td>
283 <td className="px-3 py-2 border-b whitespace-nowrap">{formatDate(row.snapshot)}</td>
284 <td className="px-3 py-2 border-b whitespace-nowrap">{formatDate(row.finalised)}</td>
285 <td className="px-3 py-2 border-b whitespace-nowrap">{row.store}</td>
286 <td className="px-3 py-2 border-b text-right">{row.counted ?? ""}</td>
287 <td className="px-3 py-2 border-b text-right">{formatMoney(row.value)}</td>
288 <td className="px-3 py-2 border-b whitespace-nowrap">{row.status}</td>
289 <td className="px-3 py-2 border-b whitespace-nowrap">
290 <div className="flex gap-2">
291 <button
292 onClick={() => runControlAction(row.id, "snapshot")}
293 disabled={row.status !== "New"}
294 className="px-3 py-1 bg-info/10 text-info rounded text-xs disabled:bg-surface-2 disabled:text-muted"
295 >
296 Snapshot
297 </button>
298 <button
299 onClick={() => runControlAction(row.id, "finalise")}
300 disabled={row.status !== "New"}
301 className="px-3 py-1 bg-green-100 text-green-800 rounded text-xs disabled:bg-surface-2 disabled:text-muted"
302 >
303 Finalise
304 </button>
305 <button
306 onClick={() => runControlAction(row.id, "cancel")}
307 disabled={row.status !== "New"}
308 className="px-3 py-1 bg-red-100 text-red-800 rounded text-xs disabled:bg-surface-2 disabled:text-muted"
309 >
310 Cancel
311 </button>
312 </div>
313 </td>
314 </tr>
315 ))
316 )}
317 </tbody>
318 </table>
319 </div>
320 </div>
321
322 <div className="bg-surface rounded-lg shadow p-4 space-y-4">
323 <div>
324 <h2 className="text-lg font-semibold text-text">Create New Stocktake</h2>
325 <p className="text-sm text-muted mb-3">Create a formal stocktake for a store.</p>
326 <label className="block text-sm font-medium text-text mb-1">Description</label>
327 <input
328 type="text"
329 value={newStock.description}
330 onChange={(e) => setNewStock({ ...newStock, description: e.target.value })}
331 className="w-full px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-brand/50"
332 placeholder="EOY 2025 Stocktake"
333 />
334
335 <label className="block text-sm font-medium text-text mb-1 mt-3">Store</label>
336 <select
337 value={newStock.storeId}
338 onChange={(e) => setNewStock({ ...newStock, storeId: e.target.value })}
339 className="w-full px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-brand/50"
340 >
341 <option value="0">-- Select store --</option>
342 {stores.map((s) => (
343 <option key={s.id} value={s.id}>
344 {s.name}
345 </option>
346 ))}
347 </select>
348
349 <label className="block text-sm font-medium text-text mb-1 mt-3">Comments (optional)</label>
350 <textarea
351 value={newStock.comments}
352 onChange={(e) => setNewStock({ ...newStock, comments: e.target.value })}
353 className="w-full px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-brand/50"
354 rows={3}
355 />
356
357 <div className="flex gap-3 mt-4">
358 <button
359 onClick={createStocktake}
360 disabled={saving}
361 className="px-4 py-2 bg-brand text-white rounded-md text-sm font-semibold hover:bg-brand/90 disabled:bg-muted disabled:cursor-not-allowed"
362 >
363 {saving ? "Saving..." : "Create Stocktake"}
364 </button>
365 <button
366 onClick={() => setNewStock({ description: "", storeId: "0", comments: "" })}
367 className="px-4 py-2 border border-border rounded-md text-sm text-text hover:bg-surface-2"
368 >
369 Clear
370 </button>
371 </div>
372 </div>
373
374 <div className="border-t pt-4 space-y-3 text-sm text-text">
375 <h3 className="text-base font-semibold text-text">Stock Counting Tools</h3>
376 <p className="text-xs text-muted">Choose the right tool for how you want to count.</p>
377 <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
378 <div className="border rounded-lg p-3 bg-surface shadow-sm">
379 <h4 className="font-semibold text-text">Enter Counts</h4>
380 <p className="text-xs text-muted mt-1">Quick, ad‑hoc counting of items on hand. Not tied to a formal stocktake.</p>
381 <a
382 href="/pages/products/stocktake/simple-count"
383 className="inline-block mt-3 px-3 py-1.5 rounded-md text-xs font-semibold text-white bg-brand hover:bg-brand/90"
384 >
385 Open Simple Counting
386 </a>
387 </div>
388
389 <div className="border rounded-lg p-3 bg-surface shadow-sm">
390 <h4 className="font-semibold text-text">Counting Review</h4>
391 <p className="text-xs text-muted mt-1">Review and reconcile counts entered via simple counting. Fix mistakes before posting.</p>
392 <a
393 href="/report/pos/stock/fieldpine/stock2countreview.htm"
394 className="inline-block mt-3 px-3 py-1.5 rounded-md text-xs font-semibold text-white bg-brand hover:bg-brand/90"
395 >
396 Open Review
397 </a>
398 </div>
399
400 <div className="border rounded-lg p-3 bg-surface shadow-sm">
401 <h4 className="font-semibold text-text">Formal Stocktake Counts</h4>
402 <p className="text-xs text-muted mt-1">Enter counts against a specific stocktake event. Supports sections and variance handling.</p>
403 <a
404 href="/report/pos/stock/fieldpine/stocktakecount.htm"
405 className="inline-block mt-3 px-3 py-1.5 rounded-md text-xs font-semibold text-white bg-brand hover:bg-brand/90"
406 >
407 Open Formal Counting
408 </a>
409 </div>
410
411 <div className="border rounded-lg p-3 bg-surface shadow-sm">
412 <h4 className="font-semibold text-text">Bulk Load Counts</h4>
413 <p className="text-xs text-muted mt-1">Upload CSV or scanner files to import large counts in one go.</p>
414 <a
415 href="/report/pos/stock/fieldpine/stocktakecountupload.htm"
416 className="inline-block mt-3 px-3 py-1.5 rounded-md text-xs font-semibold text-white bg-brand hover:bg-brand/90"
417 >
418 Open Bulk Upload
419 </a>
420 </div>
421
422 <div className="border rounded-lg p-3 bg-surface shadow-sm sm:col-span-2">
423 <h4 className="font-semibold text-text">Spot Check (Tracked Items)</h4>
424 <p className="text-xs text-muted mt-1">Quick verification for serial/batch tracked items and high‑value products.</p>
425 <a
426 href="/report/pos/stock/fieldpine/spotcheck.htm"
427 className="inline-block mt-3 px-3 py-1.5 rounded-md text-xs font-semibold text-white bg-brand hover:bg-brand/90"
428 >
429 Open Spot Check
430 </a>
431 </div>
432 </div>
433 </div>
434 </div>
435 </div>
436 </div>
437 );
438}