diff --git a/src/app/admin/wholesale/WholesaleClient.tsx b/src/app/admin/wholesale/WholesaleClient.tsx index 3c2ec0d..106024e 100644 --- a/src/app/admin/wholesale/WholesaleClient.tsx +++ b/src/app/admin/wholesale/WholesaleClient.tsx @@ -1,57 +1,41 @@ "use client"; -import { useState, useEffect, useRef } from "react"; -import crypto from "crypto"; +import { useEffect, useState } from "react"; 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, 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 { getPendingWholesaleRegistrations } 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"; +import { PageHeader, AdminFilterTabs } from "@/components/admin/design-system"; +import WholesaleIcon from "@/components/wholesale/admin/WholesaleIcon"; +import WholesaleLoadingSkeleton from "@/components/wholesale/admin/WholesaleLoadingSkeleton"; +import DashboardTab from "@/components/wholesale/admin/DashboardTab"; +import CustomersTab from "@/components/wholesale/admin/CustomersTab"; +import ProductsTab from "@/components/wholesale/admin/ProductsTab"; +import OrdersTab from "@/components/wholesale/admin/OrdersTab"; +import SettingsTab from "@/components/wholesale/admin/SettingsTab"; +import type { PendingRegistration, WebhookActivityEntry, MsgKind } from "@/components/wholesale/admin/types"; type Tab = "dashboard" | "customers" | "products" | "orders" | "settings"; -// SVG Icon for Wholesale - defined at module level to prevent recreation on each render -function WholesaleIcon() { - return ( - - - - ); -} - +/** + * Wholesale Portal admin shell. Owns the data-fetch + tab-state and delegates + * rendering to the per-tab components under `src/components/wholesale/admin/`. + */ export default function WholesaleClient({ brandId }: { brandId: string }) { const [tab, setTab] = useState("dashboard"); - const [msg, setMsg] = useState<{ kind: "success" | "error"; text: string } | null>(null); + const [msg, setMsg] = useState<{ kind: MsgKind; text: string } | null>(null); const [adminUser, setAdminUser] = useState(null); // Data state @@ -60,15 +44,9 @@ export default function WholesaleClient({ brandId }: { brandId: string }) { const [products, setProducts] = useState([]); const [settings, setSettings] = useState(null); const [stats, setStats] = useState(null); - const [registrations, setRegistrations] = useState>([]); + const [registrations, setRegistrations] = useState([]); const [loading, setLoading] = useState(true); - const [webhookActivity, setWebhookActivity] = useState>([]); + const [webhookActivity, setWebhookActivity] = useState([]); useEffect(() => { async function load() { @@ -89,14 +67,14 @@ export default function WholesaleClient({ brandId }: { brandId: string }) { setProducts(p); setSettings(s); setStats(st); - setRegistrations(r as typeof registrations); - setWebhookActivity(wa); + setRegistrations(r as PendingRegistration[]); + setWebhookActivity(wa as WebhookActivityEntry[]); setLoading(false); } load(); }, [brandId]); - function showMsg(kind: "success" | "error", text: string) { + function showMsg(kind: MsgKind, text: string) { setMsg({ kind, text }); setTimeout(() => setMsg(null), 4000); } @@ -117,13 +95,10 @@ export default function WholesaleClient({ brandId }: { brandId: string }) { { value: "settings", label: "Settings" }, ]; - // Declare icon reference for PageHeader - const pageIcon = ; - return (
} title="Wholesale Portal" subtitle="Manage wholesale orders, customers, and products" className="mb-0" @@ -150,12 +125,10 @@ export default function WholesaleClient({ brandId }: { brandId: string }) {
)} - {tab === "dashboard" && ( + {tab === "dashboard" && stats && ( )} @@ -168,7 +141,7 @@ export default function WholesaleClient({ brandId }: { brandId: string }) { registrations={registrations} onRefresh={async () => { const r = await getPendingWholesaleRegistrations(brandId); - setRegistrations(r as typeof registrations); + setRegistrations(r as PendingRegistration[]); }} /> )} @@ -210,2183 +183,4 @@ export default function WholesaleClient({ brandId }: { brandId: string }) { ); -} - -// ── 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) => ( - - - - - - - - - ))} - -
InvoiceCustomerPickup DateTotalStatusPayment
{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) => ( - - - - - - - - ))} - -
EventOrderStatusAttemptsSent 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 => ( - - - - - - - - ))} - -
CompanyContactStatusRegistered
{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} - /> - CompanyContactStatusCreditDepositsCreated
-
-
- - - -
-

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 ( - - - - - - - ); - })} - -
ProductStandard PriceOverride 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)]" - /> -
-
- -