"use client"; import { useState, useEffect, useRef } from "react"; import crypto from "crypto"; import { type WholesaleOrder, type WholesaleCustomer, type WholesaleProduct, type WholesaleSettings, type WholesaleDashboardStats, type NotificationRecipient, getWholesaleOrders, getWholesaleCustomers, getWholesaleProducts, getWholesaleSettings, getWholesaleDashboardStats, markWholesaleOrderFulfilled, updateWholesaleOrderStatus, deleteWholesaleOrder, deleteWholesaleCustomer, deleteWholesaleProduct, saveWholesaleCustomer, saveWholesaleProduct, saveWholesaleSettings, recordWholesaleDeposit, bulkFulfillWholesaleOrders, bulkRecordWholesaleDeposit, enqueueWholesaleNotification, getWebhookSettings, saveWebhookSettings, enqueueWholesaleWebhook, getRecentWebhookActivity, } from "@/actions/wholesale"; import DepositModal from "@/components/wholesale/DepositModal"; import OrderDetailsModal from "@/components/wholesale/OrderDetailsModal"; import { getPendingWholesaleRegistrations, approveWholesaleRegistration, getWholesaleCustomerPricing, upsertWholesaleCustomerPricing, deleteWholesaleCustomerPricing } from "@/actions/wholesale-register"; import { getCurrentAdminUser } from "@/actions/admin-user"; import { type AdminUser } from "@/lib/admin-permissions"; import { formatDate } from "@/lib/format-date"; import { PageHeader, AdminButton, AdminSearchInput, AdminFilterTabs, AdminBadge } from "@/components/admin/design-system"; type Tab = "dashboard" | "customers" | "products" | "orders" | "settings"; // SVG Icon for Wholesale - defined at module level to prevent recreation on each render function WholesaleIcon() { return ( ); } export default function WholesaleClient({ brandId }: { brandId: string }) { const [tab, setTab] = useState("dashboard"); const [msg, setMsg] = useState<{ kind: "success" | "error"; text: string } | null>(null); const [adminUser, setAdminUser] = useState(null); // Data state const [orders, setOrders] = useState([]); const [customers, setCustomers] = useState([]); const [products, setProducts] = useState([]); const [settings, setSettings] = useState(null); const [stats, setStats] = useState(null); const [registrations, setRegistrations] = useState>([]); const [loading, setLoading] = useState(true); const [webhookActivity, setWebhookActivity] = useState>([]); useEffect(() => { async function load() { setLoading(true); const [au, o, c, p, s, st, r, wa] = await Promise.all([ getCurrentAdminUser(), getWholesaleOrders(brandId), getWholesaleCustomers(brandId), getWholesaleProducts(brandId), getWholesaleSettings(brandId), getWholesaleDashboardStats(brandId), getPendingWholesaleRegistrations(brandId), getRecentWebhookActivity(brandId, 5), ]); setAdminUser(au); setOrders(o); setCustomers(c); setProducts(p); setSettings(s); setStats(st); setRegistrations(r as typeof registrations); setWebhookActivity(wa); setLoading(false); } load(); }, [brandId]); function showMsg(kind: "success" | "error", text: string) { setMsg({ kind, text }); setTimeout(() => setMsg(null), 4000); } if (loading) { return (
); } const tabs = [ { value: "dashboard", label: "Dashboard", count: undefined }, { value: "customers", label: "Customers", count: customers.filter(c => c.account_status !== "pending_approval" && c.account_status !== "rejected").length }, { value: "products", label: "Products", count: products.length }, { value: "orders", label: "Orders", count: orders.length }, { value: "settings", label: "Settings" }, ]; // Declare icon reference for PageHeader const pageIcon = ; return (
{/* Tab nav */}
setTab(value as Tab)} tabs={tabs} size="md" />
{msg && (
{msg.text}
)} {tab === "dashboard" && ( )} {tab === "customers" && ( { const r = await getPendingWholesaleRegistrations(brandId); setRegistrations(r as typeof registrations); }} /> )} {tab === "products" && ( { const p = await getWholesaleProducts(brandId); setProducts(p); }} /> )} {tab === "orders" && ( { const o = await getWholesaleOrders(brandId); setOrders(o); }} /> )} {tab === "settings" && ( { const s = await getWholesaleSettings(brandId); setSettings(s); }} canManageSettings={adminUser?.can_manage_settings ?? false} /> )}
); } // ── Dashboard Tab ───────────────────────────────────────────────────────────── function DashboardTab({ stats, recentOrders, brandId, onMsg, webhookActivity }: { stats: WholesaleDashboardStats; recentOrders: WholesaleOrder[]; brandId: string; onMsg: (kind: "success" | "error", text: string) => void; webhookActivity: Array<{ id: string; event_type: string; order_id: string | null; status: string; attempts: number; created_at: string; response: string | null; }>; }) { return (
{/* Stat cards */}
{[ { label: "Open Orders", value: stats.open_orders, variant: "default" }, { label: "Pickup Today", value: stats.pickup_today, variant: "default" }, { label: "Past Due", value: stats.past_due, variant: "danger" }, { label: "Total Unpaid", value: `$${stats.total_unpaid.toFixed(2)}`, variant: "default" }, { label: "Awaiting Deposit", value: stats.awaiting_deposit, variant: "warning" }, { label: "Fulfilled Today", value: stats.fulfilled_today, variant: "success" }, ].map((card) => (

{card.label}

{card.value}

))}
{/* Recent orders */}

