diff --git a/app/globals.css b/app/globals.css index 3028556..f949a13 100644 --- a/app/globals.css +++ b/app/globals.css @@ -267,3 +267,52 @@ ul { .mt-4 { margin-top: 1rem; } .mb-4 { margin-bottom: 1rem; } .text-center { text-align: center; } + +/* Print Styles */ +@media print { + /* Remove scroll contexts to allow thead to repeat */ + .table-container { + overflow: visible !important; + overflow-x: visible !important; + border: none !important; + } + + thead { + display: table-header-group; + } + + tr { + page-break-inside: avoid; + break-inside: avoid; + } + + .glass { + box-shadow: none !important; + border: none !important; + background: transparent !important; + } + + /* Hide UI elements unnecessary for print */ + .btn, .input-field, .navbar { + display: none !important; + } + + body { + background-color: white !important; + color: black !important; + } + + .data-table th { + color: black !important; + border-bottom: 2px solid black !important; + } + + .data-table td { + border-bottom: 1px solid #ccc !important; + } + + .text-muted { + color: #555 !important; + } +} + diff --git a/app/print/page.js b/app/print/page.js index b60b41e..9e5957a 100644 --- a/app/print/page.js +++ b/app/print/page.js @@ -2,11 +2,13 @@ import { useEffect, useState } from "react"; import QRCode from "react-qr-code"; -import { Printer, AlertCircle } from "lucide-react"; +import { Printer, AlertCircle, Filter, Search, Tag, CheckCircle2 } from "lucide-react"; export default function PrintPage() { const [items, setItems] = useState([]); const [selectedIds, setSelectedIds] = useState(new Set()); + const [selectedCategory, setSelectedCategory] = useState(""); + const [searchQuery, setSearchQuery] = useState(""); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -27,6 +29,21 @@ export default function PrintPage() { fetchItems(); }, []); + const uniqueCategories = [ + ...new Set(items.map(item => item.category?.trim() || "Uncategorized")) + ].filter(Boolean).sort(); + + const filteredItems = items.filter(item => { + const itemCat = item.category?.trim() || "Uncategorized"; + const matchesCategory = selectedCategory === "" || itemCat === selectedCategory; + const matchesSearch = searchQuery === "" || + item.name?.toLowerCase().includes(searchQuery.toLowerCase()) || + (item.qrCodeId && item.qrCodeId.toLowerCase().includes(searchQuery.toLowerCase())); + return matchesCategory && matchesSearch; + }); + + const isAllFilteredSelected = filteredItems.length > 0 && filteredItems.every(i => selectedIds.has(i.id)); + const handlePrint = () => { if (selectedIds.size === 0) return alert("Please select at least one label to print."); window.print(); @@ -39,12 +56,32 @@ export default function PrintPage() { setSelectedIds(next); }; - const toggleAll = () => { - if (selectedIds.size === items.length) setSelectedIds(new Set()); - else setSelectedIds(new Set(items.map(i => i.id))); + const toggleFilteredAll = () => { + const next = new Set(selectedIds); + if (isAllFilteredSelected) { + filteredItems.forEach(i => next.delete(i.id)); + } else { + filteredItems.forEach(i => next.add(i.id)); + } + setSelectedIds(next); + }; + + const selectOnlyCurrentFiltered = () => { + const next = new Set(); + filteredItems.forEach(i => next.add(i.id)); + setSelectedIds(next); + }; + + const selectAll = () => { + setSelectedIds(new Set(items.map(i => i.id))); + }; + + const deselectAll = () => { + setSelectedIds(new Set()); }; const itemsToPrint = items.filter(i => selectedIds.has(i.id)); + const sheetCount = Math.ceil(itemsToPrint.length / 30); if (loading) return
Loading inventory codes...
; if (error) return
Error: {error}
; @@ -52,8 +89,8 @@ export default function PrintPage() { return ( <> {/* On-screen Controls (Hidden in Print) */} -
-
+
+

@@ -63,10 +100,21 @@ export default function PrintPage() { Designed for Avery 5160 standard labels (1" x 2-5/8", 30 per sheet).

- +
+
+
0 ? "var(--text-main)" : "var(--danger)" }}> + {itemsToPrint.length} of {items.length} selected +
+
+ {sheetCount} {sheetCount === 1 ? "sheet" : "sheets"} required +
+
+ +
+
@@ -81,73 +129,245 @@ export default function PrintPage() {
+ {/* Filter and Category Selection Toolbar (Hidden in Print) */} +
+
+
+ +

Filter & Select Labels

+
+
+ {selectedCategory && ( + + )} + + +
+
+ + {/* Filter Inputs */} +
+
+ + setSearchQuery(e.target.value)} + style={{ width: "100%", paddingLeft: "36px" }} + /> +
+ +
+ + +
+
+ + {/* Quick Category Badges */} + {uniqueCategories.length > 1 && ( +
+ Quick Category Filter: + + {uniqueCategories.map(cat => { + const count = items.filter(i => (i.category?.trim() || "Uncategorized") === cat).length; + const isSelected = selectedCategory === cat; + return ( + + ); + })} +
+ )} +
+ {/* Item Selection Table (Hidden in Print) */}
-
-

Select Labels to Print ({selectedIds.size} selected)

-
-
+ +
+ + - {items.map(item => ( - toggleSelection(item.id)}> - + - - - - ))} + ) : ( + filteredItems.map(item => { + const isSelected = selectedIds.has(item.id); + return ( + toggleSelection(item.id)} + > + + + + + + + + ); + }) + )}
0} - onChange={toggleAll} + checked={isAllFilteredSelected} + onChange={toggleFilteredAll} /> Name QR ID CategoryLocationStatus
- {}} /* Handled by row click */ - /> + {filteredItems.length === 0 ? ( +
+ No items found matching the selected filters. {item.name}{item.qrCodeId}{item.category || "-"}
e.stopPropagation()}> + toggleSelection(item.id)} + /> + {item.name}{item.qrCodeId} + + {item.category || "Uncategorized"} + + {item.location || "-"} + {isSelected ? ( + + To Print + + ) : ( + + Excluded + + )} +
{/* Printable Sheet Area */} - {/* Printable Sheet Area */} - {Array.from({ length: Math.ceil(itemsToPrint.length / 30) }, (_, pageIndex) => ( -
- {itemsToPrint.slice(pageIndex * 30, (pageIndex + 1) * 30).map((item) => ( -
-
- -
-
-
{item.name}
-
{item.qrCodeId}
-
-
- ))} + {itemsToPrint.length === 0 ? ( +
+

No labels selected to print. Select items above to generate printable sheets.

- ))} - + ) : ( + Array.from({ length: Math.ceil(itemsToPrint.length / 30) }, (_, pageIndex) => ( +
+ {itemsToPrint.slice(pageIndex * 30, (pageIndex + 1) * 30).map((item) => ( +
+
+ +
+
+
{item.name}
+
{item.qrCodeId}
+ {item.category &&
{item.category}
} +
+
+ ))} +
+ )) + )}