"use client"; import { useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import { QrCode, CheckCircle2, XCircle, TrendingUp, Calendar, Search, Eye, Download, RefreshCw, MoreVertical, Archive, Power, Trash2, Plus, } from "lucide-react"; import { api, apiBase } from "@/lib/api"; import QrDetailDrawer from "@/components/QrDetailDrawer"; export default function QRCenter() { const router = useRouter(); const [rows, setRows] = useState([]); const [summary, setSummary] = useState(null); const [brands, setBrands] = useState([]); const [products, setProducts] = useState([]); const [filters, setFilters] = useState({ search: "", product_id: "", brand_id: "", status_filter: "", qr_type: "" ,start_date: "", end_date: ""}); const [selected, setSelected] = useState>(new Set()); const [openId, setOpenId] = useState(null); const [menuFor, setMenuFor] = useState(null); const [page, setPage] = useState(1); const pageSize = 25; const [showProductPicker, setShowProductPicker] = useState(false); const [productSearch, setProductSearch] = useState(""); const [brandFilter, setBrandFilter] = useState(""); const filteredProducts = products.filter((p) => { const matchSearch = p.name.toLowerCase().includes(productSearch.toLowerCase()) || (p.sku ?? "").toLowerCase().includes(productSearch.toLowerCase()); const matchBrand = !brandFilter || p.brand_id === brandFilter; return matchSearch && matchBrand; }); function load() { const qs = new URLSearchParams(Object.entries(filters).filter(([, v]) => v) as any).toString(); api(`/qr${qs ? "?" + qs : ""}`).then(setRows).catch(() => setRows([])); api("/qr/summary").then(setSummary).catch(() => {}); } useEffect(() => { load(); }, [filters]); useEffect(() => { api("/brands").then(setBrands).catch(() => {}); api("/products").then((d) => setProducts(d.items ?? [])).catch(() => {}); }, []); function handleExport() { const exportRows = selected.size > 0 ? rows.filter((r) => selected.has(r.id)) : rows; const headers = [ "QR Code", "Product", "SKU", "Brand", "Batch Number", "Serial", "QR Type", "Status", "Scan Count", "Last Scan", "Last Scan Location", "Created By", "Created Date", "Verification URL", "QR Image", ]; const csv = [ headers.join(","), ...exportRows.map((r) => [ `"${r.code ?? ""}"`, `"${r.product_name ?? ""}"`, `"${r.sku ?? ""}"`, `"${r.brand_name ?? ""}"`, `"${r.batch_number ?? ""}"`, `"${r.serial ?? ""}"`, `"${r.qr_type ?? ""}"`, `"${r.status ?? ""}"`, `"${r.scan_count ?? 0}"`, `"${r.last_scan_at ? new Date(r.last_scan_at).toLocaleString("en-IN") : ""}"`, `"${r.last_scan_location ?? ""}"`, `"${r.created_by ?? ""}"`, `"${r.created_at ? new Date(r.created_at).toLocaleString("en-IN") : ""}"`, `"${r.url ?? ""}"`, `"${r.image_url ? apiBase + r.image_url : apiBase + `/qr/${r.id}/download?format=png`}"`, ].join(",") ), ].join("\n"); const blob = new Blob([csv], { type: "text/csv;charset=utf-8;", }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = selected.size > 0 ? `selected_qr_${selected.size}.csv` : `qr_report_${new Date().toISOString().slice(0, 10)}.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } const paged = useMemo(() => rows.slice((page - 1) * pageSize, page * pageSize), [rows, page]); const totalPages = Math.max(1, Math.ceil(rows.length / pageSize)); function toggle(id: string) { setSelected((s) => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; }); } function toggleAll() { setSelected((s) => s.size === paged.length ? new Set() : new Set(paged.map((r) => r.id))); } async function bulk(action: string) { if (selected.size === 0) return; if (action === "delete" && !confirm(`Delete ${selected.size} QR code(s)?`)) return; await api("/qr/bulk", { method: "POST", body: JSON.stringify({ ids: [...selected], action }) }); setSelected(new Set()); load(); } const set = (k: string, v: string) => { setPage(1); setFilters((f) => ({ ...f, [k]: v })); }; return (
setMenuFor(null)}>

QR Management Center

{/* KPIs */}
} tint="bg-blue-50 text-blue-600" /> } tint="bg-emerald-50 text-emerald-600" /> } tint="bg-rose-50 text-rose-500" /> } tint="bg-violet-50 text-violet-600" /> } tint="bg-cyan-50 text-cyan-600" />
{/* Filters */}
set("search", e.target.value)} placeholder="Search QR…" className="pl-9 pr-3 py-2 w-44 rounded-lg border border-slate-200 text-sm" />
set("product_id", v)} opts={products.map((p) => ({ value: p.id, label: p.name }))} all="All Products" /> set("brand_id", v)} opts={brands.map((b) => ({ value: b.id, label: b.name }))} all="All Brands" /> set("status_filter", v)} opts={[{ value: "active", label: "Active" }, { value: "inactive", label: "Inactive" }, { value: "disabled", label: "Disabled" }]} all="All Status" /> set("qr_type", v)} opts={[{ value: "batch", label: "Batch" }, { value: "per_pack", label: "Per-Pack" }]} all="All Types" />
Created On
set("start_date", e.target.value)} className="px-3 py-2 rounded-lg border border-slate-200 text-sm" /> to set("end_date", e.target.value)} className="px-3 py-2 rounded-lg border border-slate-200 text-sm" />
{/* Bulk actions bar */}
{selected.size} selected {[["disable", Power, "Disable"], ["archive", Archive, "Archive"], ["delete", Trash2, "Delete"]].map(([a, Icon, label]: any) => ( ))}
Total: {rows.length}
{/* Table */}
{paged.map((q) => ( ))} {paged.length === 0 && }
0 && selected.size === paged.length} onChange={toggleAll} /> QR Product / SKU Batch Type Status Scans Last Scan Created Actions
toggle(q.id)} /> {q.image_url ? : }
{q.product_name ?? "—"}
{q.sku ?? ""}
{q.batch_number ?? "—"} {q.qr_type === "per_pack" ? "Per-Pack" : "Batch"} {q.status} {q.scan_count.toLocaleString()} {q.last_scan_at ? ( <>
{new Date(q.last_scan_at).toLocaleDateString("en-IN")}
{new Date(q.last_scan_at).toLocaleTimeString("en-IN", { hour: "2-digit", minute: "2-digit", second: "2-digit", })}
{q.last_scan_location ?? ""}
) : ( "—" )}
{new Date(q.created_at).toLocaleDateString("en-IN")}
{new Date(q.created_at).toLocaleTimeString("en-IN", { hour: "2-digit", minute: "2-digit", second: "2-digit", })}
setOpenId(q.id)}>
{menuFor === q.id && (
e.stopPropagation()} className="absolute right-3 top-12 z-20 w-44 bg-white rounded-xl border border-slate-200 shadow-lg py-1 text-left"> Download PNG Download SVG Download PDF
)}
No QR codes match.
{/* Pagination */}
Showing {(page - 1) * pageSize + 1} to {Math.min(page * pageSize, rows.length)} of {rows.length}
{Array.from({ length: totalPages }).slice(0, 5).map((_, i) => ( ))}
{showProductPicker && (

Generate QR

Select a product.

setProductSearch(e.target.value)} placeholder="Search product..." className="flex-1 rounded-lg border px-3 py-2" />
{filteredProducts.map((p) => ( ))}
)} {/* QR Details modal */} {openId && ( setOpenId(null)} onChanged={load} /> )}
); } function Kpi({ label, value, sub, subColor = "text-slate-400", icon, tint }: any) { return (
{icon}
{label}
{value}
{sub}
); } function Filter({ label, value, onChange, opts, all }: { label: string; value: string; onChange: (v: string) => void; opts: { value: string; label: string }[]; all: string; }) { return ( ); } function IconBtn({ children, onClick, title }: any) { return ( ); }