3import React, { useEffect, useMemo, useState } from "react";
4import { apiClient } from "@/lib/client/apiClient";
5import { Icon } from '@/contexts/IconContext';
7interface StocktakeRow {
13 status: "New" | "Complete" | "Cancelled";
20interface StoreOption {
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>("");
43 // eslint-disable-next-line react-hooks/exhaustive-deps
44 }, [showCurrent, showCompleted, showAll]);
46 const filterParam = useMemo(() => {
47 if (showAll) return "all";
48 if (showCurrent && !showCompleted) return "current";
49 if (showCompleted && !showCurrent) return "complete";
51 }, [showAll, showCurrent, showCompleted]);
53 const loadStores = async () => {
55 // Use new OpenAPI locations endpoint
56 const result = await apiClient.getLocations({ source: 'openapi' });
58 const apiData = result.data as any;
59 const locationData = apiData?.data?.Location;
60 if (Array.isArray(locationData)) {
62 locationData.map((loc: any) => ({
63 id: Number(loc.Locid) || 0,
70 console.error("Failed to load stores", err);
74 const loadList = async () => {
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 });
83 if (!result.success) {
89 const appData = result.data?.APPD;
90 if (!Array.isArray(appData)) {
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";
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,
117 console.error("Failed to load stocktake list", err);
118 setError("Failed to load stocktakes. Check API connection.");
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", {
135 const formatMoney = (value?: number) => {
136 return new Intl.NumberFormat("en-US", {
139 minimumFractionDigits: 2,
140 }).format(value || 0);
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.`,
149 if (!window.confirm(confirmText[action])) return;
151 const actionCode: Record<typeof action, number> = { cancel: 8, snapshot: 2, finalise: 4 };
156 "<f8_s>retailmax.elink.stocktake.control</f8_s>",
158 `<f180_E>${actionCode[action]}</f180_E>`,
159 `<f100_E>${id}</f100_E>`,
163 const resp = await fetch("/dati", {
165 headers: { "Content-Type": "application/xml", Accept: "application/json" },
166 credentials: "include",
169 if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
170 setActionMsg(`${action.charAt(0).toUpperCase() + action.slice(1)} request sent.`);
173 console.error(`Failed to ${action} stocktake`, err);
174 setActionMsg(`Failed to ${action} stocktake.`);
178 const createStocktake = async () => {
179 if (!newStock.description.trim()) {
180 setActionMsg("Description is required.");
183 if (!newStock.storeId || newStock.storeId === "0") {
184 setActionMsg("Please select a store.");
192 "<f8_s>retailmax.elink.stocktake.control</f8_s>",
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>` : "",
201 const resp = await fetch("/dati", {
203 headers: { "Content-Type": "application/xml", Accept: "application/json" },
204 credentials: "include",
207 if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
208 setActionMsg("Stocktake created.");
209 setNewStock({ description: "", storeId: "0", comments: "" });
212 console.error("Failed to create stocktake", err);
213 setActionMsg("Failed to create stocktake.");
219 const escapeXml = (v: string) =>
220 v.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
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">
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>
231 {actionMsg && <div className="text-sm text-text">{actionMsg}</div>}
232 {error && <div className="text-sm text-red-700">{error}</div>}
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" />
243 <label className="flex items-center gap-2">
244 <input type="checkbox" checked={showCompleted} onChange={(e) => setShowCompleted(e.target.checked)} className="rounded" />
247 <label className="flex items-center gap-2">
248 <input type="checkbox" checked={showAll} onChange={(e) => setShowAll(e.target.checked)} className="rounded" />
251 {loading && <span className="text-xs text-muted">Loading…</span>}
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">
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>
271 {rows.length === 0 ? (
273 <td colSpan={10} className="text-center py-6 text-muted">
274 {loading ? "Loading stocktakes..." : "No stocktakes to show."}
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">
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"
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"
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"
322 <div className="bg-surface rounded-lg shadow p-4 space-y-4">
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>
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"
335 <label className="block text-sm font-medium text-text mb-1 mt-3">Store</label>
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"
341 <option value="0">-- Select store --</option>
343 <option key={s.id} value={s.id}>
349 <label className="block text-sm font-medium text-text mb-1 mt-3">Comments (optional)</label>
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"
357 <div className="flex gap-3 mt-4">
359 onClick={createStocktake}
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"
363 {saving ? "Saving..." : "Create Stocktake"}
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"
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>
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"
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>
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"
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>
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"
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>
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"
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>
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"