215 lines
8.6 KiB
JavaScript
215 lines
8.6 KiB
JavaScript
"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: "", location: "", 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: "", location: "", 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"] || "",
|
|
location: normalizedRow["location"] || normalizedRow["bin"] || normalizedRow["aisle"] || "",
|
|
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 style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}>
|
|
<div className="form-group">
|
|
<label>Category</label>
|
|
<input type="text" name="category" className="input-field" value={formData.category} onChange={handleInputChange} />
|
|
</div>
|
|
<div className="form-group">
|
|
<label>Location</label>
|
|
<input type="text" name="location" className="input-field" value={formData.location} onChange={handleInputChange} placeholder="e.g. Aisle 4" />
|
|
</div>
|
|
</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, Location</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>
|
|
);
|
|
}
|