feat: initialize inventory management application with Prisma schema, QR scanning, and UI components
This commit is contained in:
224
app/scan/page.js
Normal file
224
app/scan/page.js
Normal file
@ -0,0 +1,224 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { Html5QrcodeScanner, Html5QrcodeSupportedFormats } from "html5-qrcode";
|
||||
import { Scan as ScanIcon, Check, Minus, Plus, AlertTriangle, X } from "lucide-react";
|
||||
|
||||
export default function ScanPage() {
|
||||
const [scannedData, setScannedData] = useState(null);
|
||||
const [item, setItem] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const [quantityAdjust, setQuantityAdjust] = useState(0);
|
||||
const [updating, setUpdating] = useState(false);
|
||||
|
||||
const scannerRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Only initialize scanner if not already scanning a specific item
|
||||
if (scannedData) return;
|
||||
|
||||
const onScanSuccess = (decodedText) => {
|
||||
setScannedData(decodedText);
|
||||
scannerRef.current?.clear();
|
||||
};
|
||||
|
||||
scannerRef.current = new Html5QrcodeScanner(
|
||||
"reader",
|
||||
{
|
||||
fps: 10,
|
||||
qrbox: { width: 250, height: 250 },
|
||||
formatsToSupport: [ Html5QrcodeSupportedFormats.QR_CODE ]
|
||||
},
|
||||
/* verbose= */ false
|
||||
);
|
||||
scannerRef.current.render(onScanSuccess, () => {});
|
||||
|
||||
return () => {
|
||||
scannerRef.current?.clear().catch(e => console.error("Failed to clear scanner", e));
|
||||
};
|
||||
}, [scannedData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!scannedData) return;
|
||||
|
||||
const lookupItem = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
// Fetch items and find the matched one
|
||||
const res = await fetch("/api/inventory");
|
||||
const list = await res.json();
|
||||
|
||||
const matchedItem = list.find(i => i.qrCodeId === scannedData || i.id === scannedData);
|
||||
if (matchedItem) {
|
||||
setItem(matchedItem);
|
||||
setQuantityAdjust(0);
|
||||
} else {
|
||||
setError(`No item found in database matching: ${scannedData}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
lookupItem();
|
||||
}, [scannedData]);
|
||||
|
||||
const handleUpdate = async () => {
|
||||
if (!item) return;
|
||||
setUpdating(true);
|
||||
try {
|
||||
const updatedQuantity = item.quantity + quantityAdjust;
|
||||
const res = await fetch("/api/inventory", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
id: item.id,
|
||||
quantity: updatedQuantity
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error("Failed to update quantity");
|
||||
|
||||
const updated = await res.json();
|
||||
setItem(updated);
|
||||
setQuantityAdjust(0);
|
||||
alert(`Updated successfully. New quantity: ${updated.quantity}`);
|
||||
|
||||
// Optionally continue scanning
|
||||
resetScanner();
|
||||
} catch (err) {
|
||||
alert("Error: " + err.message);
|
||||
} finally {
|
||||
setUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetScanner = () => {
|
||||
setScannedData(null);
|
||||
setItem(null);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: "600px", margin: "0 auto" }}>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h2 className="flex items-center gap-2 mb-2">
|
||||
<ScanIcon size={28} className="text-primary" />
|
||||
Scan QR
|
||||
</h2>
|
||||
<p className="text-muted">Use your camera scanner to locate and update items.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!scannedData ? (
|
||||
<div className="glass overflow-hidden">
|
||||
<div id="reader" style={{ width: "100%", border: "none" }}></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="glass" style={{ padding: "2rem" }}>
|
||||
{loading ? (
|
||||
<div className="text-center p-8">Loading inventory details...</div>
|
||||
) : error ? (
|
||||
<div className="text-center p-8">
|
||||
<AlertTriangle size={48} className="text-danger m-auto mb-4" />
|
||||
<h3 className="text-danger mb-2">Item not found</h3>
|
||||
<p className="text-muted mb-6">{error}</p>
|
||||
<button className="btn btn-secondary w-full" onClick={resetScanner}>
|
||||
Scan Again
|
||||
</button>
|
||||
</div>
|
||||
) : item && (
|
||||
<div>
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div>
|
||||
<h3 style={{ fontSize: "1.5rem", marginBottom: "0.25rem" }}>{item.name}</h3>
|
||||
<code className="text-muted" style={{ background: "rgba(255,255,255,0.05)", padding: "2px 6px", borderRadius: "4px" }}>
|
||||
ID: {item.qrCodeId}
|
||||
</code>
|
||||
</div>
|
||||
<button className="btn btn-danger" onClick={resetScanner} style={{ padding: "0.5rem" }} title="Cancel">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem", marginBottom: "2rem" }}>
|
||||
<div style={{ padding: "1rem", backgroundColor: "rgba(255,255,255,0.05)", borderRadius: "8px" }}>
|
||||
<div className="text-muted" style={{ fontSize: "0.85rem", textTransform: "uppercase" }}>Current Quant.</div>
|
||||
<div style={{ fontSize: "2rem", fontWeight: "bold" }}>{item.quantity}</div>
|
||||
</div>
|
||||
<div style={{ padding: "1rem", backgroundColor: "rgba(255,255,255,0.05)", borderRadius: "8px" }}>
|
||||
<div className="text-muted" style={{ fontSize: "0.85rem", textTransform: "uppercase" }}>Price</div>
|
||||
<div style={{ fontSize: "2rem", fontWeight: "bold" }}>${Number(item.price).toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<label className="text-muted mb-2 block text-center">Adjust Quantity</label>
|
||||
<div className="flex justify-center items-center gap-4">
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
style={{ padding: "1rem", borderRadius: "50%" }}
|
||||
onClick={() => setQuantityAdjust(q => q - 1)}
|
||||
>
|
||||
<Minus size={24} />
|
||||
</button>
|
||||
<div style={{ fontSize: "2.5rem", width: "80px", textAlign: "center", fontWeight: "bold" }}>
|
||||
{quantityAdjust > 0 ? `+${quantityAdjust}` : quantityAdjust}
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-success"
|
||||
style={{ padding: "1rem", borderRadius: "50%", backgroundColor: "var(--success)", color: "white", border: "none" }}
|
||||
onClick={() => setQuantityAdjust(q => q + 1)}
|
||||
>
|
||||
<Plus size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-center mt-4">
|
||||
<span className="text-muted">New Quantity will be: </span>
|
||||
<strong style={{ fontSize: "1.2rem", color: (item.quantity + quantityAdjust) < 0 ? "var(--danger)" : "var(--text-main)" }}>
|
||||
{item.quantity + quantityAdjust}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
style={{ width: "100%", justifyContent: "center", padding: "1rem" }}
|
||||
onClick={handleUpdate}
|
||||
disabled={updating || quantityAdjust === 0 || (item.quantity + quantityAdjust) < 0}
|
||||
>
|
||||
{updating ? "Saving..." : <><Check size={20} /> Confirm Update</>}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style>{`
|
||||
/* Scanner UI Overrides */
|
||||
#reader__dashboard_section_csr span { color: var(--text-main) !important; }
|
||||
#reader__dashboard_section_swaplink { color: var(--primary) !important; }
|
||||
#reader button {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
#reader {
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-main);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user