added location field
This commit is contained in:
@ -5,7 +5,7 @@ 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 [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);
|
||||
@ -37,7 +37,7 @@ export default function AddImportPage() {
|
||||
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: "" });
|
||||
setFormData({ name: "", qrCodeId: "", quantity: 0, price: 0.0, category: "", location: "", description: "" });
|
||||
} catch (err) {
|
||||
setStatus({ type: "error", message: err.message });
|
||||
} finally {
|
||||
@ -81,6 +81,7 @@ export default function AddImportPage() {
|
||||
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"] || "",
|
||||
};
|
||||
|
||||
@ -147,10 +148,16 @@ export default function AddImportPage() {
|
||||
</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"}
|
||||
@ -170,7 +177,7 @@ export default function AddImportPage() {
|
||||
<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>.
|
||||
Make sure your file has headers like: <strong>Name, QR ID, Quantity, Price, Location</strong>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ export async function GET() {
|
||||
export async function POST(req) {
|
||||
try {
|
||||
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({
|
||||
where: { qrCodeId },
|
||||
@ -26,6 +26,7 @@ export async function POST(req) {
|
||||
quantity: quantity || 0,
|
||||
price: price || 0.0,
|
||||
category: category || "",
|
||||
location: location || "",
|
||||
description: description || "",
|
||||
},
|
||||
create: {
|
||||
@ -34,6 +35,7 @@ export async function POST(req) {
|
||||
quantity: quantity || 0,
|
||||
price: price || 0.0,
|
||||
category: category || "",
|
||||
location: location || "",
|
||||
description: description || "",
|
||||
},
|
||||
});
|
||||
|
||||
104
app/page.js
104
app/page.js
@ -11,6 +11,11 @@ export default function Dashboard() {
|
||||
const [editingId, setEditingId] = useState(null);
|
||||
const [editForm, setEditForm] = useState({});
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedCategory, setSelectedCategory] = useState("");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const itemsPerPage = 10;
|
||||
|
||||
const fetchItems = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@ -46,16 +51,18 @@ export default function Dashboard() {
|
||||
|
||||
const startEdit = (item) => {
|
||||
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) => {
|
||||
try {
|
||||
const parsedQty = parseInt(editForm.quantity, 10);
|
||||
const payload = {
|
||||
id,
|
||||
name: editForm.name,
|
||||
category: editForm.category,
|
||||
quantity: parseInt(editForm.quantity) || 0,
|
||||
location: editForm.location,
|
||||
quantity: isNaN(parsedQty) ? 0 : parsedQty,
|
||||
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 = () => {
|
||||
if (items.length === 0) return alert("No items to export");
|
||||
const ws = xlsx.utils.json_to_sheet(items.map(i => ({
|
||||
if (filteredItems.length === 0) return alert("No items to export");
|
||||
const ws = xlsx.utils.json_to_sheet(filteredItems.map(i => ({
|
||||
"QR ID": i.qrCodeId,
|
||||
"Name": i.name,
|
||||
"Quantity": i.quantity,
|
||||
"Price": i.price,
|
||||
"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();
|
||||
xlsx.utils.book_append_sheet(wb, ws, "Inventory");
|
||||
@ -133,19 +160,49 @@ export default function Dashboard() {
|
||||
</div>
|
||||
) : (
|
||||
<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">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>QR ID</th>
|
||||
<th>Category</th>
|
||||
<th>Location</th>
|
||||
<th>Quantity</th>
|
||||
<th>Price</th>
|
||||
<th>Last Updated</th>
|
||||
<th style={{ textAlign: "right" }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<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}>
|
||||
<td style={{ fontWeight: 500 }}>
|
||||
{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})} />
|
||||
) : (item.category || "-")}
|
||||
</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>
|
||||
{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})} />
|
||||
@ -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})} />
|
||||
) : `$${Number(item.price).toFixed(2)}`}
|
||||
</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" }}>
|
||||
<div className="flex" style={{ gap: "0.5rem", justifyContent: "flex-end" }}>
|
||||
{editingId === item.id ? (
|
||||
@ -199,6 +264,33 @@ export default function Dashboard() {
|
||||
))}
|
||||
</tbody>
|
||||
</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>
|
||||
|
||||
@ -65,8 +65,10 @@ export default function PrintPage() {
|
||||
</div>
|
||||
|
||||
{/* Printable Sheet Area */}
|
||||
<div className="print-container">
|
||||
{items.map((item, index) => (
|
||||
{/* Printable Sheet Area */}
|
||||
{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 className="qr-wrapper">
|
||||
<QRCode
|
||||
@ -83,6 +85,8 @@ export default function PrintPage() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
<style>{`
|
||||
/* Standard View Adjustments */
|
||||
@ -125,7 +129,7 @@ export default function PrintPage() {
|
||||
|
||||
.item-name {
|
||||
font-weight: bold;
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.55rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@ -147,6 +151,8 @@ export default function PrintPage() {
|
||||
body {
|
||||
background-color: white !important;
|
||||
-webkit-print-color-adjust: exact;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.no-print, nav, .navbar {
|
||||
@ -159,12 +165,13 @@ export default function PrintPage() {
|
||||
grid-template-columns: repeat(3, 2.625in);
|
||||
grid-template-rows: repeat(10, 1in);
|
||||
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;
|
||||
height: 11in;
|
||||
background: white;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
break-after:page;
|
||||
}
|
||||
|
||||
.print-container {
|
||||
|
||||
@ -150,11 +150,19 @@ export default function ScanPage() {
|
||||
<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 style={{ fontSize: "2rem", fontWeight: "bold", color: "var(--text-main)" }}>{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 className="text-muted" style={{ fontSize: "0.85rem", textTransform: "uppercase" }}>Location</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>
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ export default function Navigation() {
|
||||
|
||||
return (
|
||||
<nav className="navbar glass">
|
||||
<div className="nav-brand">InventoryPro</div>
|
||||
<div className="nav-brand">Simpson's Fence Inventory</div>
|
||||
<ul className="nav-links">
|
||||
{navItems.map((item) => (
|
||||
<li key={item.href}>
|
||||
|
||||
@ -18,6 +18,7 @@ model InventoryItem {
|
||||
quantity Int @default(0)
|
||||
price Float @default(0.0)
|
||||
category String?
|
||||
location String?
|
||||
description String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
BIN
public/Logo.jpg
Normal file
BIN
public/Logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
Reference in New Issue
Block a user