diff --git a/components/Inventory.tsx b/components/Inventory.tsx index 6cafc53..dd0278c 100644 --- a/components/Inventory.tsx +++ b/components/Inventory.tsx @@ -1,5 +1,5 @@ import { useState, useRef, useEffect } from "react"; -import * as XLSX from "xlsx"; +import ExcelJS from "exceljs"; import { chemicalsApi } from "../lib/api"; import type { ChemicalInventory } from "../shared/types"; import { Card } from "./ui/card"; @@ -282,15 +282,19 @@ export function Inventory() { URL.revokeObjectURL(url); } - function handleDownloadExcel() { - const wsData = [ - TABLE_COLUMNS.map(c => c.label), - ...filtered.map(item => TABLE_COLUMNS.map(c => item[c.key] ?? "")), - ]; - const ws = XLSX.utils.aoa_to_sheet(wsData); - const wb = XLSX.utils.book_new(); - XLSX.utils.book_append_sheet(wb, ws, "Chemical Inventory"); - XLSX.writeFile(wb, "chemical_inventory.xlsx"); + async function handleDownloadExcel() { + const wb = new ExcelJS.Workbook(); + const ws = wb.addWorksheet("Chemical Inventory"); + ws.addRow(TABLE_COLUMNS.map(c => c.label)); + filtered.forEach(item => ws.addRow(TABLE_COLUMNS.map(c => item[c.key] ?? ""))); + const buffer = await wb.xlsx.writeBuffer(); + const blob = new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "chemical_inventory.xlsx"; + a.click(); + URL.revokeObjectURL(url); } // ── derived ───────────────────────────────────────────────────────────── @@ -622,7 +626,7 @@ export function Inventory() { -
+
diff --git a/package.json b/package.json index baaea8f..b161a7d 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "cmdk": "^1.1.1", "dotenv": "^17.3.1", "embla-carousel-react": "^8.6.0", + "exceljs": "^4.4.0", "input-otp": "^1.4.2", "lucide-react": "^0.487.0", "next-themes": "^0.4.6", @@ -53,8 +54,7 @@ "recharts": "^2.15.2", "sonner": "^2.0.3", "tailwind-merge": "^2.6.0", - "vaul": "^1.1.2", - "xlsx": "^0.18.5" + "vaul": "^1.1.2" }, "devDependencies": { "@tailwindcss/vite": "^4.0.0", diff --git a/styles/globals.css b/styles/globals.css index 9030b3f..83bdb17 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -197,4 +197,92 @@ html { font-size: var(--font-size); +} + +.table-scroll { + overflow: scroll; + flex: 1; +} + +.table-scroll::-webkit-scrollbar { + width: 14px; + height: 14px; +} + +.table-scroll::-webkit-scrollbar-track { + background: var(--muted); + border: 1px solid var(--border); +} + +.table-scroll::-webkit-scrollbar-thumb { + background: var(--muted-foreground); + border: 1px solid var(--border); + border-radius: 999px; + min-width: 30px; + min-height: 30px; +} + +.table-scroll::-webkit-scrollbar-thumb:hover { + background: var(--foreground); +} + +/* Hide all buttons by default, then selectively show the correct one per end */ +.table-scroll::-webkit-scrollbar-button { + display: none; +} + +/* Up arrow — top of vertical bar */ +.table-scroll::-webkit-scrollbar-button:vertical:decrement:start { + display: block; + background: var(--muted); + border: 1px solid var(--border); + border-radius: 999px 999px 0 0; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath d='M4 2 L7 6 L1 6 Z' fill='%23666'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center; + background-size: 8px 8px; +} + +/* Down arrow — bottom of vertical bar */ +.table-scroll::-webkit-scrollbar-button:vertical:increment:end { + display: block; + background: var(--muted); + border: 1px solid var(--border); + border-radius: 0 0 999px 999px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath d='M4 6 L7 2 L1 2 Z' fill='%23666'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center; + background-size: 8px 8px; +} + +/* Left arrow — left end of horizontal bar */ +.table-scroll::-webkit-scrollbar-button:horizontal:decrement:start { + display: block; + background: var(--muted); + border: 1px solid var(--border); + border-radius: 999px 0 0 999px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath d='M2 4 L6 1 L6 7 Z' fill='%23666'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center; + background-size: 8px 8px; +} + +/* Right arrow — right end of horizontal bar */ +.table-scroll::-webkit-scrollbar-button:horizontal:increment:end { + display: block; + background: var(--muted); + border: 1px solid var(--border); + border-radius: 0 999px 999px 0; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath d='M6 4 L2 1 L2 7 Z' fill='%23666'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center; + background-size: 8px 8px; +} + +.table-scroll::-webkit-scrollbar-button:hover { + background: var(--border); +} + +.table-scroll::-webkit-scrollbar-corner { + background: transparent; } \ No newline at end of file