"use client"; import { useState, useCallback, useTransition, useEffect, useRef } from "react"; import { createPortal } from "react-dom"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { createProduct } from "@/actions/products/create-product"; import { updateProduct } from "@/actions/products/update-product"; import { deleteProduct } from "@/actions/products"; import { uploadProductImage } from "@/actions/products/upload-image"; import ProductFormModal, { type ProductFormValues } from "@/components/admin/ProductFormModal"; import { PageHeader, AdminButton, AdminIconButton, AdminSearchInput, AdminFilterTabs, AdminViewModeTabs, useToast, Skeleton } from "@/components/admin/design-system"; type Product = { id: string; name: string; description: string; price: number; type: string; active: boolean; image_url?: string | null; brand_id: string; is_taxable: boolean; }; type ViewMode = "table" | "cards"; // Icons const Icons = { search: (className: string) => ( ), plus: (className: string) => ( ), package: (className: string) => ( ), upload: (className: string) => ( ), }; // Page header icon const PackageIconHeader = () => ( ); export default function ProductsClient({ products, brandId, brands = [], isPlatformAdmin = false, }: { products: Product[]; brandId?: string; brands?: { id: string; name: string }[]; isPlatformAdmin?: boolean; }) { const router = useRouter(); const { success: showSuccess, error: showError } = useToast(); const [, startTransition] = useTransition(); const [search, setSearch] = useState(""); const [statusFilter, setStatusFilter] = useState<"all" | "active" | "inactive">("all"); const [viewMode, setViewMode] = useState("table"); const [showModal, setShowModal] = useState(false); const [editingProduct, setEditingProduct] = useState(null); const [deleteConfirm, setDeleteConfirm] = useState(null); const [deletingId, setDeletingId] = useState(null); const [isLoading, setIsLoading] = useState(false); const filtered = products.filter((p) => { const matchesSearch = !search || p.name.toLowerCase().includes(search.toLowerCase()) || p.description.toLowerCase().includes(search.toLowerCase()); const matchesStatus = statusFilter === "all" || (statusFilter === "active" && p.active) || (statusFilter === "inactive" && !p.active); return matchesSearch && matchesStatus; }); const activeCount = products.filter((p) => p.active).length; const inactiveCount = products.filter((p) => !p.active).length; async function resizeImage(file: File, maxWidth: number): Promise { return new Promise((resolve, reject) => { const img = document.createElement("img"); img.onload = () => { let { width, height } = img; if (width > maxWidth) { height = Math.round(height * (maxWidth / width)); width = maxWidth; } const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d")!; ctx.drawImage(img, 0, 0, width, height); canvas.toBlob((blob) => { if (!blob) { reject(new Error("Failed to resize image")); return; } blob.arrayBuffer().then((buf) => resolve(buf)); }, "image/jpeg", 0.85); }; img.onerror = reject; img.src = URL.createObjectURL(file); }); } const handleUploadImage = useCallback( async (file: File): Promise<{ success: boolean; imageUrl?: string; error?: string }> => { const validTypes = ["image/png", "image/jpeg", "image/webp"]; if (!validTypes.includes(file.type)) { return { success: false, error: "Only PNG, JPEG, and WebP images are allowed." }; } if (file.size > 5 * 1024 * 1024) { return { success: false, error: "Image must be under 5MB." }; } const resizedBuffer = await resizeImage(file, 1200); const resizedFile = new File([resizedBuffer], file.name, { type: "image/jpeg" }); const productIdForUpload = editingProduct ? editingProduct.id : "__NEW__"; const result = await uploadProductImage(productIdForUpload, resizedFile); if (result.success) { return { success: true, imageUrl: result.imageUrl }; } return { success: false, error: result.error }; }, [editingProduct] ); const openAddModal = useCallback(() => { setEditingProduct(null); setShowModal(true); }, []); const openEditModal = useCallback((product: Product) => { setEditingProduct(product); setShowModal(true); }, []); const closeModal = useCallback(() => { setShowModal(false); setEditingProduct(null); }, []); const handleModalSubmit = useCallback( async (values: ProductFormValues): Promise<{ success: boolean; error?: string }> => { setIsLoading(true); try { const price = parseFloat(values.price); if (isNaN(price) || price < 0) { return { success: false, error: "Please enter a valid price" }; } if (!values.name.trim()) { return { success: false, error: "Product name is required" }; } // Platform admins pick a brand in the modal; everyone else is locked. const effectiveBrandId = isPlatformAdmin ? values.brand_id : brandId; if (!effectiveBrandId) { return { success: false, error: "Please choose a brand for this product." }; } let result; if (editingProduct) { result = await updateProduct(editingProduct.id, effectiveBrandId, { name: values.name.trim(), description: values.description.trim(), price, type: values.type, active: values.active, image_url: values.image_url, is_taxable: values.is_taxable, }); } else { result = await createProduct(effectiveBrandId, { name: values.name.trim(), description: values.description.trim(), price, type: values.type, active: values.active, image_url: values.image_url, is_taxable: values.is_taxable, }); } if (!result.success) { showError("Failed to save product", result.error ?? "Please try again"); return { success: false, error: result.error }; } showSuccess( editingProduct ? "Product updated" : "Product created", `${values.name} has been saved` ); startTransition(() => router.refresh()); return { success: true }; } catch { return { success: false, error: "Network error. Please try again." }; } finally { setIsLoading(false); } }, [editingProduct, brandId, isPlatformAdmin, router, showError, showSuccess] ); const handleDelete = async (productId: string) => { setDeletingId(productId); const result = await deleteProduct(productId, null); setDeletingId(null); if (result.success) { setDeleteConfirm(null); showSuccess("Product deleted", "The product has been removed"); startTransition(() => router.refresh()); } else { showError("Failed to delete product", result.error ?? "Please try again"); } }; // Filter tabs configuration const filterTabs = [ { value: "all", label: "All", count: products.length }, { value: "active", label: "Active", count: activeCount }, { value: "inactive", label: "Inactive", count: inactiveCount }, ]; return (
{/* Page Header */} } title="Products" subtitle={`${filtered.length} product${filtered.length !== 1 ? "s" : ""}`} actions={ Add Product } /> {/* Stats Cards */}