Recent Orders

{recentOrders.length === 0 ? (

No wholesale orders yet

Wholesale orders placed by your customers will appear here.

) : (
{recentOrders.map((order) => ( ))}
Invoice Customer Pickup Date Total Status Payment
{order.invoice_number ?? "—"} {order.company_name} {order.anticipated_pickup_date ?? "—"} ${Number(order.subtotal).toFixed(2)} {order.payment_status === "paid" ? "Paid" : order.balance_due > 0 ? `$${Number(order.balance_due).toFixed(2)} due` : "Partial"}
)}
{/* Recent webhook activity */} {webhookActivity.length > 0 && (

Recent Webhook Activity

{webhookActivity.map((entry) => ( ))}
Event Order Status Attempts Sent At
{entry.event_type} {entry.order_id ? entry.order_id.slice(0, 8) : "—"} {entry.status} {entry.attempts} {new Date(entry.created_at).toLocaleString()}
)}
); } // ── Customers Tab ──────────────────────────────────────────────────────────── function CustomersTab({ customers, products, brandId, onMsg, registrations = [], onRefresh }: { customers: WholesaleCustomer[]; products: WholesaleProduct[]; brandId: string; onMsg: (kind: "success" | "error", text: string) => void; registrations?: Array<{ id: string; company_name: string | null; contact_name: string | null; email: string; phone: string | null; account_status: string; created_at: string; }>; onRefresh: () => void; }) { const [showForm, setShowForm] = useState(false); const [editing, setEditing] = useState(null); const [subTab, setSubTab] = useState<"customers" | "registrations">("customers"); const [form, setForm] = useState({ companyName: "", contactName: "", email: "", phone: "", accountStatus: "active", creditLimit: 0, depositsEnabled: false, depositThreshold: "", depositPercentage: "", orderEmail: "", invoiceEmail: "", adminNotes: "", }); const [saving, setSaving] = useState(false); const [processingReg, setProcessingReg] = useState(null); const [pricingCustomer, setPricingCustomer] = useState(null); const [selectedCustomers, setSelectedCustomers] = useState>(new Set()); const [sendingPriceSheet, setSendingPriceSheet] = useState(false); const [priceSheetTarget, setPriceSheetTarget] = useState<{ customerIds: string[]; defaultSubject: string; } | null>(null); const [openCustomerActions, setOpenCustomerActions] = useState(null); const [deletingCustomer, setDeletingCustomer] = useState(null); function toggleSelectCustomer(id: string) { setSelectedCustomers(prev => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); } function toggleAllCustomers() { if (selectedCustomers.size === customers.length) { setSelectedCustomers(new Set()); } else { setSelectedCustomers(new Set(customers.map(c => c.id))); } } function openPriceSheetModal(customerIds: string[]) { const ids = customerIds; if (ids.length === 0) return; // Generate default subject from brand name in settings const brandName = ""; // will be filled by the API call setPriceSheetTarget({ customerIds: ids, defaultSubject: `Wholesale Price Sheet — ${new Date().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}`, }); } async function handleSendPriceSheet(customerId?: string) { const ids = customerId ? [customerId] : Array.from(selectedCustomers); if (ids.length === 0) return; openPriceSheetModal(ids); } async function handleConfirmPriceSheet(subject: string, customNote: string) { if (!priceSheetTarget) return; setSendingPriceSheet(true); setPriceSheetTarget(null); try { const res = await fetch("/api/wholesale/price-sheet", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ customerIds: priceSheetTarget.customerIds, brandId, subject, customNote: customNote || undefined, }), }); const data = await res.json(); if (res.ok) { onMsg("success", `Price sheet sent to ${data.enqueued} customer(s).`); setSelectedCustomers(new Set()); } else { onMsg("error", data.error ?? "Failed to send price sheet."); } } catch { onMsg("error", "Failed to send price sheet."); } finally { setSendingPriceSheet(false); } } async function handleApproveReject(regId: string, action: "approve" | "reject") { setProcessingReg(regId); const result = await approveWholesaleRegistration(regId, brandId, action); setProcessingReg(null); if (result.success) { onMsg("success", action === "approve" ? "Registration approved." : "Registration rejected."); onRefresh(); } else { onMsg("error", result.error ?? "Failed to process registration."); } } // Close customer actions dropdown when clicking outside useEffect(() => { function handleClick(e: MouseEvent) { if (!(e.target as Element).closest(".customer-actions-cell")) { setOpenCustomerActions(null); } } document.addEventListener("click", handleClick); return () => document.removeEventListener("click", handleClick); }, []); function toggleCustomerActions(customerId: string, e: React.MouseEvent) { e.stopPropagation(); setOpenCustomerActions(prev => prev === customerId ? null : customerId); } async function handleDeleteCustomer(customerId: string) { if (!confirm("Delete this customer? This cannot be undone. Customers with existing orders cannot be deleted.")) return; setDeletingCustomer(customerId); const result = await deleteWholesaleCustomer(customerId); setDeletingCustomer(null); if (result.success) { onMsg("success", "Customer deleted."); onRefresh(); } else { onMsg("error", result.error ?? "Failed to delete customer."); } } async function handleSave() { setSaving(true); const result = await saveWholesaleCustomer({ brandId, userId: editing?.user_id ?? undefined, companyName: form.companyName || undefined, contactName: form.contactName || undefined, email: form.email || undefined, phone: form.phone || undefined, accountStatus: form.accountStatus, creditLimit: form.creditLimit, depositsEnabled: form.depositsEnabled, depositThreshold: form.depositThreshold ? Number(form.depositThreshold) : undefined, depositPercentage: form.depositPercentage ? Number(form.depositPercentage) : undefined, orderEmail: form.orderEmail || undefined, invoiceEmail: form.invoiceEmail || undefined, adminNotes: form.adminNotes || undefined, }); setSaving(false); if (result.success) { onMsg("success", editing ? "Customer updated." : "Customer created."); setShowForm(false); onRefresh(); } else { onMsg("error", result.error ?? "Failed to save."); } } function openNew() { setEditing(null); setForm({ companyName: "", contactName: "", email: "", phone: "", accountStatus: "active", creditLimit: 0, depositsEnabled: false, depositThreshold: "", depositPercentage: "", orderEmail: "", invoiceEmail: "", adminNotes: "" }); setShowForm(true); } function openEdit(c: WholesaleCustomer) { setEditing(c); setForm({ companyName: c.company_name ?? "", contactName: c.contact_name ?? "", email: c.email ?? "", phone: c.phone ?? "", accountStatus: c.account_status ?? "active", creditLimit: Number(c.credit_limit), depositsEnabled: c.deposits_enabled, depositThreshold: c.deposit_threshold?.toString() ?? "", depositPercentage: c.deposit_percentage?.toString() ?? "", orderEmail: c.order_email ?? "", invoiceEmail: c.invoice_email ?? "", adminNotes: c.admin_notes ?? "", }); setShowForm(true); } return (
{/* Sub-tab nav */}
setSubTab("customers")} > Customers ({customers.filter(c => c.account_status !== "pending_approval" && c.account_status !== "rejected").length}) setSubTab("registrations")} > Registrations ({registrations.filter(r => r.account_status === "pending_approval").length}) {subTab === "customers" && ( + Add Customer )}
{subTab === "registrations" && (

Pending Registrations

{registrations.filter(r => r.account_status === "pending_approval").length === 0 ? (

No pending registrations.

) : (
{registrations.filter(r => r.account_status === "pending_approval").map(r => ( ))}
Company Contact Status Registered
{r.company_name ?? "—"} {r.contact_name ?? "—"}
{r.email}
Pending {formatDate(new Date(r.created_at))}
handleApproveReject(r.id, "approve")} disabled={processingReg === r.id} isLoading={processingReg === r.id} > {processingReg === r.id ? "..." : "Approve"} handleApproveReject(r.id, "reject")} disabled={processingReg === r.id} > Reject
)}
)} {subTab === "customers" && ( <> {showForm && (

{editing ? "Edit Customer" : "New Customer"}

setForm(f => ({ ...f, companyName: e.target.value }))} className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" />
setForm(f => ({ ...f, contactName: e.target.value }))} className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" />
setForm(f => ({ ...f, email: e.target.value }))} className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" autoComplete="email" />
setForm(f => ({ ...f, phone: e.target.value }))} className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" autoComplete="tel" />
setForm(f => ({ ...f, creditLimit: Number(e.target.value) }))} className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" placeholder="0 = unlimited" />

