feat: initialize inventory management application with Prisma schema, QR scanning, and UI components

This commit is contained in:
2026-04-05 14:27:36 -04:00
parent 6663856ffb
commit 9b746388fd
16 changed files with 2698 additions and 121 deletions

207
app/add/page.js Normal file
View File

@ -0,0 +1,207 @@
"use client";
import { useState } from "react";
import { Upload, Plus, AlertCircle, CheckCircle2 } from "lucide-react";
import * as xlsx from "xlsx";
export default function AddImportPage() {
const [formData, setFormData] = useState({ name: "", qrCodeId: "", quantity: 0, price: 0.0, category: "", description: "" });
const [loading, setLoading] = useState(false);
const [status, setStatus] = useState({ type: "", message: "" });
const [importStats, setImportStats] = useState(null);
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleManualSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setStatus({ type: "", message: "" });
try {
const payload = {
...formData,
quantity: parseInt(formData.quantity) || 0,
price: parseFloat(formData.price) || 0.0,
};
const res = await fetch("/api/inventory", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Failed to create item");
setStatus({ type: "success", message: `Successfully added ${data.name}.` });
setFormData({ name: "", qrCodeId: "", quantity: 0, price: 0.0, category: "", description: "" });
} catch (err) {
setStatus({ type: "error", message: err.message });
} finally {
setLoading(false);
}
};
const handleFileUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
setLoading(true);
setStatus({ type: "", message: "" });
setImportStats(null);
const reader = new FileReader();
reader.onload = async (evt) => {
try {
const bstr = evt.target.result;
const wb = xlsx.read(bstr, { type: "binary" });
const wsname = wb.SheetNames[0];
const ws = wb.Sheets[wsname];
const data = xlsx.utils.sheet_to_json(ws);
let successCount = 0;
let errorCount = 0;
for (const row of data) {
try {
// Normalize row keys to prevent trailing spaces or casing errors
const normalizedRow = {};
for (const key in row) {
if (row.hasOwnProperty(key)) {
normalizedRow[key.trim().toLowerCase()] = row[key];
}
}
const payload = {
qrCodeId: (normalizedRow["qr id"] || normalizedRow["qrcodeid"] || normalizedRow["id"] || normalizedRow["sku"])?.toString(),
name: normalizedRow["name"] || normalizedRow["item"],
quantity: parseInt(normalizedRow["quantity"] || normalizedRow["qty"]) || 0,
price: parseFloat(normalizedRow["price"] || normalizedRow["cost"]) || 0.0,
category: normalizedRow["category"] || "",
description: normalizedRow["description"] || "",
};
if (!payload.qrCodeId || !payload.name) {
errorCount++;
continue;
}
const res = await fetch("/api/inventory", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (res.ok) {
successCount++;
} else {
errorCount++;
}
} catch (error) {
errorCount++;
}
}
setImportStats({ successCount, errorCount, total: data.length });
setStatus({ type: "success", message: `Import completed.` });
} catch (err) {
setStatus({ type: "error", message: "Failed to parse or process the Excel file." });
} finally {
setLoading(false);
}
};
reader.readAsBinaryString(file);
e.target.value = ""; // reset file input
};
return (
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "2rem" }}>
{/* Manual Entry Form */}
<div className="glass" style={{ padding: "2rem" }}>
<h2 className="flex items-center gap-2 mb-4">
<Plus size={24} className="text-primary" />
Add Item Manually
</h2>
<form onSubmit={handleManualSubmit}>
<div className="form-group">
<label>Item Name *</label>
<input required type="text" name="name" className="input-field" value={formData.name} onChange={handleInputChange} placeholder="e.g. Widget A" />
</div>
<div className="form-group">
<label>QR Code ID / SKU *</label>
<input required type="text" name="qrCodeId" className="input-field" value={formData.qrCodeId} onChange={handleInputChange} placeholder="Unique identifier" />
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}>
<div className="form-group">
<label>Quantity</label>
<input type="number" name="quantity" className="input-field" value={formData.quantity} onChange={handleInputChange} />
</div>
<div className="form-group">
<label>Price</label>
<input type="number" step="0.01" name="price" className="input-field" value={formData.price} onChange={handleInputChange} />
</div>
</div>
<div className="form-group">
<label>Category</label>
<input type="text" name="category" className="input-field" value={formData.category} onChange={handleInputChange} />
</div>
<button type="submit" className="btn btn-primary mt-4" style={{ width: "100%", justifyContent: "center" }} disabled={loading}>
{loading ? "Saving..." : "Add to Inventory"}
</button>
</form>
</div>
{/* Excel Import Panel */}
<div style={{ display: "flex", flexDirection: "column", gap: "2rem" }}>
<div className="glass" style={{ padding: "2rem", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", minHeight: "300px", textAlign: "center", borderStyle: "dashed" }}>
<Upload size={48} className="text-primary m-auto mb-4" />
<h2 style={{ marginBottom: "0.5rem" }}>Import via Excel</h2>
<p className="text-muted mb-4">Upload an .xlsx file to bulk import inventory.</p>
<label className="btn btn-secondary cursor-pointer" style={{ cursor: "pointer" }}>
Select Excel File
<input type="file" accept=".xlsx, .xls, .csv" style={{ display: "none" }} onChange={handleFileUpload} disabled={loading} />
</label>
<div className="mt-4" style={{ fontSize: "0.85rem", color: "var(--text-muted)" }}>
Make sure your file has headers like: <strong>Name, QR ID, Quantity, Price</strong>.
</div>
</div>
{/* Status Reporting */}
{status.message && (
<div className="glass flex items-center gap-4" style={{ padding: "1.5rem", borderLeft: `4px solid ${status.type === 'error' ? 'var(--danger)' : 'var(--success)'}` }}>
{status.type === 'error' ? <AlertCircle className="text-danger" size={32} /> : <CheckCircle2 className="text-success" size={32} />}
<div>
<h4 style={{ color: status.type === 'error' ? 'var(--danger)' : 'var(--success)' }}>
{status.type === 'error' ? 'Error' : 'Success'}
</h4>
<p className="text-muted" style={{ fontSize: "0.95rem" }}>{status.message}</p>
{importStats && (
<div style={{ marginTop: "0.5rem", fontSize: "0.85rem" }}>
<p>Total Items: {importStats.total}</p>
<p className="text-success">Successful: {importStats.successCount}</p>
<p className="text-danger">Failed/Skipped: {importStats.errorCount}</p>
</div>
)}
</div>
</div>
)}
</div>
<style>{`
@media (max-width: 900px) {
div[style*="grid-template-columns: 1fr 1fr"] {
grid-template-columns: 1fr !important;
}
}
`}</style>
</div>
);
}