From 880c52227a34a65c177ec01dfa6126e6219fe603 Mon Sep 17 00:00:00 2001 From: Nora Date: Thu, 25 Jun 2026 21:37:21 -0600 Subject: [PATCH] refactor(wholesale): split 2391-line WholesaleClient into focused modules The Wholesale Portal admin client was the largest single file in the app (2391 lines) and mixed 11 distinct UI concerns in one module. Split into focused files under src/components/wholesale/admin/: types.ts shared types (MsgFn, PendingRegistration, ...) WholesaleIcon.tsx header SVG WholesaleLoadingSkeleton.tsx loading state StatusBadge.tsx order status pill AddRecipientForm.tsx notification recipient input PriceSheetModal.tsx bulk price-sheet send confirmation CustomerPricingPanel.tsx per-customer pricing overrides WebhookSettingsSection.tsx outbound webhook config + test dispatch DashboardTab.tsx stat cards + recent orders + webhook feed ProductsTab.tsx wholesale product CRUD CustomersTab.tsx customers + registrations + pricing OrdersTab.tsx orders + bulk ops + deposit modals SettingsTab.tsx portal settings + webhook + recipients WholesaleClient.tsx is now a 189-line shell that owns data loading and tab routing. No behavior changes; the original logic was preserved verbatim. Mechanical extraction only. Also fixed an existing import bug where getPendingWholesaleRegistrations was being imported from @/actions/wholesale instead of @/actions/wholesale-register (typecheck caught it). Verified: typecheck clean for new code (pre-existing dahlia/fetch mock errors unrelated). Build compiles through to the same pre-existing billing type error. Zero new lint warnings introduced. Refactor-progress tracked at /tmp/refactor-routecomm.md. --- src/app/admin/wholesale/WholesaleClient.tsx | 2256 +---------------- .../wholesale/admin/AddRecipientForm.tsx | 52 + .../wholesale/admin/CustomerPricingPanel.tsx | 124 + .../wholesale/admin/CustomersTab.tsx | 529 ++++ .../wholesale/admin/DashboardTab.tsx | 126 + src/components/wholesale/admin/OrdersTab.tsx | 540 ++++ .../wholesale/admin/PriceSheetModal.tsx | 93 + .../wholesale/admin/ProductsTab.tsx | 264 ++ .../wholesale/admin/SettingsTab.tsx | 287 +++ .../wholesale/admin/StatusBadge.tsx | 25 + .../admin/WebhookSettingsSection.tsx | 167 ++ .../wholesale/admin/WholesaleIcon.tsx | 9 + .../admin/WholesaleLoadingSkeleton.tsx | 47 + src/components/wholesale/admin/types.ts | 29 + 14 files changed, 2317 insertions(+), 2231 deletions(-) create mode 100644 src/components/wholesale/admin/AddRecipientForm.tsx create mode 100644 src/components/wholesale/admin/CustomerPricingPanel.tsx create mode 100644 src/components/wholesale/admin/CustomersTab.tsx create mode 100644 src/components/wholesale/admin/DashboardTab.tsx create mode 100644 src/components/wholesale/admin/OrdersTab.tsx create mode 100644 src/components/wholesale/admin/PriceSheetModal.tsx create mode 100644 src/components/wholesale/admin/ProductsTab.tsx create mode 100644 src/components/wholesale/admin/SettingsTab.tsx create mode 100644 src/components/wholesale/admin/StatusBadge.tsx create mode 100644 src/components/wholesale/admin/WebhookSettingsSection.tsx create mode 100644 src/components/wholesale/admin/WholesaleIcon.tsx create mode 100644 src/components/wholesale/admin/WholesaleLoadingSkeleton.tsx create mode 100644 src/components/wholesale/admin/types.ts 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)]" - /> -
-
- -