Deposit Rules

setForm(f => ({ ...f, depositsEnabled: e.target.checked }))} className="rounded" id="dep-enabled" />
{form.depositsEnabled && (
setForm(f => ({ ...f, depositThreshold: e.target.value }))} className="w-full rounded-lg border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" placeholder="0" />
setForm(f => ({ ...f, depositPercentage: e.target.value }))} className="w-full rounded-lg border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" placeholder="20" min="1" max="100" />
)}
{saving ? "Saving..." : "Save Customer"} setShowForm(false)} variant="secondary"> Cancel
)}
{customers.length === 0 ? ( ) : customers.map(c => ( ))} {selectedCustomers.size > 0 && ( )}
0} onChange={toggleAllCustomers} /> Company Contact Status Credit Deposits Created

No customers yet

Wholesale customers will appear here once registered.

toggleSelectCustomer(c.id)} /> {c.company_name ?? "—"} {c.contact_name ?? "—"}
{c.email}
{c.account_status} {c.credit_limit <= 0 ? "Unlimited" : `$${Number(c.credit_limit).toFixed(2)}`} {c.deposits_enabled ? `${c.deposit_percentage}%` : "—"} {formatDate(new Date(c.created_at))}
{openCustomerActions === c.id && (
e.stopPropagation()} > View Portal As
)}
{selectedCustomers.size} selected openPriceSheetModal(Array.from(selectedCustomers))} disabled={sendingPriceSheet} variant="primary" size="sm" isLoading={sendingPriceSheet} > {sendingPriceSheet ? "Sending..." : `Send Price Sheet to ${selectedCustomers.size} Customer(s)`}
)} {/* Customer Pricing Overlays Panel */} {pricingCustomer && ( setPricingCustomer(null)} onMsg={onMsg} /> )} {/* Price Sheet Modal */} {priceSheetTarget && ( { setPriceSheetTarget(null); setSendingPriceSheet(false); }} /> )}
); } // ── Customer Pricing Panel ────────────────────────────────────────────────────── function CustomerPricingPanel({ customer, products, onClose, onMsg }: { customer: WholesaleCustomer; products: WholesaleProduct[]; onClose: () => void; onMsg: (kind: "success" | "error", text: string) => void; }) { const [overrides, setOverrides] = useState>({}); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); useEffect(() => { getWholesaleCustomerPricing(customer.id).then(pricing => { const map: Record = {}; for (const p of pricing) { map[p.product_id] = p.custom_unit_price.toString(); } setOverrides(map); setLoading(false); }); }, [customer.id]); async function handleSave(productId: string, price: string) { if (!price || isNaN(Number(price))) { await deleteWholesaleCustomerPricing({ customerId: customer.id, productId }); setOverrides(prev => { const n = { ...prev }; delete n[productId]; return n; }); } else { await upsertWholesaleCustomerPricing({ customerId: customer.id, productId, customUnitPrice: Number(price) }); setOverrides(prev => ({ ...prev, [productId]: price })); } onMsg("success", "Pricing updated."); } return (

