diff --git a/public/sw.js b/public/sw.js index b9b49d7..080695a 100644 --- a/public/sw.js +++ b/public/sw.js @@ -26,13 +26,15 @@ self.addEventListener("install", (event) => { self.addEventListener("activate", (event) => { event.waitUntil( Promise.all([ - caches.keys().then((cacheNames) => - Promise.all( - cacheNames - .filter((name) => name !== SHELL_CACHE && name !== DATA_CACHE) - .map((name) => caches.delete(name)) - ) - ), + caches.keys().then((cacheNames) => { + const toDelete = []; + for (const name of cacheNames) { + if (name !== SHELL_CACHE && name !== DATA_CACHE) { + toDelete.push(caches.delete(name)); + } + } + return Promise.all(toDelete); + }), self.clients.claim(), ]) ); diff --git a/src/actions/ai-import.ts b/src/actions/ai-import.ts index 42ebe3d..33ebf9f 100644 --- a/src/actions/ai-import.ts +++ b/src/actions/ai-import.ts @@ -266,7 +266,15 @@ function fallbackParse( } } - const detectedType = (Object.entries(typeScores).sort((a, b) => b[1] - a[1])[0]?.[0] ?? "unknown") as ImportEntityType; + let bestEtype: string | undefined; + let bestScore = -1; + for (const [etype, score] of Object.entries(typeScores)) { + if (score > bestScore) { + bestScore = score; + bestEtype = etype; + } + } + const detectedType = (bestEtype ?? "unknown") as ImportEntityType; // Map headers to semantic fields const columnMappings: ColumnMapping = {}; @@ -335,14 +343,26 @@ function fallbackParse( // ── Import Executors ───────────────────────────────────────────────────────── async function executeProductsImport(brandId: string, rows: Record[]) { - const products = rows.map((r) => ({ - name: String(r.product_name ?? r.name ?? ""), - description: String(r.description ?? ""), - price: parseFloat(String(r.price ?? "0").replace(/[^0-9.]/g, "")) || 0, - type: normalizeFulfillmentType(String(r.product_type ?? r.type ?? "Pickup")), - active: String(r.active ?? "true").toLowerCase() !== "false", - image_url: r.image_url ? String(r.image_url) : undefined, - })).filter((p) => p.name !== ""); + const products: Array<{ + name: string; + description: string; + price: number; + type: string; + active: boolean; + image_url: string | undefined; + }> = []; + for (const r of rows) { + const name = String(r.product_name ?? r.name ?? ""); + if (name === "") continue; + products.push({ + name, + description: String(r.description ?? ""), + price: parseFloat(String(r.price ?? "0").replace(/[^0-9.]/g, "")) || 0, + type: normalizeFulfillmentType(String(r.product_type ?? r.type ?? "Pickup")), + active: String(r.active ?? "true").toLowerCase() !== "false", + image_url: r.image_url ? String(r.image_url) : undefined, + }); + } const result = await importProductsBatch(brandId, products); if (!result.success) { @@ -380,15 +400,24 @@ async function executeOrdersImport(brandId: string, rows: Record o.customer_email && o.stop_id) - .map((o) => ({ - customer_name: o.customer_name || "", - customer_email: o.customer_email, - customer_phone: o.customer_phone || "", - stop_id: o.stop_id, - items: o.items, - })); + const orders: Array<{ + customer_name: string; + customer_email: string; + customer_phone: string; + stop_id: string; + items: { product_id: string; quantity: number; fulfillment: string }[]; + }> = []; + for (const o of Object.values(orderMap)) { + if (o.customer_email && o.stop_id) { + orders.push({ + customer_name: o.customer_name || "", + customer_email: o.customer_email, + customer_phone: o.customer_phone || "", + stop_id: o.stop_id, + items: o.items, + }); + } + } return importOrdersBatch(brandId, orders).then((r) => { if (r.success) { @@ -403,16 +432,31 @@ async function executeOrdersImport(brandId: string, rows: Record[]) { - const stops = rows.map((r) => ({ - city: String(r.city ?? ""), - state: String(r.state ?? ""), - location: String(r.location ?? r.address ?? ""), - date: normalizeDate(String(r.date ?? "")), - time: String(r.time ?? ""), - address: r.address ? String(r.address) : undefined, - zip: r.zip ? String(r.zip) : undefined, - notes: r.notes ? String(r.notes) : undefined, - })).filter((s) => s.city !== "" && s.state !== ""); + const stops: Array<{ + city: string; + state: string; + location: string; + date: string; + time: string; + address: string | undefined; + zip: string | undefined; + notes: string | undefined; + }> = []; + for (const r of rows) { + const city = String(r.city ?? ""); + const state = String(r.state ?? ""); + if (city === "" || state === "") continue; + stops.push({ + city, + state, + location: String(r.location ?? r.address ?? ""), + date: normalizeDate(String(r.date ?? "")), + time: String(r.time ?? ""), + address: r.address ? String(r.address) : undefined, + zip: r.zip ? String(r.zip) : undefined, + notes: r.notes ? String(r.notes) : undefined, + }); + } return createStopsBatch(brandId, stops).then((r) => { if (r.success) { @@ -423,17 +467,33 @@ async function executeStopsImport(brandId: string, rows: Record } async function executeContactsImport(brandId: string, rows: Record[]) { - const contacts = rows.map((r) => ({ - email: r.email ? String(r.email).toLowerCase().trim() : undefined, - phone: r.phone ? String(r.phone).trim() : undefined, - first_name: r.first_name ? String(r.first_name).trim() : undefined, - last_name: r.last_name ? String(r.last_name).trim() : undefined, - full_name: r.full_name ? String(r.full_name).trim() : undefined, - tags: r.tags ? String(r.tags).split(",").map((t: string) => t.trim()) : [], - email_opt_in: r.email_opt_in !== undefined ? String(r.email_opt_in).toLowerCase() === "true" : true, - sms_opt_in: r.sms_opt_in !== undefined ? String(r.sms_opt_in).toLowerCase() === "true" : false, - external_id: r.external_id ? String(r.external_id).trim() : undefined, - })).filter((c) => c.email || c.phone); + const contacts: Array<{ + email: string | undefined; + phone: string | undefined; + first_name: string | undefined; + last_name: string | undefined; + full_name: string | undefined; + tags: string[]; + email_opt_in: boolean; + sms_opt_in: boolean; + external_id: string | undefined; + }> = []; + for (const r of rows) { + const email = r.email ? String(r.email).toLowerCase().trim() : undefined; + const phone = r.phone ? String(r.phone).trim() : undefined; + if (!email && !phone) continue; + contacts.push({ + email, + phone, + first_name: r.first_name ? String(r.first_name).trim() : undefined, + last_name: r.last_name ? String(r.last_name).trim() : undefined, + full_name: r.full_name ? String(r.full_name).trim() : undefined, + tags: r.tags ? String(r.tags).split(",").map((t: string) => t.trim()) : [], + email_opt_in: r.email_opt_in !== undefined ? String(r.email_opt_in).toLowerCase() === "true" : true, + sms_opt_in: r.sms_opt_in !== undefined ? String(r.sms_opt_in).toLowerCase() === "true" : false, + external_id: r.external_id ? String(r.external_id).trim() : undefined, + }); + } const result = await importContactsBatch({ brandId, contacts }); if (!result.success) { diff --git a/src/actions/dashboard.ts b/src/actions/dashboard.ts index dd35fb1..9788718 100644 --- a/src/actions/dashboard.ts +++ b/src/actions/dashboard.ts @@ -198,15 +198,23 @@ await getSession(); LIMIT 10`, ); - const recentOrders = recentRes.rows - .filter((o) => o.status !== "cancelled") - .map((o) => ({ + const recentOrders: Array<{ + id: string; + customer_name: string; + total: number; + status: string; + created_at: string; + }> = []; + for (const o of recentRes.rows) { + if (o.status === "cancelled") continue; + recentOrders.push({ id: o.id || "", customer_name: o.customer_name || "Guest", total: (o.total_cents || 0) / 100, status: o.status || "unknown", created_at: formatTimeAgo(o.placed_at), - })); + }); + } return { todayOrders: todayOrderCount, diff --git a/src/actions/shipping/fedex-rates.ts b/src/actions/shipping/fedex-rates.ts index fde2558..3c0e4ae 100644 --- a/src/actions/shipping/fedex-rates.ts +++ b/src/actions/shipping/fedex-rates.ts @@ -213,6 +213,7 @@ await getSession(); const adminUser = await getAdminUser(); const allowedServices = perishable ? (["FEDEX_OVERNIGHT", "FEDEX_2_DAY_AIR"] as FedExServiceType[]) : ALL_SERVICES; + const allowedServicesSet = new Set(allowedServices); // Build FedEx rate request. The SaaS rebuild doesn't store the // recipient's city/state/zip on the order — FedEx rate quotes @@ -283,7 +284,7 @@ await getSession(); const adminUser = await getAdminUser(); for (const detail of output) { const serviceType = detail.serviceType as FedExServiceType; - if (!allowedServices.includes(serviceType)) continue; + if (!allowedServicesSet.has(serviceType)) continue; const dayOption = detail.derivedDetails?.deliveryDayOfWeek ?? ""; const deliveryDate = detail.derivedDetails?.deliveryDate ?? ""; diff --git a/src/actions/square-orders.ts b/src/actions/square-orders.ts index 8077c7c..84b4859 100644 --- a/src/actions/square-orders.ts +++ b/src/actions/square-orders.ts @@ -87,6 +87,7 @@ await getSession(); const adminUser = await getAdminUser(); const errors: string[] = []; let synced = 0; + const completedStatuses = new Set(["COMPLETED", "APPROVED"]); // Determine sync start time — last sync or 30 days ago const since = settings.square_last_sync_at @@ -105,7 +106,7 @@ await getSession(); const adminUser = await getAdminUser(); // Skip if no order_id if (!payment.order_id) continue; // Skip if already completed/canceled - if (!["COMPLETED", "APPROVED"].includes(payment.status ?? "")) continue; + if (!completedStatuses.has(payment.status ?? "")) continue; const orderData = await fetchSquareOrder(settings.square_access_token, payment.order_id); const order = orderData.order; diff --git a/src/actions/tax.ts b/src/actions/tax.ts index 379bf2f..71bdb8e 100644 --- a/src/actions/tax.ts +++ b/src/actions/tax.ts @@ -66,13 +66,15 @@ await getSession(); // 1. If not collecting tax, return zero const stripe = new Stripe(stripeKey); // Build Stripe line items for tax calculation — only taxable items - const lineItems: Stripe.Tax.CalculationCreateParams.LineItem[] = params.items - .filter((item) => item.is_taxable !== false) - .map((item) => ({ + const lineItems: Stripe.Tax.CalculationCreateParams.LineItem[] = []; + for (const item of params.items) { + if (item.is_taxable === false) continue; + lineItems.push({ amount: Math.round(item.price * item.quantity * 100), // cents reference: item.id, tax_behavior: "exclusive", - })); + }); + } // If no taxable items, return zero tax if (lineItems.length === 0) { diff --git a/src/app/admin/import/ImportCenterClient.tsx b/src/app/admin/import/ImportCenterClient.tsx index 6f48b87..647a776 100644 --- a/src/app/admin/import/ImportCenterClient.tsx +++ b/src/app/admin/import/ImportCenterClient.tsx @@ -316,7 +316,10 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName: {analysis.headers.map((header) => { const mappedField = editedMappings[header]; const sampleIdx = analysis.headers.indexOf(header); - const samples = analysis.rawRows.slice(0, 3).map((r) => r[sampleIdx] ?? "").filter(Boolean); + const samples = analysis.rawRows.slice(0, 3).flatMap((r) => { + const val = r[sampleIdx] ?? ""; + return val ? [val] : []; + }); return ( {header} diff --git a/src/app/admin/orders/[id]/page.tsx b/src/app/admin/orders/[id]/page.tsx index cfe0e27..5e8e5b3 100644 --- a/src/app/admin/orders/[id]/page.tsx +++ b/src/app/admin/orders/[id]/page.tsx @@ -25,8 +25,10 @@ type OrderItem = { } | null; }; +const currencyFormatter = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }); + function formatCurrency(amount: number) { - return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(amount); + return currencyFormatter.format(amount); } export default async function OrderDetailPage({ params }: OrderDetailPageProps) { diff --git a/src/app/admin/settings/ai/AIClient.tsx b/src/app/admin/settings/ai/AIClient.tsx index 4980c22..f3a0b69 100644 --- a/src/app/admin/settings/ai/AIClient.tsx +++ b/src/app/admin/settings/ai/AIClient.tsx @@ -814,16 +814,21 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) { setResult(null); try { - const parsedTiers = priceTiers - .filter((t) => t.tier.trim() && t.price.trim()) - .map((t) => ({ tier: t.tier.trim(), price: parseFloat(t.price) || 0 })); - const parsedSales = historicalSales - .filter((s) => s.date.trim()) - .map((s) => ({ + const parsedTiers: Array<{ tier: string; price: number }> = []; + for (const t of priceTiers) { + if (t.tier.trim() && t.price.trim()) { + parsedTiers.push({ tier: t.tier.trim(), price: parseFloat(t.price) || 0 }); + } + } + const parsedSales: Array<{ date: string; units_sold: number; revenue: number }> = []; + for (const s of historicalSales) { + if (!s.date.trim()) continue; + parsedSales.push({ date: s.date.trim(), units_sold: parseInt(s.units_sold) || 0, revenue: parseFloat(s.revenue) || 0, - })); + }); + } const res = await fetch("/api/ai/pricing-advisor", { method: "POST", @@ -1591,13 +1596,15 @@ function DemandForecastTool({ brandId }: { brandId: string }) { setResult(null); try { - const parsedData = historicalData - .filter((d) => d.date.trim()) - .map((d) => ({ + const parsedData: Array<{ date: string; quantity_sold: number; stop: string }> = []; + for (const d of historicalData) { + if (!d.date.trim()) continue; + parsedData.push({ date: d.date.trim(), quantity_sold: parseInt(d.quantity_sold) || 0, stop: d.stop.trim(), - })); + }); + } const res = await fetch("/api/ai/demand-forecast", { method: "POST", diff --git a/src/app/admin/settings/integrations/IntegrationsClientPage.tsx b/src/app/admin/settings/integrations/IntegrationsClientPage.tsx index 339d070..67f69b4 100644 --- a/src/app/admin/settings/integrations/IntegrationsClientPage.tsx +++ b/src/app/admin/settings/integrations/IntegrationsClientPage.tsx @@ -102,6 +102,10 @@ const INTEGRATIONS: Integration[] = [ }, ]; +const INTEGRATIONS_WITHOUT_OPENAI: Integration[] = INTEGRATIONS.filter( + (i) => i.id !== "openai" +); + function IntegrationCard({ integration, initialCredentials, @@ -438,7 +442,7 @@ export default function IntegrationsClientPage({ brandId, brands, isPlatformAdmi ) : ( - INTEGRATIONS.filter(i => i.id !== "openai").map((integration) => ( + INTEGRATIONS_WITHOUT_OPENAI.map((integration) => ( } /> - {BUCKET_ORDER.filter((b) => grouped[b].length > 0).map((bucket) => ( + {BUCKET_ORDER.flatMap((bucket) => + grouped[bucket].length > 0 ? [bucket] : [] + ).map((bucket) => (

Headgate Name setNewName(e.target.value)} diff --git a/src/app/admin/wholesale/WholesaleClient.tsx b/src/app/admin/wholesale/WholesaleClient.tsx index 8d92095..6b905b9 100644 --- a/src/app/admin/wholesale/WholesaleClient.tsx +++ b/src/app/admin/wholesale/WholesaleClient.tsx @@ -575,7 +575,9 @@ function CustomersTab({ customers, products, brandId, onMsg, registrations = [], - {registrations.filter(r => r.account_status === "pending_approval").map(r => ( + {registrations.flatMap(r => + r.account_status === "pending_approval" ? [r] : [] + ).map(r => ( {r.company_name ?? "—"} {r.contact_name ?? "—"}
{r.email} @@ -890,7 +892,15 @@ function CustomerPricingPanel({ customer, products, onClose, onMsg }: { } return ( -
+
e.target === e.currentTarget && onClose()} + onKeyDown={(e) => { if (e.key === "Escape") onClose(); }} + >
@@ -993,7 +1003,15 @@ function PriceSheetModal({ } return ( -
+
{ if (e.key === "Escape") onClose(); }} + >
e.stopPropagation()}>
diff --git a/src/app/api/time-tracking/export/route.ts b/src/app/api/time-tracking/export/route.ts index 3dba183..4720b31 100644 --- a/src/app/api/time-tracking/export/route.ts +++ b/src/app/api/time-tracking/export/route.ts @@ -151,13 +151,17 @@ export async function GET(req: NextRequest) { else if (exportType === "payroll") { // Standard payroll export — grouped by worker with totals // Columns: Worker Name, Role, Total Hours, Regular Hours, Overtime Hours, Days Worked, Tasks + const workersById = new Map(); + for (const w of allWorkers.flat()) { + if (w) workersById.set(w.id, w); + } const workerMap = new Map; tasks: Set; dailyOTMin: number; weeklyOTMin: number; }>(); for (const log of allLogs) { if (!workerMap.has(log.worker_id)) { - const w = allWorkers.flat().find(w => w.id === log.worker_id); + const w = workersById.get(log.worker_id); workerMap.set(log.worker_id, { name: log.worker_name, role: w?.role ?? "worker", @@ -192,12 +196,16 @@ export async function GET(req: NextRequest) { // Pay period summary — one row per worker per pay period const brandName = allSettings[0]?.brand_name ?? "Farm"; const headers = ["Brand", "Worker Name", "Pay Period Start", "Pay Period End", "Total Hours", "Daily OT Hours", "Weekly OT Hours", "Status"]; + const settingsByBrand = new Map>(); + for (const s of allSettings) { + if (s && s.brand_id) settingsByBrand.set(s.brand_id, s); + } const workerMap = new Map(); for (const log of allLogs) { if (!workerMap.has(log.worker_id)) { - const settings = allSettings.find(s => s && s.brand_id === log.brandId); + const settings = settingsByBrand.get(log.brandId); workerMap.set(log.worker_id, { brandName: settings?.brand_name ?? brandName, name: log.worker_name, @@ -206,7 +214,7 @@ export async function GET(req: NextRequest) { } const entry = workerMap.get(log.worker_id)!; const totalH = log.total_minutes / 60; - const settings = allSettings.find(s => s && s.brand_id === log.brandId); + const settings = settingsByBrand.get(log.brandId); if (settings) { const dailyThr = Number(settings.daily_overtime_threshold); const weeklyThr = Number(settings.weekly_overtime_threshold); diff --git a/src/app/api/wholesale/price-sheet/route.ts b/src/app/api/wholesale/price-sheet/route.ts index 916e83f..1ffc123 100644 --- a/src/app/api/wholesale/price-sheet/route.ts +++ b/src/app/api/wholesale/price-sheet/route.ts @@ -73,9 +73,9 @@ export async function POST(req: NextRequest) { const hasCustomNote = Boolean(customNote?.trim()); // Build product rows HTML - const productRows = products - .filter(p => p.availability === "available") - .map(p => { + const productRows = products.flatMap(p => + p.availability === "available" ? [p] : [] + ).map(p => { const tiersHtml = p.price_tiers .map(t => { const max = t.max_qty === 0 ? "+" : `-${t.max_qty}`; @@ -143,7 +143,9 @@ ${hasCustomNote ? `

p.availability === "available").map(p => { + const text = `${brandName} Wholesale Price Sheet — ${generatedDate}\n${"=".repeat(50)}\n${hasCustomNote ? `\n${customNote!.trim()}\n\n` : ""}${products.flatMap(p => + p.availability === "available" ? [p] : [] + ).map(p => { const tiers = p.price_tiers.map(t => `${t.min_qty}${t.max_qty === 0 ? "+" : `-${t.max_qty}`}: $${Number(t.price).toFixed(2)}`).join(", "); return `${p.name} (${p.unit_type})\n ${tiers}`; }).join("\n\n")}\n\nGenerated ${generatedDate}. Prices subject to change.`; diff --git a/src/app/cart/CartClient.tsx b/src/app/cart/CartClient.tsx index 40d85a3..52c7f99 100644 --- a/src/app/cart/CartClient.tsx +++ b/src/app/cart/CartClient.tsx @@ -61,12 +61,16 @@ export default function CartClient() { setIncompatibleItems([]); if (hasPickupItems) { - const pickupProductIds = cart.filter((i) => i.fulfillment === "pickup").map((i) => i.id); + const pickupProductIds: string[] = []; + for (const i of cart) { + if (i.fulfillment === "pickup") pickupProductIds.push(i.id); + } checkStopProductAvailability(stop.id, pickupProductIds) .then((data) => { - const unavailable = (data ?? []) - .filter((row) => row.is_available === false) - .map((row) => row.product_id); + const unavailable: string[] = []; + for (const row of data ?? []) { + if (row.is_available === false) unavailable.push(row.product_id); + } setIncompatibleItems(unavailable); }) .catch(() => { @@ -135,7 +139,11 @@ export default function CartClient() {

Shed Pickup

- {cart.filter((i) => i.fulfillment === "pickup" && i.pickup_type === "shed").map((i) => i.description || i.name).join(", ")} + {cart.flatMap((i) => + i.fulfillment === "pickup" && i.pickup_type === "shed" + ? [i.description || i.name] + : [] + ).join(", ")}

@@ -313,7 +321,11 @@ export default function CartClient() { Shed Pickup

- {cart.filter((i) => i.fulfillment === "pickup" && i.pickup_type === "shed").map((i) => i.description || i.name).join(", ")} + {cart.flatMap((i) => + i.fulfillment === "pickup" && i.pickup_type === "shed" + ? [i.description || i.name] + : [] + ).join(", ")}

)} diff --git a/src/app/change-password/ChangePasswordForm.tsx b/src/app/change-password/ChangePasswordForm.tsx index a45d670..46dd090 100644 --- a/src/app/change-password/ChangePasswordForm.tsx +++ b/src/app/change-password/ChangePasswordForm.tsx @@ -90,7 +90,6 @@ export default function ChangePasswordForm({ userId }: { userId: string }) { minLength={8} className="w-full rounded-xl border border-zinc-700 bg-zinc-800 px-4 py-3 text-zinc-100 placeholder:text-zinc-600 outline-none focus:border-violet-500 transition-colors" placeholder="Min. 8 characters" - autoFocus />
diff --git a/src/app/change-password/page.tsx b/src/app/change-password/page.tsx index 69f9390..48e6eef 100644 --- a/src/app/change-password/page.tsx +++ b/src/app/change-password/page.tsx @@ -81,7 +81,6 @@ export default function ChangePasswordPage() { minLength={8} className="w-full rounded-xl border border-zinc-700 bg-zinc-800 px-4 py-3 text-zinc-100 placeholder:text-zinc-600 outline-none focus:border-violet-500 transition-colors" placeholder="Min. 8 characters" - autoFocus />
diff --git a/src/app/checkout/CheckoutClient.tsx b/src/app/checkout/CheckoutClient.tsx index fb5a607..0cdf9a0 100644 --- a/src/app/checkout/CheckoutClient.tsx +++ b/src/app/checkout/CheckoutClient.tsx @@ -281,7 +281,11 @@ export default function CheckoutClient() {
Shed Pickup

- {cart.filter((i) => i.fulfillment === "pickup" && i.pickup_type === "shed").map((i) => i.description || i.name).join(", ")} + {cart.flatMap((i) => + i.fulfillment === "pickup" && i.pickup_type === "shed" + ? [i.description || i.name] + : [] + ).join(", ")}

Pickup location details are included in your order confirmation.

diff --git a/src/app/indian-river-direct/about/error.tsx b/src/app/indian-river-direct/about/error.tsx index 989165d..0c28291 100644 --- a/src/app/indian-river-direct/about/error.tsx +++ b/src/app/indian-river-direct/about/error.tsx @@ -24,7 +24,7 @@ export default function ErrorPage({
- {CATEGORIES.filter(c => c !== "All").map((category) => ( + {CATEGORIES.flatMap(c => + c === "All" ? [] : [c] + ).map((category) => ( ))} diff --git a/src/app/tuxedo/about/error.tsx b/src/app/tuxedo/about/error.tsx index b58c5f7..bb7863c 100644 --- a/src/app/tuxedo/about/error.tsx +++ b/src/app/tuxedo/about/error.tsx @@ -19,7 +19,7 @@ export default function ErrorPage({ className="max-w-lg w-full text-center" > {/* Error icon */} {error && (

{error}

diff --git a/src/components/admin/AdminOrdersPanel.tsx b/src/components/admin/AdminOrdersPanel.tsx index 1561235..2701ced 100644 --- a/src/components/admin/AdminOrdersPanel.tsx +++ b/src/components/admin/AdminOrdersPanel.tsx @@ -82,8 +82,10 @@ function shortId(id: string) { return id.slice(0, 8).toUpperCase(); } +const currencyFormatter = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }); + function formatCurrency(amount: number) { - return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(amount); + return currencyFormatter.format(amount); } // Icon wrappers (lucide-react) — kept compact for inline use. @@ -452,7 +454,7 @@ export default function AdminOrdersPanel({ {showStopDropdown && ( <> -
setShowStopDropdown(false)} /> +
- -
-
- {children} +
+ {children} +
-
+ ); } \ No newline at end of file diff --git a/src/components/admin/GlassModal.tsx b/src/components/admin/GlassModal.tsx index 40cf203..ff4db17 100644 --- a/src/components/admin/GlassModal.tsx +++ b/src/components/admin/GlassModal.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; type Props = { title: string; @@ -25,113 +25,117 @@ export default function GlassModal({ compact = false, eyebrow, }: Props) { - // Lock body scroll - useEffect(() => { - document.body.style.overflow = "hidden"; - return () => { document.body.style.overflow = ""; }; - }, []); + const dialogRef = useRef(null); - // Escape key + // Native handles focus trapping, ESC, and the backdrop. useEffect(() => { - const handleEscape = (e: KeyboardEvent) => { - if (e.key === "Escape") onClose(); + const dialog = dialogRef.current; + if (!dialog) return; + if (!dialog.open) dialog.showModal(); + const onCancel = (e: Event) => { + e.preventDefault(); + onClose(); + }; + dialog.addEventListener("cancel", onCancel); + return () => { + dialog.removeEventListener("cancel", onCancel); }; - window.addEventListener("keydown", handleEscape); - return () => window.removeEventListener("keydown", handleEscape); }, [onClose]); - const handleBackdropClick = (e: React.MouseEvent) => { - if (e.target === e.currentTarget) onClose(); - }; - return ( -
- {/* Accent bar — only for non-compact (compact forms have their own internal accent) */} - {!compact && ( -
- )} - - {/* Header */}
-
- {titleIcon && !compact && ( -
- {titleIcon} -
- )} -
- {compact && eyebrow && ( -

{eyebrow}

- )} -

- {title} -

- {subtitle && !compact && ( -

- {subtitle} -

- )} -
-
- -
+
+ {titleIcon && !compact && ( +
+ {titleIcon} +
+ )} +
+ {compact && eyebrow && ( +

{eyebrow}

+ )} +

+ {title} +

+ {subtitle && !compact && ( +

+ {subtitle} +

+ )} +
+
+ +
- {/* Content */} -
- {children} + {/* Content */} +
+ {children} +
-
+
); } \ No newline at end of file diff --git a/src/components/admin/HarvestReach/AnalyticsDashboard.tsx b/src/components/admin/HarvestReach/AnalyticsDashboard.tsx index 0ad6bc5..23f91d9 100644 --- a/src/components/admin/HarvestReach/AnalyticsDashboard.tsx +++ b/src/components/admin/HarvestReach/AnalyticsDashboard.tsx @@ -236,7 +236,12 @@ export default function AnalyticsDashboard({ analytics }: Props) { const campaignCount = filteredAnalytics.length; // Best performing campaign - const bestCampaign = [...filteredAnalytics].sort((a, b) => Number(b.open_rate) - Number(a.open_rate))[0]; + let bestCampaign: (typeof filteredAnalytics)[number] | undefined; + for (const a of filteredAnalytics) { + if (!bestCampaign || Number(a.open_rate) > Number(bestCampaign.open_rate)) { + bestCampaign = a; + } + } const emptyStateIcon = Icons.chart("w-10 h-10"); diff --git a/src/components/admin/HarvestReach/SegmentBuilderPanel.tsx b/src/components/admin/HarvestReach/SegmentBuilderPanel.tsx index bfd427d..bf19255 100644 --- a/src/components/admin/HarvestReach/SegmentBuilderPanel.tsx +++ b/src/components/admin/HarvestReach/SegmentBuilderPanel.tsx @@ -391,7 +391,10 @@ function FilterBlock({ brandId, filter, onChange, onRemove }: FilterBlockProps) onChange({ params: { ...filter.params, - zip_codes: e.target.value.split(",").map((z) => z.trim()).filter(Boolean), + zip_codes: e.target.value.split(",").flatMap((z) => { + const trimmed = z.trim(); + return trimmed ? [trimmed] : []; + }), }, }) } @@ -446,7 +449,10 @@ function FilterBlock({ brandId, filter, onChange, onRemove }: FilterBlockProps) onChange({ params: { ...filter.params, - tags: e.target.value.split(",").map((t) => t.trim()).filter(Boolean), + tags: e.target.value.split(",").flatMap((t) => { + const trimmed = t.trim(); + return trimmed ? [trimmed] : []; + }), }, }) } diff --git a/src/components/admin/HarvestReach/SegmentEditModal.tsx b/src/components/admin/HarvestReach/SegmentEditModal.tsx index 526dcbd..0458357 100644 --- a/src/components/admin/HarvestReach/SegmentEditModal.tsx +++ b/src/components/admin/HarvestReach/SegmentEditModal.tsx @@ -41,8 +41,10 @@ export default function SegmentEditModal({ initialName = "", initialDescription return (
{/* Backdrop */} -
@@ -82,7 +84,6 @@ export default function SegmentEditModal({ initialName = "", initialDescription onKeyDown={(e) => e.key === "Enter" && handleSave()} placeholder="e.g. Fort Pierce Regulars" className="w-full border border-[var(--admin-border)] rounded-lg px-3 py-2.5 text-sm bg-white text-[var(--admin-text-primary)] focus:ring-2 focus:ring-[var(--admin-accent)] focus:border-[var(--admin-accent)] outline-none" - autoFocus />
diff --git a/src/components/admin/IntegrationsInner.tsx b/src/components/admin/IntegrationsInner.tsx index 695f929..bcfd89a 100644 --- a/src/components/admin/IntegrationsInner.tsx +++ b/src/components/admin/IntegrationsInner.tsx @@ -97,6 +97,10 @@ const INTEGRATIONS: Integration[] = [ }, ]; +const INTEGRATIONS_WITHOUT_OPENAI: Integration[] = INTEGRATIONS.filter( + (i) => i.id !== "openai" +); + const iconColors: Record = { AI: "bg-violet-100 text-violet-700", Email: "bg-amber-100 text-amber-700", @@ -318,7 +322,7 @@ export default function IntegrationsInner({ brandId, brands }: Props) { {/* Payment & email integrations grid */}
- {INTEGRATIONS.filter(i => i.id !== "openai").map((integration) => ( + {INTEGRATIONS_WITHOUT_OPENAI.map((integration) => ( { - // Click on the backdrop (the dialog element itself) closes; clicks on content don't - if (e.target === dialogRef.current) onClose(); - }} + aria-label="More navigation" className="w-full max-w-[640px] p-0 m-0 ml-auto h-full max-h-screen backdrop:bg-black/40" style={{ backgroundColor: "var(--color-bg)", diff --git a/src/components/admin/NewProductForm.tsx b/src/components/admin/NewProductForm.tsx index 5985f10..06ad288 100644 --- a/src/components/admin/NewProductForm.tsx +++ b/src/components/admin/NewProductForm.tsx @@ -241,6 +241,13 @@ export default function NewProductForm({ defaultBrandId = "", brands = [], lockB

JPG, PNG, WebP · 1200px max width · max 5MB (ideally under 2MB)

{ + if (e.key === "Enter" || e.key === " ") { + fileInputRef.current?.click(); + } + }} onDragOver={(e) => e.preventDefault()} onDrop={(e) => { e.preventDefault(); diff --git a/src/components/admin/OrderEditForm.tsx b/src/components/admin/OrderEditForm.tsx index c3d94b5..a105bcd 100644 --- a/src/components/admin/OrderEditForm.tsx +++ b/src/components/admin/OrderEditForm.tsx @@ -50,8 +50,10 @@ type EditableItem = { removed: boolean; }; +const currencyFormatter = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }); + function formatCurrency(amount: number) { - return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(amount); + return currencyFormatter.format(amount); } export default function OrderEditForm({ order, brandId }: OrderEditFormProps) { diff --git a/src/components/admin/OrderPaymentSection.tsx b/src/components/admin/OrderPaymentSection.tsx index 1820ec4..1be7d6c 100644 --- a/src/components/admin/OrderPaymentSection.tsx +++ b/src/components/admin/OrderPaymentSection.tsx @@ -30,8 +30,10 @@ type OrderPaymentSectionProps = { existingRefunds: Refund[]; }; +const currencyFormatter = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }); + function formatCurrency(amount: number) { - return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(amount); + return currencyFormatter.format(amount); } export default function OrderPaymentSection({ diff --git a/src/components/admin/ProductEditForm.tsx b/src/components/admin/ProductEditForm.tsx index 5adfbcd..e589111 100644 --- a/src/components/admin/ProductEditForm.tsx +++ b/src/components/admin/ProductEditForm.tsx @@ -321,6 +321,13 @@ export default function ProductEditForm({ product, brands }: ProductEditFormProp

JPG, PNG, WebP · 1200px max width · max 5MB (ideally under 2MB)

{ + if (e.key === "Enter" || e.key === " ") { + fileInputRef.current?.click(); + } + }} onDragOver={(e) => e.preventDefault()} onDrop={(e) => { e.preventDefault(); diff --git a/src/components/admin/ProductFormModal.tsx b/src/components/admin/ProductFormModal.tsx index 1f36e7d..1c92ef4 100644 --- a/src/components/admin/ProductFormModal.tsx +++ b/src/components/admin/ProductFormModal.tsx @@ -311,6 +311,13 @@ export default function ProductFormModal({
01 · Media
{ + if (e.key === "Enter" || e.key === " ") { + if (!uploading) fileInputRef.current?.click(); + } + }} onDragOver={(e) => { e.preventDefault(); setIsDrag(true); diff --git a/src/components/admin/ProductTableBody.tsx b/src/components/admin/ProductTableBody.tsx index e23fb06..5d09eaa 100644 --- a/src/components/admin/ProductTableBody.tsx +++ b/src/components/admin/ProductTableBody.tsx @@ -186,8 +186,10 @@ export default function ProductTableBody({ {openMenu === product.id && ( <> -
{ setOpenMenu(null); setConfirmDelete(null); }} />
@@ -203,8 +205,10 @@ export default function ProductTableBody({ {confirmDelete === product.id && ( <> -
{ setConfirmDelete(null); setOpenMenu(null); }} /> diff --git a/src/components/admin/ProductTableClient.tsx b/src/components/admin/ProductTableClient.tsx index 1baa5b1..fac83f6 100644 --- a/src/components/admin/ProductTableClient.tsx +++ b/src/components/admin/ProductTableClient.tsx @@ -232,8 +232,10 @@ function ProductRowBase({ {openMenu === product.id && ( <> -
{ setOpenMenu(null); setConfirmDelete(null); }} />
@@ -249,8 +251,10 @@ function ProductRowBase({ {confirmDelete === product.id && ( <> -
{ setConfirmDelete(null); setOpenMenu(null); }} />
diff --git a/src/components/admin/ScheduleImportModal.tsx b/src/components/admin/ScheduleImportModal.tsx index 0349c55..11fe765 100644 --- a/src/components/admin/ScheduleImportModal.tsx +++ b/src/components/admin/ScheduleImportModal.tsx @@ -163,6 +163,13 @@ export default function ScheduleImportModal({ brandId, onClose, onComplete }: Pr {/* Drop zone */}
{ + if (e.key === "Enter" || e.key === " ") { + fileInputRef.current?.click(); + } + }} onDragOver={(e) => { e.preventDefault(); setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={handleDrop} diff --git a/src/components/admin/ShippingFulfillmentPanel.tsx b/src/components/admin/ShippingFulfillmentPanel.tsx index 0819f1b..65f85a0 100644 --- a/src/components/admin/ShippingFulfillmentPanel.tsx +++ b/src/components/admin/ShippingFulfillmentPanel.tsx @@ -73,8 +73,10 @@ function shortId(id: string) { return id.slice(0, 8).toUpperCase(); } +const currencyFormatter = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }); + function formatCurrency(amount: number) { - return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(amount); + return currencyFormatter.format(amount); } function statusLabel(status: string) { @@ -526,8 +528,8 @@ function OrderCard({ /> )} - {SHIPPING_STATUSES.filter( - (s) => s.value !== order.shipping_status && s.value !== "label_created" + {SHIPPING_STATUSES.flatMap((s) => + s.value !== order.shipping_status && s.value !== "label_created" ? [s] : [] ).map((s) => { const isDanger = s.value === "returned"; return ( diff --git a/src/components/admin/StopTableClient.tsx b/src/components/admin/StopTableClient.tsx index 6df801a..4b5fb81 100644 --- a/src/components/admin/StopTableClient.tsx +++ b/src/components/admin/StopTableClient.tsx @@ -144,7 +144,7 @@ export default function StopTableClient({ stops, hideInternalFilterBar = false } }, [stops, search, statusFilter]); const sorted = useMemo(() => { - return [...filtered].sort((a, b) => { + return filtered.toSorted((a, b) => { let comparison = 0; switch (sortField) { case "date": @@ -845,10 +845,11 @@ function StopRow({ {openMenu === stop.id && ( <> -
{ - e.stopPropagation(); + {actionMenuId === user.id && ( <> -
setActionMenuId(null)} /> +
- -
- {/* Content */} -
{children}
+ {/* Content */} +
{children}
+
-
+ ); } \ No newline at end of file diff --git a/src/components/admin/orders/OrdersFilterButton.tsx b/src/components/admin/orders/OrdersFilterButton.tsx index 45bea57..3761d0f 100644 --- a/src/components/admin/orders/OrdersFilterButton.tsx +++ b/src/components/admin/orders/OrdersFilterButton.tsx @@ -75,7 +75,7 @@ export function OrdersFilterButton() { { if (e.target === dialogRef.current) closeDialog(); }} + aria-label="Filter orders" className="w-full max-w-[640px] p-0 m-0 ml-auto h-full max-h-screen backdrop:bg-black/40" style={{ backgroundColor: "var(--color-bg)", color: "var(--color-text)" }} > diff --git a/src/components/admin/stops/StopsList.tsx b/src/components/admin/stops/StopsList.tsx index aa72da7..f39d0dc 100644 --- a/src/components/admin/stops/StopsList.tsx +++ b/src/components/admin/stops/StopsList.tsx @@ -12,7 +12,7 @@ type Props = { // dedicated /admin/stops page. export default function StopsList({ stops }: Props) { const sorted = useMemo(() => { - return [...stops].sort((a, b) => (a.date || "").localeCompare(b.date || "")); + return stops.toSorted((a, b) => (a.date || "").localeCompare(b.date || "")); }, [stops]); if (sorted.length === 0) { diff --git a/src/components/admin/stops/StopsLocations.tsx b/src/components/admin/stops/StopsLocations.tsx index afe91fe..e15fc4b 100644 --- a/src/components/admin/stops/StopsLocations.tsx +++ b/src/components/admin/stops/StopsLocations.tsx @@ -55,13 +55,21 @@ function groupStopsByLocation(stops: Stop[]): LocationGroup[] { const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`; for (const g of map.values()) { - const venues = new Set(g.stops.map((s) => (s.location || "").trim().toLowerCase()).filter(Boolean)); + const venues = new Set( + g.stops.flatMap((s) => { + const trimmed = (s.location || "").trim().toLowerCase(); + return trimmed ? [trimmed] : []; + }) + ); g.venueCount = Math.max(1, venues.size); g.upcoming = g.stops.filter((s) => s.date >= todayStr && getStopStatus(s) !== "inactive").length; - const future = g.stops - .filter((s) => s.date >= todayStr && getStopStatus(s) !== "inactive") - .map((s) => s.date) - .sort(); + const future: string[] = []; + for (const s of g.stops) { + if (s.date >= todayStr && getStopStatus(s) !== "inactive") { + future.push(s.date); + } + } + future.sort(); g.nextDate = future[0] ?? null; g.stops.sort((a, b) => (a.date || "").localeCompare(b.date || "")); } diff --git a/src/components/cart/CartRestoredToast.tsx b/src/components/cart/CartRestoredToast.tsx index f958f05..cd6172c 100644 --- a/src/components/cart/CartRestoredToast.tsx +++ b/src/components/cart/CartRestoredToast.tsx @@ -23,7 +23,15 @@ export default function CartRestoredToast() { return (
{ + if (e.key === "Enter" || e.key === " ") { + setVisible(false); + dismissRestoredToast(); + } + }} + className="fixed bottom-6 right-6 z-50 animate-slide-up cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-green-500 rounded-2xl" onClick={() => { setVisible(false); dismissRestoredToast(); }} >
diff --git a/src/components/changelog/ChangelogFeed.tsx b/src/components/changelog/ChangelogFeed.tsx index e92c6db..f565bb0 100644 --- a/src/components/changelog/ChangelogFeed.tsx +++ b/src/components/changelog/ChangelogFeed.tsx @@ -78,7 +78,14 @@ export default function ChangelogFeed({ entries = DEFAULT_ENTRIES }: ChangelogFe return (
{ + if (e.key === "Enter" || e.key === " ") { + markAsRead(entry.id); + } + }} + className={`bg-white rounded-xl p-5 border transition-all hover:shadow-md cursor-pointer ${ isRead ? "border-gray-200 opacity-70" : "border-emerald-200 shadow-sm" }`} onClick={() => markAsRead(entry.id)} diff --git a/src/components/landing/HeroSection.tsx b/src/components/landing/HeroSection.tsx index 990d043..8c51a47 100644 --- a/src/components/landing/HeroSection.tsx +++ b/src/components/landing/HeroSection.tsx @@ -562,6 +562,9 @@ export default function HeroSection() { {/* ─── SCROLL INDICATOR ────────────────────────────────────────────────── */}
{ if (e.key === "Enter" || e.key === " ") scrollToContent(); }} className="scroll-indicator absolute bottom-12 left-1/2 -translate-x-1/2 flex flex-col items-center gap-4 cursor-pointer" onClick={scrollToContent} > diff --git a/src/components/notifications/InAppNotificationCenter.tsx b/src/components/notifications/InAppNotificationCenter.tsx index 5b1e118..97b314d 100644 --- a/src/components/notifications/InAppNotificationCenter.tsx +++ b/src/components/notifications/InAppNotificationCenter.tsx @@ -176,6 +176,14 @@ export default function NotificationCenter({ brandId, userId }: NotificationCent {notifications.map((notification) => (
{ + if (e.key === "Enter" || e.key === " ") { + if (!notification.read) markAsRead(notification.id); + if (notification.link) window.location.href = notification.link; + } + }} className={`p-4 hover:bg-gray-50 transition-colors cursor-pointer ${ !notification.read ? "bg-emerald-50/50" : "" }`} diff --git a/src/components/onboarding/OnboardingFlow.tsx b/src/components/onboarding/OnboardingFlow.tsx index 6202538..a83b3b4 100644 --- a/src/components/onboarding/OnboardingFlow.tsx +++ b/src/components/onboarding/OnboardingFlow.tsx @@ -287,6 +287,11 @@ export function InteractiveDemo({ onClose }: { onClose: () => void }) { {demoSteps.map((step, i) => (
{ + if (e.key === "Enter" || e.key === " ") setDemoStep(i); + }} className={`p-4 rounded-lg border-2 cursor-pointer transition-colors ${ i === demoStep ? "border-primary bg-primary/5" : "border-gray-200" }`} diff --git a/src/components/route-trace/FsmaReportModal.tsx b/src/components/route-trace/FsmaReportModal.tsx index 50659a6..92fc2eb 100644 --- a/src/components/route-trace/FsmaReportModal.tsx +++ b/src/components/route-trace/FsmaReportModal.tsx @@ -256,8 +256,13 @@ export default function FsmaReportModal({ brandId }: { brandId: string }) { {open && (
e.target === e.currentTarget && setOpen(false)} + onKeyDown={(e) => { if (e.key === "Escape") setOpen(false); }} style={{ backdropFilter: "blur(4px)" }} >
{/* Backdrop */} -
diff --git a/src/components/route-trace/LotDetailPanel.tsx b/src/components/route-trace/LotDetailPanel.tsx index 7ba5b61..e7bec26 100644 --- a/src/components/route-trace/LotDetailPanel.tsx +++ b/src/components/route-trace/LotDetailPanel.tsx @@ -758,7 +758,9 @@ export default function LotDetailPanel({ className="w-full rounded-xl border border-[var(--admin-border)] bg-[var(--admin-bg-subtle)]/50 px-3 py-3 text-sm text-[var(--admin-text-primary)] outline-none focus:border-[var(--admin-accent)] focus:ring-2 focus:ring-[var(--admin-accent)]/20 transition-all" > - {STATUS_FLOW.filter((s) => s !== lot.status).map((s) => { + {STATUS_FLOW.flatMap((s) => + s !== lot.status ? [s] : [] + ).map((s) => { const c = EVENT_CONFIG[s]; return (
diff --git a/src/components/route-trace/RouteTraceDashboard.tsx b/src/components/route-trace/RouteTraceDashboard.tsx index d0c954e..a6c9686 100644 --- a/src/components/route-trace/RouteTraceDashboard.tsx +++ b/src/components/route-trace/RouteTraceDashboard.tsx @@ -643,7 +643,7 @@ export default function RouteTraceDashboard({ className="rounded-lg border border-[var(--admin-border)] bg-white text-xs text-[var(--admin-text-primary)] px-2 py-1.5 sm:py-2 focus:outline-none focus:border-[var(--admin-accent)] flex-shrink-0" > - {[...new Set(haulingLots.map((l) => l.crop_type))].sort().map((c) => ( + {[...new Set(haulingLots.map((l) => l.crop_type))].toSorted().map((c) => ( ))} diff --git a/src/components/route-trace/StickerPreviewModal.tsx b/src/components/route-trace/StickerPreviewModal.tsx index 4b01f66..9da046a 100644 --- a/src/components/route-trace/StickerPreviewModal.tsx +++ b/src/components/route-trace/StickerPreviewModal.tsx @@ -84,7 +84,15 @@ export default function StickerPreviewModal({ lot, onClose }: { lot: LotDetail; const qrPreviewSize = qrSize * scale * 0.55; // scaled down for preview return ( -
+
{ if (e.key === "Escape") onClose(); }} + >
e.stopPropagation()}>
diff --git a/src/components/storefront/QuickCartSheet.tsx b/src/components/storefront/QuickCartSheet.tsx index dbb3dfd..fb7aeaa 100644 --- a/src/components/storefront/QuickCartSheet.tsx +++ b/src/components/storefront/QuickCartSheet.tsx @@ -212,7 +212,7 @@ function SheetContent({
{cartCount > 0 && ( diff --git a/src/components/water/WaterFieldClient.tsx b/src/components/water/WaterFieldClient.tsx index a081530..ab47a21 100644 --- a/src/components/water/WaterFieldClient.tsx +++ b/src/components/water/WaterFieldClient.tsx @@ -464,7 +464,6 @@ function WaterFieldInner() { placeholder={t.pinPlaceholder} className="w-full rounded-xl border-2 border-[#d4d9d3] bg-white px-6 py-6 text-center text-4xl font-bold tracking-widest text-[#1d1d1f] outline-none focus:border-[#1a4d2e] mb-4" style={{ letterSpacing: "0.3em" }} - autoFocus /> {error && (

{error}

diff --git a/src/components/wholesale/DepositModal.tsx b/src/components/wholesale/DepositModal.tsx index b6209d9..3f4822f 100644 --- a/src/components/wholesale/DepositModal.tsx +++ b/src/components/wholesale/DepositModal.tsx @@ -40,7 +40,15 @@ export default function DepositModal({ order, onClose, onFulfilled }: Props) { } return ( -
+
{ if (e.key === "Escape") onClose(); }} + >
e.stopPropagation()}>

Record Deposit

diff --git a/src/components/wholesale/OrderDetailsModal.tsx b/src/components/wholesale/OrderDetailsModal.tsx index d38f421..f742c3f 100644 --- a/src/components/wholesale/OrderDetailsModal.tsx +++ b/src/components/wholesale/OrderDetailsModal.tsx @@ -29,7 +29,15 @@ export default function OrderDetailsModal({ order, onClose, onFulfill, onRecordD const hasPhone = Boolean(order.customer_phone); return ( -

+
{ if (e.key === "Escape") onClose(); }} + >
e.stopPropagation()}> {/* Header */}
diff --git a/src/components/wholesale/admin/CustomersTab.tsx b/src/components/wholesale/admin/CustomersTab.tsx index e345c10..37b46e2 100644 --- a/src/components/wholesale/admin/CustomersTab.tsx +++ b/src/components/wholesale/admin/CustomersTab.tsx @@ -248,7 +248,9 @@ export default function CustomersTab({ - {registrations.filter(r => r.account_status === "pending_approval").map(r => ( + {registrations.flatMap(r => + r.account_status === "pending_approval" ? [r] : [] + ).map(r => ( {r.company_name ?? "—"} {r.contact_name ?? "—"}
{r.email} diff --git a/src/components/wholesale/admin/PriceSheetModal.tsx b/src/components/wholesale/admin/PriceSheetModal.tsx index fda34f3..f998e3b 100644 --- a/src/components/wholesale/admin/PriceSheetModal.tsx +++ b/src/components/wholesale/admin/PriceSheetModal.tsx @@ -28,7 +28,15 @@ export default function PriceSheetModal({ } return ( -
+
{ if (e.key === "Escape") onClose(); }} + >
e.stopPropagation()}>
diff --git a/src/context/CartContext.tsx b/src/context/CartContext.tsx index b9a1a6b..4ba3769 100644 --- a/src/context/CartContext.tsx +++ b/src/context/CartContext.tsx @@ -256,10 +256,14 @@ export function CartProvider({ children }: { children: ReactNode }) { }, []); const decreaseQuantity = useCallback((id: string) => { - setCart((prev) => - prev.map((item) => (item.id === id ? { ...item, quantity: item.quantity - 1 } : item)) - .filter((item) => item.quantity > 0) - ); + setCart((prev) => { + const next = []; + for (const item of prev) { + const updated = item.id === id ? { ...item, quantity: item.quantity - 1 } : item; + if (updated.quantity > 0) next.push(updated); + } + return next; + }); }, []); const removeFromCart = useCallback((id: string) => { diff --git a/src/lib/column-detector.ts b/src/lib/column-detector.ts index 2b1501f..6eb43de 100644 --- a/src/lib/column-detector.ts +++ b/src/lib/column-detector.ts @@ -176,9 +176,8 @@ export function detectColumnMappings( }); // Collect sample values and handle disambiguation - for (const mapping of mappings) { - const colIdx = headers.indexOf(mapping.csvColumn); - if (colIdx === -1 || mapping.field === null) continue; + for (const [colIdx, mapping] of mappings.entries()) { + if (mapping.field === null) continue; const samples: string[] = []; for (const row of rows) { @@ -223,9 +222,8 @@ export function mapRowToContactEntry( let normalizedEmail: string | null = null; let normalizedPhone: string | null = null; - for (const mapping of mappings) { + for (const [colIdx, mapping] of mappings.entries()) { if (mapping.field === null) continue; - const colIdx = mappings.indexOf(mapping); const raw = row[colIdx] ?? ""; switch (mapping.field) { @@ -252,7 +250,10 @@ export function mapRowToContactEntry( break; case "tags": entry.tags = raw - ? raw.split(/[;:,]/).map((t) => t.trim()).filter(Boolean) + ? raw.split(/[;:,]/).flatMap((t) => { + const trimmed = t.trim(); + return trimmed ? [trimmed] : []; + }) : undefined; break; case "email_opt_in": diff --git a/src/lib/csv-parsers.ts b/src/lib/csv-parsers.ts index 49f578b..05fc23c 100644 --- a/src/lib/csv-parsers.ts +++ b/src/lib/csv-parsers.ts @@ -26,9 +26,11 @@ export function parseProductCSV(csvText: string): { } const header = lines[0].split(",").map((h) => h.trim().toLowerCase()); + const headerSet = new Set(header); + const VALID_TYPES = new Set(["Pickup", "Shipping", "Pickup & Shipping"]); const required = ["name", "price", "type"]; for (const col of required) { - if (!header.includes(col)) { + if (!headerSet.has(col)) { return { success: false, error: `Missing required column: ${col}` }; } } @@ -60,7 +62,7 @@ export function parseProductCSV(csvText: string): { } const type = cols[typeIdx] ?? ""; - if (!["Pickup", "Shipping", "Pickup & Shipping"].includes(type)) { + if (!VALID_TYPES.has(type)) { errors.push({ row: i + 1, error: `Invalid type: ${type}` }); continue; } @@ -125,11 +127,13 @@ export function parseStopCSV(csvText: string): { // Build column index map — common synonyms supported const rawHeader = lines[0].split(",").map((h) => h.trim()); const header = rawHeader.map((h) => h.toLowerCase()); + const headerIndex = new Map(); + header.forEach((h, i) => headerIndex.set(h, i)); function idx(...names: string[]): number { for (const n of names) { - const i = header.indexOf(n); - if (i >= 0) return i; + const i = headerIndex.get(n); + if (i !== undefined) return i; } return -1; } @@ -215,9 +219,10 @@ export function parseOrderCSV(csvText: string): { } const header = lines[0].split(",").map((h) => h.trim().toLowerCase()); + const headerSet = new Set(header); const required = ["customer_name", "customer_email", "stop_id", "product_id", "quantity", "fulfillment"]; for (const col of required) { - if (!header.includes(col)) { + if (!headerSet.has(col)) { return { success: false, error: `Missing required column: ${col}` }; } }