Total

{isLoading ? : products.length}

Active

{isLoading ? : activeCount}

Inactive

{isLoading ? : inactiveCount}

{/* Filters */}
{/* Status Tabs */} setStatusFilter(value as "all" | "active" | "inactive")} tabs={filterTabs} size="md" /> {/* Search */} setSearch(e.target.value)} onClear={() => setSearch("")} placeholder="Search products..." containerClassName="flex-1" /> {/* View Toggle */} setViewMode(value as ViewMode)} />
{/* Content */} {viewMode === "table" ? ( setDeleteConfirm(id)} deleteConfirm={deleteConfirm} onDeleteConfirm={(id) => handleDelete(id)} onDeleteCancel={() => setDeleteConfirm(null)} deletingId={deletingId} /> ) : ( setDeleteConfirm(id)} deleteConfirm={deleteConfirm} onDeleteConfirm={(id) => handleDelete(id)} onDeleteCancel={() => setDeleteConfirm(null)} deletingId={deletingId} /> )} {/* Modal — Atelier des Récoltes (editorial product form) */}
); } // ── Table View ───────────────────────────────────────────────────────────────── function TableView({ products, onEdit, onDelete, deleteConfirm, onDeleteConfirm, onDeleteCancel, deletingId, }: { products: Product[]; onEdit: (p: Product) => void; onDelete: (id: string) => void; deleteConfirm: string | null; onDeleteConfirm: (id: string) => void; onDeleteCancel: () => void; deletingId: string | null; }) { // Track the position of the open row's three-dots button so the popup can be // rendered via portal at body level (escapes any overflow:hidden ancestors // and any table-row stacking-context quirks). const [menuPos, setMenuPos] = useState<{ top: number; left: number; width: number } | null>(null); const [mounted, setMounted] = useState(false); const buttonRefs = useRef>({}); useEffect(() => { setMounted(true); }, []); useEffect(() => { if (!deleteConfirm) { setMenuPos(null); return; } const btn = buttonRefs.current[deleteConfirm]; if (btn) { const rect = btn.getBoundingClientRect(); setMenuPos({ top: rect.bottom + 4, left: rect.right, width: rect.width }); } }, [deleteConfirm]); // Reposition on scroll/resize so the popup stays anchored to its button. useEffect(() => { if (!deleteConfirm) return; const updatePos = () => { const btn = buttonRefs.current[deleteConfirm]; if (btn) { const rect = btn.getBoundingClientRect(); setMenuPos({ top: rect.bottom + 4, left: rect.right, width: rect.width }); } }; window.addEventListener("scroll", updatePos, true); window.addEventListener("resize", updatePos); return () => { window.removeEventListener("scroll", updatePos, true); window.removeEventListener("resize", updatePos); }; }, [deleteConfirm]); const openProduct = deleteConfirm ? products.find((p) => p.id === deleteConfirm) : null; return (
{products.length === 0 ? ( ) : ( products.map((product) => ( )) )}
Product Type Price Status
{Icons.package("h-8 w-8 text-stone-400")}

No products found

Try adjusting your filters

{product.image_url ? (
{product.name} { (e.target as HTMLImageElement).style.display = "none"; }} />
) : (
)}
{product.description || No description}
{product.type} ${Number(product.price).toFixed(2)} {product.active ? "Active" : "Inactive"}
onEdit(product)} > Edit
{/* Delete confirmation popup — rendered via portal at body level with position: fixed so it sits above all table rows and escapes the overflow-visible container. */} {mounted && deleteConfirm && openProduct && menuPos && createPortal( <>

Delete "{openProduct.name}"?

This will remove the product. If attached to orders, it will be hidden.

onDeleteConfirm(openProduct.id)} disabled={deletingId === openProduct.id} variant="danger" size="sm" className="flex-1" > {deletingId === openProduct.id ? "..." : "Delete"}
, document.body )}
); } // ── Card View ────────────────────────────────────────────────────────────────── function CardView({ products, onEdit, onDelete, deleteConfirm, onDeleteConfirm, onDeleteCancel, deletingId, }: { products: Product[]; onEdit: (p: Product) => void; onDelete: (id: string) => void; deleteConfirm: string | null; onDeleteConfirm: (id: string) => void; onDeleteCancel: () => void; deletingId: string | null; }) { return (
{products.length === 0 ? (
{Icons.package("h-8 w-8 text-stone-400")}

No products found

Try adjusting your filters

) : ( products.map((product) => (
{/* Image */}
{product.image_url ? ( {product.name} { (e.target as HTMLImageElement).style.display = "none"; }} /> ) : (
)} {/* Status badge */}
{product.active ? "Active" : "Inactive"}
{/* Content */}
Price

${Number(product.price).toFixed(2)}

{product.type} {product.is_taxable && ( Tax )}
onEdit(product)} className="flex-1" > Edit
{/* Delete confirm */} {deleteConfirm === product.id && (

Delete "{product.name}"?

This will remove the product. If attached to orders, it will be hidden.

)}
)) )}
); }