Pricing Overrides

{customer.company_name}

{loading ? (

Loading...

) : products.length === 0 ? (

No products available. Add wholesale products to see them here.

) : ( {products.map(p => { const standardPrice = p.price_tiers?.[0]?.price ?? 0; const overridePrice = overrides[p.id] ?? ""; return ( ); })}
Product Standard Price Override Price
{p.name} ${standardPrice.toFixed(2)}
$ setOverrides(prev => ({ ...prev, [p.id]: e.target.value }))} placeholder={standardPrice.toFixed(2)} className="w-24 rounded-lg border border-[var(--admin-border)] px-2 py-1 text-sm outline-none focus:border-[var(--admin-accent)]" /> {overridePrice && overridePrice !== standardPrice.toFixed(2) && ( Custom )}
handleSave(p.id, overrides[p.id] ?? "")} disabled={saving} > Save
)}

Leave override price blank to remove custom pricing and use standard price tiers.

); } // ── Price Sheet Modal ────────────────────────────────────────────────────────── function PriceSheetModal({ customerCount, defaultSubject, onConfirm, onClose, }: { customerCount: number; defaultSubject: string; onConfirm: (subject: string, customNote: string) => void; onClose: () => void; }) { const [subject, setSubject] = useState(defaultSubject); const [note, setNote] = useState(""); const [sending, setSending] = useState(false); function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!subject.trim()) return; setSending(true); onConfirm(subject.trim(), note.trim()); } return (
e.stopPropagation()}>

Send Price Sheet

{customerCount === 1 ? "1 customer" : `${customerCount} customers`}

setSubject(e.target.value)} required aria-required="true" className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" />