added location field

This commit is contained in:
2026-04-13 15:54:00 -04:00
parent 9b746388fd
commit 6f37ca38b5
8 changed files with 154 additions and 37 deletions

View File

@ -5,7 +5,7 @@ import { Upload, Plus, AlertCircle, CheckCircle2 } from "lucide-react";
import * as xlsx from "xlsx"; import * as xlsx from "xlsx";
export default function AddImportPage() { export default function AddImportPage() {
const [formData, setFormData] = useState({ name: "", qrCodeId: "", quantity: 0, price: 0.0, category: "", description: "" }); const [formData, setFormData] = useState({ name: "", qrCodeId: "", quantity: 0, price: 0.0, category: "", location: "", description: "" });
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [status, setStatus] = useState({ type: "", message: "" }); const [status, setStatus] = useState({ type: "", message: "" });
const [importStats, setImportStats] = useState(null); const [importStats, setImportStats] = useState(null);
@ -37,7 +37,7 @@ export default function AddImportPage() {
if (!res.ok) throw new Error(data.error || "Failed to create item"); if (!res.ok) throw new Error(data.error || "Failed to create item");
setStatus({ type: "success", message: `Successfully added ${data.name}.` }); setStatus({ type: "success", message: `Successfully added ${data.name}.` });
setFormData({ name: "", qrCodeId: "", quantity: 0, price: 0.0, category: "", description: "" }); setFormData({ name: "", qrCodeId: "", quantity: 0, price: 0.0, category: "", location: "", description: "" });
} catch (err) { } catch (err) {
setStatus({ type: "error", message: err.message }); setStatus({ type: "error", message: err.message });
} finally { } finally {
@ -81,6 +81,7 @@ export default function AddImportPage() {
quantity: parseInt(normalizedRow["quantity"] || normalizedRow["qty"]) || 0, quantity: parseInt(normalizedRow["quantity"] || normalizedRow["qty"]) || 0,
price: parseFloat(normalizedRow["price"] || normalizedRow["cost"]) || 0.0, price: parseFloat(normalizedRow["price"] || normalizedRow["cost"]) || 0.0,
category: normalizedRow["category"] || "", category: normalizedRow["category"] || "",
location: normalizedRow["location"] || normalizedRow["bin"] || normalizedRow["aisle"] || "",
description: normalizedRow["description"] || "", description: normalizedRow["description"] || "",
}; };
@ -147,10 +148,16 @@ export default function AddImportPage() {
</div> </div>
</div> </div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}>
<div className="form-group"> <div className="form-group">
<label>Category</label> <label>Category</label>
<input type="text" name="category" className="input-field" value={formData.category} onChange={handleInputChange} /> <input type="text" name="category" className="input-field" value={formData.category} onChange={handleInputChange} />
</div> </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}> <button type="submit" className="btn btn-primary mt-4" style={{ width: "100%", justifyContent: "center" }} disabled={loading}>
{loading ? "Saving..." : "Add to Inventory"} {loading ? "Saving..." : "Add to Inventory"}
@ -170,7 +177,7 @@ export default function AddImportPage() {
<input type="file" accept=".xlsx, .xls, .csv" style={{ display: "none" }} onChange={handleFileUpload} disabled={loading} /> <input type="file" accept=".xlsx, .xls, .csv" style={{ display: "none" }} onChange={handleFileUpload} disabled={loading} />
</label> </label>
<div className="mt-4" style={{ fontSize: "0.85rem", color: "var(--text-muted)" }}> <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>. Make sure your file has headers like: <strong>Name, QR ID, Quantity, Price, Location</strong>.
</div> </div>
</div> </div>

View File

@ -17,7 +17,7 @@ export async function GET() {
export async function POST(req) { export async function POST(req) {
try { try {
const body = await req.json(); const body = await req.json();
const { qrCodeId, name, quantity, price, category, description } = body; const { qrCodeId, name, quantity, price, category, location, description } = body;
const newItem = await prisma.inventoryItem.upsert({ const newItem = await prisma.inventoryItem.upsert({
where: { qrCodeId }, where: { qrCodeId },
@ -26,6 +26,7 @@ export async function POST(req) {
quantity: quantity || 0, quantity: quantity || 0,
price: price || 0.0, price: price || 0.0,
category: category || "", category: category || "",
location: location || "",
description: description || "", description: description || "",
}, },
create: { create: {
@ -34,6 +35,7 @@ export async function POST(req) {
quantity: quantity || 0, quantity: quantity || 0,
price: price || 0.0, price: price || 0.0,
category: category || "", category: category || "",
location: location || "",
description: description || "", description: description || "",
}, },
}); });

View File

@ -11,6 +11,11 @@ export default function Dashboard() {
const [editingId, setEditingId] = useState(null); const [editingId, setEditingId] = useState(null);
const [editForm, setEditForm] = useState({}); const [editForm, setEditForm] = useState({});
const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 10;
const fetchItems = async () => { const fetchItems = async () => {
setLoading(true); setLoading(true);
setError(null); setError(null);
@ -46,16 +51,18 @@ export default function Dashboard() {
const startEdit = (item) => { const startEdit = (item) => {
setEditingId(item.id); setEditingId(item.id);
setEditForm({ name: item.name, category: item.category || "", quantity: item.quantity, price: item.price }); setEditForm({ name: item.name, category: item.category || "", location: item.location || "", quantity: item.quantity, price: item.price });
}; };
const saveEdit = async (id) => { const saveEdit = async (id) => {
try { try {
const parsedQty = parseInt(editForm.quantity, 10);
const payload = { const payload = {
id, id,
name: editForm.name, name: editForm.name,
category: editForm.category, category: editForm.category,
quantity: parseInt(editForm.quantity) || 0, location: editForm.location,
quantity: isNaN(parsedQty) ? 0 : parsedQty,
price: parseFloat(editForm.price) || 0.0, price: parseFloat(editForm.price) || 0.0,
}; };
@ -78,15 +85,35 @@ export default function Dashboard() {
}; };
// Filter and Paginate Items
useEffect(() => {
setCurrentPage(1);
}, [searchQuery, selectedCategory]);
const uniqueCategories = [...new Set(items.map(item => item.category?.trim() || "Uncategorized"))].filter(c => c).sort();
const filteredItems = items.filter(item => {
const matchesSearch = item.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.qrCodeId.toLowerCase().includes(searchQuery.toLowerCase());
const matchesCategory = selectedCategory === "" || (item.category?.trim() || "Uncategorized") === selectedCategory;
return matchesSearch && matchesCategory;
});
const totalPages = Math.max(1, Math.ceil(filteredItems.length / itemsPerPage));
const startIndex = (currentPage - 1) * itemsPerPage;
const paginatedItems = filteredItems.slice(startIndex, startIndex + itemsPerPage);
const handleExport = () => { const handleExport = () => {
if (items.length === 0) return alert("No items to export"); if (filteredItems.length === 0) return alert("No items to export");
const ws = xlsx.utils.json_to_sheet(items.map(i => ({ const ws = xlsx.utils.json_to_sheet(filteredItems.map(i => ({
"QR ID": i.qrCodeId, "QR ID": i.qrCodeId,
"Name": i.name, "Name": i.name,
"Quantity": i.quantity, "Quantity": i.quantity,
"Price": i.price, "Price": i.price,
"Category": i.category || "", "Category": i.category || "",
"Description": i.description || "" "Location": i.location || "",
"Description": i.description || "",
"Last Updated": i.updatedAt ? new Date(i.updatedAt).toLocaleString() : ""
}))); })));
const wb = xlsx.utils.book_new(); const wb = xlsx.utils.book_new();
xlsx.utils.book_append_sheet(wb, ws, "Inventory"); xlsx.utils.book_append_sheet(wb, ws, "Inventory");
@ -133,19 +160,49 @@ export default function Dashboard() {
</div> </div>
) : ( ) : (
<div className="table-container mt-4"> <div className="table-container mt-4">
<div className="flex gap-4 mb-4" style={{ alignItems: "center" }}>
<input
type="text"
placeholder="Search by name or QR..."
className="input-field"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
style={{ flex: 1 }}
/>
<select
className="input-field"
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
style={{ width: "200px" }}
>
<option value="">All Categories</option>
{uniqueCategories.map(cat => (
<option key={cat} value={cat}>{cat}</option>
))}
</select>
</div>
<table className="data-table"> <table className="data-table">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>QR ID</th> <th>QR ID</th>
<th>Category</th> <th>Category</th>
<th>Location</th>
<th>Quantity</th> <th>Quantity</th>
<th>Price</th> <th>Price</th>
<th>Last Updated</th>
<th style={{ textAlign: "right" }}>Actions</th> <th style={{ textAlign: "right" }}>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{items.map(item => ( {paginatedItems.length === 0 ? (
<tr>
<td colSpan="8" style={{ textAlign: "center", padding: "2rem", color: "var(--text-muted)" }}>
No items match your filters.
</td>
</tr>
) : paginatedItems.map(item => (
<tr key={item.id}> <tr key={item.id}>
<td style={{ fontWeight: 500 }}> <td style={{ fontWeight: 500 }}>
{editingId === item.id ? ( {editingId === item.id ? (
@ -158,6 +215,11 @@ export default function Dashboard() {
<input className="input-field" style={{ padding: "0.2rem 0.5rem", width: "100%" }} value={editForm.category} onChange={e => setEditForm({...editForm, category: e.target.value})} /> <input className="input-field" style={{ padding: "0.2rem 0.5rem", width: "100%" }} value={editForm.category} onChange={e => setEditForm({...editForm, category: e.target.value})} />
) : (item.category || "-")} ) : (item.category || "-")}
</td> </td>
<td>
{editingId === item.id ? (
<input className="input-field" style={{ padding: "0.2rem 0.5rem", width: "100%" }} value={editForm.location} onChange={e => setEditForm({...editForm, location: e.target.value})} />
) : (item.location || "-")}
</td>
<td> <td>
{editingId === item.id ? ( {editingId === item.id ? (
<input type="number" className="input-field" style={{ padding: "0.2rem 0.5rem", width: "80px" }} value={editForm.quantity} onChange={e => setEditForm({...editForm, quantity: e.target.value})} /> <input type="number" className="input-field" style={{ padding: "0.2rem 0.5rem", width: "80px" }} value={editForm.quantity} onChange={e => setEditForm({...editForm, quantity: e.target.value})} />
@ -172,6 +234,9 @@ export default function Dashboard() {
<input type="number" step="0.01" className="input-field" style={{ padding: "0.2rem 0.5rem", width: "80px" }} value={editForm.price} onChange={e => setEditForm({...editForm, price: e.target.value})} /> <input type="number" step="0.01" className="input-field" style={{ padding: "0.2rem 0.5rem", width: "80px" }} value={editForm.price} onChange={e => setEditForm({...editForm, price: e.target.value})} />
) : `$${Number(item.price).toFixed(2)}`} ) : `$${Number(item.price).toFixed(2)}`}
</td> </td>
<td className="text-muted" style={{ fontSize: "0.85rem" }}>
{item.updatedAt ? new Date(item.updatedAt).toLocaleDateString() + " " + new Date(item.updatedAt).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) : "N/A"}
</td>
<td style={{ textAlign: "right" }}> <td style={{ textAlign: "right" }}>
<div className="flex" style={{ gap: "0.5rem", justifyContent: "flex-end" }}> <div className="flex" style={{ gap: "0.5rem", justifyContent: "flex-end" }}>
{editingId === item.id ? ( {editingId === item.id ? (
@ -199,6 +264,33 @@ export default function Dashboard() {
))} ))}
</tbody> </tbody>
</table> </table>
{filteredItems.length > 0 && (
<div className="flex justify-between items-center mt-4" style={{ padding: "1rem 0 0 0" }}>
<span className="text-muted" style={{ fontSize: "0.9rem" }}>
Showing {startIndex + 1} to {Math.min(startIndex + itemsPerPage, filteredItems.length)} of {filteredItems.length} entries
</span>
<div className="flex gap-2">
<button
className="btn btn-secondary"
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
>
Previous
</button>
<span className="flex items-center text-muted" style={{ padding: "0 0.5rem", fontSize: "0.9rem" }}>
Page {currentPage} of {totalPages}
</span>
<button
className="btn btn-secondary"
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
>
Next
</button>
</div>
</div>
)}
</div> </div>
)} )}
</div> </div>

View File

@ -65,8 +65,10 @@ export default function PrintPage() {
</div> </div>
{/* Printable Sheet Area */} {/* Printable Sheet Area */}
<div className="print-container"> {/* Printable Sheet Area */}
{items.map((item, index) => ( {Array.from({ length: Math.ceil(items.length / 30) }, (_, pageIndex) => (
<div key={pageIndex} className="print-container">
{items.slice(pageIndex * 30, (pageIndex + 1) * 30).map((item) => (
<div key={item.id} className="label-cell"> <div key={item.id} className="label-cell">
<div className="qr-wrapper"> <div className="qr-wrapper">
<QRCode <QRCode
@ -83,6 +85,8 @@ export default function PrintPage() {
</div> </div>
))} ))}
</div> </div>
))}
<style>{` <style>{`
/* Standard View Adjustments */ /* Standard View Adjustments */
@ -125,7 +129,7 @@ export default function PrintPage() {
.item-name { .item-name {
font-weight: bold; font-weight: bold;
font-size: 0.75rem; font-size: 0.55rem;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -147,6 +151,8 @@ export default function PrintPage() {
body { body {
background-color: white !important; background-color: white !important;
-webkit-print-color-adjust: exact; -webkit-print-color-adjust: exact;
margin: 0;
padding: 0;
} }
.no-print, nav, .navbar { .no-print, nav, .navbar {
@ -159,12 +165,13 @@ export default function PrintPage() {
grid-template-columns: repeat(3, 2.625in); grid-template-columns: repeat(3, 2.625in);
grid-template-rows: repeat(10, 1in); grid-template-rows: repeat(10, 1in);
gap: 0; gap: 0;
padding: 0.5in 0.21975in; /* Top/Bottom 0.5", Left/Right approx 0.22" */ padding: 0.5in 0.1250in; /* Top/Bottom 0.5", Left/Right approx 0.1250" */
width: 8.5in; width: 8.5in;
height: 11in; height: 11in;
background: white; background: white;
border: none; border: none;
border-radius: 0; border-radius: 0;
break-after:page;
} }
.print-container { .print-container {

View File

@ -150,11 +150,19 @@ export default function ScanPage() {
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem", marginBottom: "2rem" }}> <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 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 className="text-muted" style={{ fontSize: "0.85rem", textTransform: "uppercase" }}>Current Quant.</div>
<div style={{ fontSize: "2rem", fontWeight: "bold" }}>{item.quantity}</div> <div style={{ fontSize: "2rem", fontWeight: "bold", color: "var(--text-main)" }}>{item.quantity}</div>
</div> </div>
<div style={{ padding: "1rem", backgroundColor: "rgba(255,255,255,0.05)", borderRadius: "8px" }}> <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 className="text-muted" style={{ fontSize: "0.85rem", textTransform: "uppercase" }}>Location</div>
<div style={{ fontSize: "2rem", fontWeight: "bold" }}>${Number(item.price).toFixed(2)}</div> <div style={{ fontSize: "1.4rem", fontWeight: "600", color: "var(--text-main)", marginTop: "0.3rem" }}>
{item.location || "-"}
</div>
</div>
<div style={{ padding: "1rem", backgroundColor: "rgba(255,255,255,0.05)", borderRadius: "8px", gridColumn: "1 / -1" }}>
<div className="text-muted" style={{ fontSize: "0.85rem", textTransform: "uppercase", marginBottom: "0.5rem" }}>Last Updated</div>
<div style={{ fontSize: "1.1rem", color: "var(--text-main)" }}>
{item.updatedAt ? new Date(item.updatedAt).toLocaleString() : "Never"}
</div>
</div> </div>
</div> </div>

View File

@ -16,7 +16,7 @@ export default function Navigation() {
return ( return (
<nav className="navbar glass"> <nav className="navbar glass">
<div className="nav-brand">InventoryPro</div> <div className="nav-brand">Simpson's Fence Inventory</div>
<ul className="nav-links"> <ul className="nav-links">
{navItems.map((item) => ( {navItems.map((item) => (
<li key={item.href}> <li key={item.href}>

View File

@ -18,6 +18,7 @@ model InventoryItem {
quantity Int @default(0) quantity Int @default(0)
price Float @default(0.0) price Float @default(0.0)
category String? category String?
location String?
description String? description String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt

BIN
public/Logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB