"use client"; import { useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { Plus, Package, AlertTriangle, MoreVertical, Eye, Pencil, Copy, QrCode, Archive, RotateCcw, } from "lucide-react"; import { api, apiBase } from "@/lib/api"; import ProductDialog from "@/components/ProductDialog"; import { Download } from "lucide-react"; type Mode = "add" | "edit" | "duplicate" | "view"; // data-URLs and absolute URLs pass through; relative /uploads paths get the API host function imgSrc(url: string) { if (!url) return ""; if (url.startsWith("data:") || url.startsWith("http")) return url; return `${apiBase}${url}`; } export default function Products() { const router = useRouter(); const [data, setData] = useState(null); const [showArchived, setShowArchived] = useState(false); const [dialog, setDialog] = useState<{ mode: Mode; id?: string } | null>(null); const [menuFor, setMenuFor] = useState(null); const [search, setSearch] = useState(""); const [brandId, setBrandId] = useState(""); const [category, setCategory] = useState(""); const [status, setStatus] = useState(""); const [qrStatus, setQrStatus] = useState(""); const [trustFrom, setTrustFrom] = useState(""); const [trustTo, setTrustTo] = useState(""); const [createdFrom, setCreatedFrom] = useState(""); const [createdTo, setCreatedTo] = useState(""); const [brands, setBrands] = useState([]); const [page, setPage] = useState(1); const pageSize = 20; const [selected, setSelected] = useState([]); const toggleSelectAll = () => { if (selected.length === (data?.items?.length ?? 0)) { setSelected([]); } else { setSelected((data?.items ?? []).map((p: any) => p.id)); } }; const toggleSelect = (id: string) => { setSelected((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id] ); }; useEffect(() => { api("/brands") .then((res) => { setBrands(res.items ?? res); }) .catch(() => setBrands([])); }, []); function load() { const params = new URLSearchParams(); if (search) params.append("search", search); if (brandId) params.append("brand_id", brandId); if (category) params.append("category", category); if (showArchived) { params.append("status_filter", "archived"); } else if (status) { params.append("status_filter", status); } if (qrStatus) params.append("qr_status", qrStatus); if (trustFrom) params.append("trust_from", trustFrom); if (trustTo) params.append("trust_to", trustTo); if (createdFrom) params.append("created_from", createdFrom); if (createdTo) params.append("created_to", createdTo); params.append("page", page.toString()); params.append("page_size", pageSize.toString()); api(`/products?${params.toString()}`) .then(setData) .catch(() => setData({ items: [] })); } useEffect(() => { load(); }, [showArchived,search, brandId, category, status, qrStatus, trustFrom, trustTo, createdFrom, createdTo, page,]); const limitReached = data?.limit_reached; async function archive(id: string) { await api(`/products/${id}/archive`, { method: "PUT" }); setMenuFor(null); load(); } async function restore(id: string) { try { await api(`/products/${id}/restore`, { method: "PUT" }); } catch (e: any) { alert(e.detail?.message || "Restore failed"); } setMenuFor(null); load(); } async function duplicate(id: string) { try { await api(`/products/${id}/duplicate`, { method: "POST" }); load(); } catch (e: any) { alert(e.detail?.message || "Duplicate failed"); } setMenuFor(null); } function exportSelected() { const rows = (data?.items ?? []).filter((p: any) => selected.includes(p.id) ); const csv = [ [ "Name", "SKU", "Brand", "Category", "Trust Score", "Status", "QR Count", "Create At" ], ...rows.map((p: any) => [ p.name, p.sku, p.brand_name, p.category, p.trust_score, p.status, p.qr_count, p.created_at, ]), ] .map((r) => r.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 = "products.csv"; a.click(); URL.revokeObjectURL(url); } async function archiveSelected() { if (selected.length === 0) return; if (!confirm(`Archive ${selected.length} products?`)) return; await Promise.all( selected.map((id) => api(`/products/${id}/archive`, { method: "PUT", }) ) ); setSelected([]); load(); } const totalPages = data?.pages ?? 1; const totalItems = data?.total ?? 0; const startPage = Math.max(1, page - 2); const endPage = Math.min(totalPages, startPage + 4); const pages = []; for (let i = startPage; i <= endPage; i++) { pages.push(i); } return (
setMenuFor(null)}>

Products

{data &&

{data.count} / {data.limit ?? "∞"} products used

}
{/* Search */} setSearch(e.target.value)} className="border rounded-lg px-3 py-2" /> {/* Brand */} {/* Category */} {/* Status */} {/* QR Status */} {/* Created From */} setCreatedFrom(e.target.value)} className="border rounded-lg px-3 py-2" /> {/* Created To */} setCreatedTo(e.target.value)} className="border rounded-lg px-3 py-2" /> {/* Search Button */} {/* Reset */}
{limitReached && !showArchived && (
You've reached your plan's product limit. Upgrade to add more.
Upgrade
)}
{/* Left */}
Selected: {selected.length} {!showArchived && ( )}
{/* Right */}
Total Products {data?.total ?? data?.items?.length ?? 0}
{(data?.items ?? []).map((p: any) => ( ))} {data?.items?.length === 0 && ( )}
0 && selected.length === (data?.items?.length ?? 0) } onChange={toggleSelectAll} /> Image Product SKU Category Brand QR Status Trust Status QRs Created Actions
toggleSelect(p.id)} /> {p.image_url ? :
}
{p.name} {p.sku ?? "—"} {p.category || "—"} {p.brand_name ?? "—"} 0 ? "bg-blue-50 text-blue-700" : "bg-slate-100 text-slate-500" }`} > {p.qr_count > 0 ? "Generated" : "Not Generated"} {p.trust_score}% {p.status} {p.qr_count} {new Date(p.created_at).toLocaleString()}
{menuFor === p.id && (
e.stopPropagation()} className="absolute right-4 top-12 z-20 w-44 bg-white rounded-xl border border-slate-200 shadow-lg py-1 text-left"> {p.status === "archived" ? ( } label="Restore" onClick={() => restore(p.id)} /> ) : ( <> } label="Edit" onClick={() => { setDialog({ mode: "edit", id: p.id }); setMenuFor(null); }} /> } label="Duplicate" onClick={() => duplicate(p.id)} /> } label="Generate QR" onClick={() => router.push(`/dashboard/products/${p.id}/batch`)} />
} label="Archive" danger onClick={() => archive(p.id)} /> )}
)}
{showArchived ? "No archived products." : "No products yet. Add your first product."}
Showing{" "} {totalItems === 0 ? 0 : (page - 1) * pageSize + 1}{" "} to{" "} {Math.min(page * pageSize, totalItems)}{" "} of {totalItems}
{pages.map((p) => ( ))}
{dialog && ( setDialog(null)} onSaved={load} /> )}
); } function MenuItem({ icon, label, onClick, danger }: any) { return ( ); }