From e97eb33bf13ce897c41b9bbbe3ea1f4df1abec01 Mon Sep 17 00:00:00 2001 From: Nora Date: Fri, 26 Jun 2026 03:43:04 -0600 Subject: [PATCH] =?UTF-8?q?fix:=20react-doctor=20dialog/a11y/labels=20?= =?UTF-8?q?=E2=86=92=2068/100=20with=20973=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/actions/communications/segments.ts | 82 ---------------- src/app/admin/import/ImportCenterClient.tsx | 3 +- src/app/admin/page.tsx | 19 ++-- src/app/admin/products/page.tsx | 62 ++++++------ src/app/admin/settings/ai/AIClient.tsx | 63 +++++-------- .../water-log/headgates/HeadgatesManager.tsx | 92 +++++++++++------- src/app/admin/wholesale/WholesaleClient.tsx | 94 ++++++++++++------- src/app/api/reports/export/route.ts | 5 +- src/app/api/time-tracking/export/route.ts | 21 +++-- src/app/api/water-logs/export/route.ts | 33 ++++--- src/app/cart/CartClient.tsx | 28 ++++-- src/app/trace/[lotNumber]/page.tsx | 9 +- src/app/tuxedo/page.tsx | 20 ++-- .../employee/EmployeePortalClient.tsx | 17 ++-- src/components/admin/AIProviderPanel.tsx | 8 +- src/components/admin/AddLocationModal.tsx | 34 +++---- src/components/admin/AdminHeader.tsx | 8 +- src/components/admin/AdminSidebar.tsx | 8 +- src/components/admin/AdvancedAIPanel.tsx | 16 ++-- src/components/admin/AdvancedIntegrations.tsx | 8 +- src/components/admin/AdvancedShipping.tsx | 4 +- src/components/admin/AnalyticsDashboard.tsx | 12 +-- src/components/admin/BrandSettingsForm.tsx | 28 +++--- src/components/admin/CommandPalette.tsx | 70 +++++++------- src/components/admin/CreateUserModal.tsx | 12 +-- src/components/admin/DashboardClient.tsx | 40 ++++---- src/components/admin/EditLocationModal.tsx | 32 +++---- src/components/admin/NewProductForm.tsx | 56 +++++------ src/components/admin/OrderEditForm.tsx | 16 ++-- src/components/admin/ProductEditForm.tsx | 54 +++++------ src/components/admin/ProductFormModal.tsx | 63 +++++++------ src/components/admin/ProductsClient.tsx | 79 +++++++++------- .../admin/products/StockAdjustButton.tsx | 41 +++++--- .../route-trace/FsmaReportModal.tsx | 54 +++++++---- 34 files changed, 611 insertions(+), 580 deletions(-) diff --git a/src/actions/communications/segments.ts b/src/actions/communications/segments.ts index 23f0743..2fa811b 100644 --- a/src/actions/communications/segments.ts +++ b/src/actions/communications/segments.ts @@ -108,85 +108,3 @@ await getSession(); const adminUser = await getAdminUser(); } } -export async function upsertSegment(params: { - id?: string; - brand_id: string; - name: string; - description?: string; - rules: AudienceRules; -}): Promise { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - - if (adminUser.role === "brand_admin" && adminUser.brand_id !== params.brand_id) { - return { success: false, error: "Not authorized" }; - } - - try { - const segments = await loadSegments(params.brand_id); - const now = new Date().toISOString(); - let saved: Segment; - if (params.id) { - const idx = segments.findIndex((s) => s.id === params.id); - if (idx === -1) { - return { success: false, error: "Segment not found" }; - } - saved = { - ...segments[idx], - name: params.name, - description: params.description ?? null, - rules: params.rules, - updated_at: now, - }; - segments[idx] = saved; - } else { - saved = { - id: crypto.randomUUID(), - brand_id: params.brand_id, - name: params.name, - description: params.description ?? null, - rules: params.rules, - created_by: adminUser.id, - created_at: now, - updated_at: now, - }; - segments.push(saved); - } - await saveSegments(params.brand_id, segments); - return { success: true, segment: saved }; - } catch (err) { - return { - success: false, - error: err instanceof Error ? err.message : "Failed to save segment", - }; - } -} - -export async function deleteSegment( - segmentId: string, - brandId: string -): Promise<{ success: boolean; error?: string }> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - - if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) { - return { success: false, error: "Not authorized" }; - } - - try { - const segments = await loadSegments(brandId); - const filtered = segments.filter((s) => s.id !== segmentId); - if (filtered.length === segments.length) { - return { success: false, error: "Segment not found" }; - } - await saveSegments(brandId, filtered); - return { success: true }; - } catch (err) { - return { - success: false, - error: err instanceof Error ? err.message : "Failed to delete segment", - }; - } -} diff --git a/src/app/admin/import/ImportCenterClient.tsx b/src/app/admin/import/ImportCenterClient.tsx index 647a776..998c715 100644 --- a/src/app/admin/import/ImportCenterClient.tsx +++ b/src/app/admin/import/ImportCenterClient.tsx @@ -13,6 +13,8 @@ const ENTITY_LABELS: Record = { unknown: "Unknown", }; +const analysisLabels = ["Reading your file...", "AI is mapping columns...", "Cleaning and normalizing data...", "Finalizing preview..."]; + const ALL_FIELDS = ["ignore", "product_name", "price", "description", "product_type", "active", "image_url", "customer_name", "customer_email", "customer_phone", "stop_id", "quantity", "fulfillment", "product_id", "first_name", "last_name", "full_name", "tags", "email_opt_in", "sms_opt_in", "external_id", @@ -134,7 +136,6 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName: } const stepIndex = ["upload", "analysis", "preview", "import"].indexOf(step); - const analysisLabels = ["Reading your file...", "AI is mapping columns...", "Cleaning and normalizing data...", "Finalizing preview..."]; return (
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index a39e761..bf87e85 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -9,6 +9,16 @@ import { pool } from "@/lib/db"; const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de"; +const featureKeys = [ + "harvest_reach", + "wholesale_portal", + "water_log", + "ai_tools", + "sms_campaigns", + "square_sync", + "route_trace", +] as const; + export default async function AdminPage() { const adminUser = await getAdminUser(); const isStoreEmployee = adminUser?.role === "store_employee"; @@ -62,15 +72,6 @@ export default async function AdminPage() { enabledAddons = o.enabledAddons; } else { // Fallback to per-feature flag check (matches prior behavior) - const featureKeys = [ - "harvest_reach", - "wholesale_portal", - "water_log", - "ai_tools", - "sms_campaigns", - "square_sync", - "route_trace", - ] as const; const featureFlags = await Promise.all( featureKeys.map((key) => isFeatureEnabled(dashboardBrandId, key)), ); diff --git a/src/app/admin/products/page.tsx b/src/app/admin/products/page.tsx index 24cd9c1..f53cd1d 100644 --- a/src/app/admin/products/page.tsx +++ b/src/app/admin/products/page.tsx @@ -22,6 +22,37 @@ type ProductRow = { available_until: string | null; }; +const baseQuery = (db: Parameters[1]>[0]) => + db + .select({ + id: products.id, + name: products.name, + description: products.description, + priceCents: products.priceCents, + active: products.active, + brandId: products.brandId, + brandName: brands.name, + firstImageKey: productImages.storageKey, + firstImagePosition: productImages.position, + }) + .from(products) + .leftJoin(brands, eq(brands.id, products.brandId)) + // Pull only the lowest-position image per product. The + // `position` index on product_images (product_id, position) makes + // this cheap; a real-world scale would replace it with a + // DISTINCT ON subquery, but for the current catalog size this + // join is fine and avoids a second round-trip. + .leftJoin( + productImages, + sql`${productImages.id} = ( + SELECT id FROM product_images + WHERE product_id = ${products.id} + ORDER BY position ASC, created_at ASC + LIMIT 1 + )`, + ) + .orderBy(asc(products.name)); + export default async function AdminProductsPage() { const adminUser = await getAdminUser(); @@ -61,37 +92,6 @@ export default async function AdminProductsPage() { let productsList: ProductRow[] = []; let queryError: string | null = null; try { - const baseQuery = (db: Parameters[1]>[0]) => - db - .select({ - id: products.id, - name: products.name, - description: products.description, - priceCents: products.priceCents, - active: products.active, - brandId: products.brandId, - brandName: brands.name, - firstImageKey: productImages.storageKey, - firstImagePosition: productImages.position, - }) - .from(products) - .leftJoin(brands, eq(brands.id, products.brandId)) - // Pull only the lowest-position image per product. The - // `position` index on product_images (product_id, position) makes - // this cheap; a real-world scale would replace it with a - // DISTINCT ON subquery, but for the current catalog size this - // join is fine and avoids a second round-trip. - .leftJoin( - productImages, - sql`${productImages.id} = ( - SELECT id FROM product_images - WHERE product_id = ${products.id} - ORDER BY position ASC, created_at ASC - LIMIT 1 - )`, - ) - .orderBy(asc(products.name)); - const rows = isPlatformAdmin ? await withPlatformAdmin((db) => baseQuery(db)) : brandId diff --git a/src/app/admin/settings/ai/AIClient.tsx b/src/app/admin/settings/ai/AIClient.tsx index f3a0b69..c5967e5 100644 --- a/src/app/admin/settings/ai/AIClient.tsx +++ b/src/app/admin/settings/ai/AIClient.tsx @@ -5,6 +5,29 @@ import Link from "next/link"; import { AdminCard } from "@/components/admin/design-system"; import { setAIProviderSettings } from "@/actions/integrations/ai-providers"; +// ── Pure helpers (module scope) ────────────────────────────────────────────── + +function copyToClipboard(text: string) { + navigator.clipboard.writeText(text); +} + +const reportTypes = [ + { value: "orders-by-stop", label: "Orders by Stop" }, + { value: "sales-by-product", label: "Sales by Product" }, + { value: "fulfillment", label: "Fulfillment" }, + { value: "pickup-status", label: "Pickup Status" }, + { value: "contact-growth", label: "Contact Growth" }, + { value: "campaigns", label: "Campaign Activity" }, +]; + +const exampleQueries = [ + "Which customers haven't ordered in 45 days?", + "What products are trending this month?", + "Who are my top customers by revenue?", + "Show recent orders from the last 7 days", + "Which customers are at risk of churning?", +]; + // ── Header Icon Component ───────────────────────────────────────────────────── function AIHeaderIcon() { @@ -662,15 +685,6 @@ function ReportExplainerTool({ brandId }: { brandId: string }) { const [result, setResult] = useState<{ summary: string; keyInsights: string[]; suggestedActions: string[] } | null>(null); const [error, setError] = useState(null); - const reportTypes = [ - { value: "orders-by-stop", label: "Orders by Stop" }, - { value: "sales-by-product", label: "Sales by Product" }, - { value: "fulfillment", label: "Fulfillment" }, - { value: "pickup-status", label: "Pickup Status" }, - { value: "contact-growth", label: "Contact Growth" }, - { value: "campaigns", label: "Campaign Activity" }, - ]; - async function handleExplain() { setLoading(true); setError(null); @@ -692,10 +706,6 @@ function ReportExplainerTool({ brandId }: { brandId: string }) { } } - function copyToClipboard(text: string) { - navigator.clipboard.writeText(text); - } - return (

@@ -850,10 +860,6 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) { } } - function copyToClipboard(text: string) { - navigator.clipboard.writeText(text); - } - function addTier() { setPriceTiers([...priceTiers, { tier: "", price: "" }]); } function removeTier(i: number) { setPriceTiers(priceTiers.filter((_, idx) => idx !== i)); } function updateTier(i: number, field: keyof PriceTier, val: string) { @@ -1099,14 +1105,11 @@ function StopBlastAdvisorTool({ brandId }: { brandId: string }) { } } - function copyToClipboard(text: string) { - navigator.clipboard.writeText(text); - } - return (

- Enter details about the stop you want to send a blast for. The AI will suggest optimal timing, subject lines, content angles, and audience targeting. + Enter details about the stop you want to send a blast for. + The AI will suggest optimal timing, subject lines, content angles, and audience targeting.

@@ -1203,14 +1206,6 @@ function CustomerInsightsTool({ brandId }: { brandId: string }) { } | null>(null); const [error, setError] = useState(null); - const exampleQueries = [ - "Which customers haven't ordered in 45 days?", - "What products are trending this month?", - "Who are my top customers by revenue?", - "Show recent orders from the last 7 days", - "Which customers are at risk of churning?", - ]; - async function handleAnalyze(nlQuery?: string) { const q = nlQuery ?? query; if (!q.trim()) return; @@ -1389,10 +1384,6 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) { } } - function copyToClipboard(text: string) { - navigator.clipboard.writeText(text); - } - const validStops = stops.filter((s) => s.name.trim() && s.city.trim() && s.state.trim()); return ( @@ -1621,10 +1612,6 @@ function DemandForecastTool({ brandId }: { brandId: string }) { } } - function copyToClipboard(text: string) { - navigator.clipboard.writeText(text); - } - function addRow() { setHistoricalData([...historicalData, { date: "", quantity_sold: "", stop: "" }]); } function removeRow(i: number) { setHistoricalData(historicalData.filter((_, idx) => idx !== i)); } function updateRow(i: number, field: keyof ForecastEntry, val: string) { diff --git a/src/app/admin/water-log/headgates/HeadgatesManager.tsx b/src/app/admin/water-log/headgates/HeadgatesManager.tsx index c58171f..67a67c3 100644 --- a/src/app/admin/water-log/headgates/HeadgatesManager.tsx +++ b/src/app/admin/water-log/headgates/HeadgatesManager.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { @@ -12,6 +12,20 @@ import { } from "@/actions/water-log/admin"; import { AdminButton } from "@/components/admin/design-system"; +async function openPrintWindow(html: string) { + const blob = new Blob([html], { type: "text/html;charset=utf-8" }); + const url = URL.createObjectURL(blob); + const w = window.open(url, "_blank", "width=700,height=700"); + if (w) { + // Revoke the blob URL once the new window has had time to load the document. + w.addEventListener("load", () => URL.revokeObjectURL(url), { once: true }); + // Safety net: revoke after 60s even if the load event never fires. + setTimeout(() => URL.revokeObjectURL(url), 60_000); + } else { + URL.revokeObjectURL(url); + } +} + type Headgate = { id: string; name: string; @@ -122,6 +136,22 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) { const [deletingId, setDeletingId] = useState(null); + const editDialogRef = useRef(null); + + useEffect(() => { + const dialog = editDialogRef.current; + if (!dialog) return; + if (!dialog.open) dialog.showModal(); + const onCancel = (e: Event) => { + e.preventDefault(); + setEditHg(null); + }; + dialog.addEventListener("cancel", onCancel); + return () => { + dialog.removeEventListener("cancel", onCancel); + }; + }, [editHg]); + async function handleDelete(hg: Headgate) { if (!window.confirm(`Delete headgate "${hg.name}"? Existing log entries will be preserved.`)) return; setDeletingId(hg.id); @@ -171,21 +201,8 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) { } } - async function openPrintWindow(html: string) { - const blob = new Blob([html], { type: "text/html;charset=utf-8" }); - const url = URL.createObjectURL(blob); - const w = window.open(url, "_blank", "width=700,height=700"); - if (w) { - // Revoke the blob URL once the new window has had time to load the document. - w.addEventListener("load", () => URL.revokeObjectURL(url), { once: true }); - // Safety net: revoke after 60s even if the load event never fires. - setTimeout(() => URL.revokeObjectURL(url), 60_000); - } else { - URL.revokeObjectURL(url); - } - } - return ( +
{/* Header */}
@@ -371,17 +388,12 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) { {/* Edit Modal */} {editHg && ( -
setEditHg(null)} - onKeyDown={(e) => { - if (e.key === "Escape") setEditHg(null); - }} + className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 m-0 p-0 max-w-none max-h-none w-full h-full backdrop:bg-black/50" > -
e.stopPropagation()}> +

Edit Headgate

-
+ )} {/* QR Modal */} @@ -475,6 +487,21 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) { function QRModal({ hg, onClose, onRegenerate }: { hg: Headgate; onClose: () => void; onRegenerate: (hg: Headgate) => void }) { const [tab, setTab] = useState<"preview" | "print" | "download">("preview"); const [loading, setLoading] = useState(false); + const dialogRef = useRef(null); + + useEffect(() => { + 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); + }; + }, [onClose]); const code = hg.name.replace(/\s+/g, "-").toUpperCase().slice(0, 6) + "-1"; const qrUrl = `/api/water-qr?token=${hg.headgate_token}&size=360`; @@ -520,17 +547,12 @@ function QRModal({ hg, onClose, onRegenerate }: { hg: Headgate; onClose: () => v } return ( -
{ - if (e.key === "Escape") onClose(); - }} + className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 m-0 p-0 max-w-none max-h-none w-full h-full backdrop:bg-black/50" > -
e.stopPropagation()}> +
{/* Header */}
@@ -616,6 +638,6 @@ function QRModal({ hg, onClose, onRegenerate }: { hg: Headgate; onClose: () => v
-
+ ); } \ No newline at end of file diff --git a/src/app/admin/wholesale/WholesaleClient.tsx b/src/app/admin/wholesale/WholesaleClient.tsx index 6b905b9..96d14cf 100644 --- a/src/app/admin/wholesale/WholesaleClient.tsx +++ b/src/app/admin/wholesale/WholesaleClient.tsx @@ -50,6 +50,24 @@ function WholesaleIcon() { ); } +const STATUS_BADGE_MAP: Record = { + pending: "warning", + awaiting_deposit: "info", + confirmed: "info", + fulfilled: "success", +}; + +const STATUS_BADGE_LABEL: Record = { + pending: "Pending", + awaiting_deposit: "Awaiting Deposit", + confirmed: "Confirmed", + fulfilled: "Fulfilled", +}; + +function isValidEmail(e: string) { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e); +} + export default function WholesaleClient({ brandId }: { brandId: string }) { const [tab, setTab] = useState("dashboard"); const [msg, setMsg] = useState<{ kind: "success" | "error"; text: string } | null>(null); @@ -868,6 +886,21 @@ function CustomerPricingPanel({ customer, products, onClose, onMsg }: { const [overrides, setOverrides] = useState>({}); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); + const dialogRef = useRef(null); + + useEffect(() => { + 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); + }; + }, [onClose]); useEffect(() => { getWholesaleCustomerPricing(customer.id).then(pricing => { @@ -892,14 +925,10 @@ function CustomerPricingPanel({ customer, products, onClose, onMsg }: { } return ( -
e.target === e.currentTarget && onClose()} - onKeyDown={(e) => { if (e.key === "Escape") onClose(); }} + className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 m-0 p-0 max-w-none max-h-none w-full h-full backdrop:bg-black/40" >
@@ -974,7 +1003,7 @@ function CustomerPricingPanel({ customer, products, onClose, onMsg }: {

-
+ ); } @@ -994,6 +1023,21 @@ function PriceSheetModal({ const [subject, setSubject] = useState(defaultSubject); const [note, setNote] = useState(""); const [sending, setSending] = useState(false); + const dialogRef = useRef(null); + + useEffect(() => { + 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); + }; + }, [onClose]); function handleSubmit(e: React.FormEvent) { e.preventDefault(); @@ -1003,16 +1047,12 @@ function PriceSheetModal({ } return ( -
{ if (e.key === "Escape") onClose(); }} + className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 px-4 m-0 max-w-none max-h-none w-full h-full backdrop:bg-black/40" > -
e.stopPropagation()}> +

Send Price Sheet

@@ -1071,7 +1111,7 @@ function PriceSheetModal({
-
+ ); } @@ -2303,10 +2343,6 @@ function AddRecipientForm({ onAdd }: { onAdd: (email: string, name: string) => v const [name, setName] = useState(""); const [error, setError] = useState(""); - function isValidEmail(e: string) { - return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e); - } - function handleAdd() { if (!email.trim()) return; if (!isValidEmail(email.trim())) { @@ -2398,21 +2434,9 @@ function WholesaleLoadingSkeleton() { } function StatusBadge({ status }: { status: string }) { - const map: Record = { - pending: "warning", - awaiting_deposit: "info", - confirmed: "info", - fulfilled: "success", - }; - const label: Record = { - pending: "Pending", - awaiting_deposit: "Awaiting Deposit", - confirmed: "Confirmed", - fulfilled: "Fulfilled", - }; return ( - - {label[status] ?? status} + + {STATUS_BADGE_LABEL[status] ?? status} ); } \ No newline at end of file diff --git a/src/app/api/reports/export/route.ts b/src/app/api/reports/export/route.ts index f835e6b..a22cad7 100644 --- a/src/app/api/reports/export/route.ts +++ b/src/app/api/reports/export/route.ts @@ -15,6 +15,8 @@ import { withDb, withPlatformAdmin } from "@/db/client"; import { orders } from "@/db/schema/orders"; import { customers } from "@/db/schema/customers"; +const REPORT_HEADERS = ["id", "customer_name", "customer_email", "status", "total_cents", "placed_at"]; + export async function GET(req: NextRequest) { const adminUser = await getAdminUser(); if (!adminUser) { @@ -74,8 +76,7 @@ export async function GET(req: NextRequest) { ); if (format === "csv") { - const headers = ["id", "customer_name", "customer_email", "status", "total_cents", "placed_at"]; - const csvRows = [headers.join(",")]; + const csvRows = [REPORT_HEADERS.join(",")]; for (const r of rows) { csvRows.push( [ diff --git a/src/app/api/time-tracking/export/route.ts b/src/app/api/time-tracking/export/route.ts index 4720b31..a0517ac 100644 --- a/src/app/api/time-tracking/export/route.ts +++ b/src/app/api/time-tracking/export/route.ts @@ -29,6 +29,15 @@ function formatTime(iso: string): string { return new Date(iso).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true }); } +const QUICKBOOKS_HEADERS = ["Employee Name", "Date", "Clock In", "Clock Out", "Hours", "Task / Service Item", "Overtime Hours", "Notes"]; +const PAYROLL_HEADERS = ["Worker Name", "Role", "Days Worked", "Total Hours", "Regular Hours", "Overtime Hours", "Tasks"]; +const SUMMARY_HEADERS = ["Brand", "Worker Name", "Pay Period Start", "Pay Period End", "Total Hours", "Daily OT Hours", "Weekly OT Hours", "Status"]; +const DETAILED_HEADERS = [ + "Worker Name", "Task", "Date", "Clock In", "Clock Out", + "Lunch (min)", "Total Minutes", "Hours", "Overtime Hours", + "Submitted Via", "Notes", "Brand", +]; + function csvRow(values: (string | number | boolean | null)[]): string { return values.map(v => `"${String(v ?? "").replace(/"/g, '""')}"`).join(","); } @@ -128,7 +137,7 @@ export async function GET(req: NextRequest) { if (exportType === "quickbooks") { // QuickBooks Time import format // Columns: Employee Name, Date, Clock In, Clock Out, Hours, Task/Service Item, Overtime Hours, Notes - const headers = ["Employee Name", "Date", "Clock In", "Clock Out", "Hours", "Task / Service Item", "Overtime Hours", "Notes"]; + const headers = QUICKBOOKS_HEADERS; const rows = allLogs.map((log, i) => { const worker = allWorkers.flat().find(w => w.id === log.worker_id); const settings = allSettings.find(s => s && s.brand_id === log.brandId); @@ -174,7 +183,7 @@ export async function GET(req: NextRequest) { entry.days.add(formatDate(log.clock_in)); entry.tasks.add(log.task_name); } - const headers = ["Worker Name", "Role", "Days Worked", "Total Hours", "Regular Hours", "Overtime Hours", "Tasks"]; + const headers = PAYROLL_HEADERS; const rows = [...workerMap.values()].map(e => { const totalH = e.totalMin / 60; const otH = Math.max(0, totalH - 40); // weekly OT at 40h @@ -195,7 +204,7 @@ export async function GET(req: NextRequest) { else if (exportType === "summary") { // 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 headers = SUMMARY_HEADERS; const settingsByBrand = new Map>(); for (const s of allSettings) { if (s && s.brand_id) settingsByBrand.set(s.brand_id, s); @@ -240,11 +249,7 @@ export async function GET(req: NextRequest) { } else { // Detailed audit log - const headers = [ - "Worker Name", "Task", "Date", "Clock In", "Clock Out", - "Lunch (min)", "Total Minutes", "Hours", "Overtime Hours", - "Submitted Via", "Notes", "Brand", - ]; + const headers = DETAILED_HEADERS; const rows = allLogs.map(log => { const settings = allSettings.find(s => s && s.brand_id === log.brandId); const { regularHours, overtimeHours } = settings ? computeOvertime(log, settings) : { regularHours: 0, overtimeHours: 0 }; diff --git a/src/app/api/water-logs/export/route.ts b/src/app/api/water-logs/export/route.ts index 4ed2da5..a4403c1 100644 --- a/src/app/api/water-logs/export/route.ts +++ b/src/app/api/water-logs/export/route.ts @@ -14,6 +14,23 @@ import type { WaterLogReportRow } from "@/lib/water-log-reporting"; const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de"; +const WATER_LOG_HEADERS = [ + "When", + "Headgate", + "User", + "Measurement", + "Unit", + "Total Gallons", + "Method", + "Notes", + "Via", + "Photo URL", +]; + +function esc(s: string | null | undefined) { + return `"${(s ?? "").replace(/"/g, '""')}"`; +} + export async function GET(request: NextRequest) { const adminUser = await getAdminUser(); if (!adminUser) { @@ -43,21 +60,7 @@ export async function GET(request: NextRequest) { })); if (format === "csv") { - const headers = [ - "When", - "Headgate", - "User", - "Measurement", - "Unit", - "Total Gallons", - "Method", - "Notes", - "Via", - "Photo URL", - ]; - const esc = (s: string | null | undefined) => - `"${(s ?? "").replace(/"/g, '""')}"`; - const lines: string[] = [headers.join(",")]; + const lines: string[] = [WATER_LOG_HEADERS.join(",")]; for (const e of raw) { lines.push( [ diff --git a/src/app/cart/CartClient.tsx b/src/app/cart/CartClient.tsx index 52c7f99..d09a7d6 100644 --- a/src/app/cart/CartClient.tsx +++ b/src/app/cart/CartClient.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useRef } from "react"; import Link from "next/link"; import { useCart } from "@/context/CartContext"; import StorefrontHeader from "@/components/storefront/StorefrontHeader"; @@ -88,6 +88,23 @@ export default function CartClient() { window.location.href = "/checkout"; }, [cart.length, stopBrandMismatch, hasStopPickupItems, selectedStop, incompatibleItems, setSelectedStop, handleOpenStopPicker]); + const stopPickerDialogRef = useRef(null); + + useEffect(() => { + if (!showStopPicker) return; + const dialog = stopPickerDialogRef.current; + if (!dialog) return; + if (!dialog.open) dialog.showModal(); + const onCancel = (e: Event) => { + e.preventDefault(); + setShowStopPicker(false); + }; + dialog.addEventListener("cancel", onCancel); + return () => { + dialog.removeEventListener("cancel", onCancel); + }; + }, [showStopPicker]); + return (
{/* Background */} @@ -208,11 +225,10 @@ export default function CartClient() { {/* Stop picker modal */} {showStopPicker && ( -

Choose Pickup Stop

@@ -245,7 +261,7 @@ export default function CartClient() { Cancel
-
+ )} {/* Cart items */} diff --git a/src/app/trace/[lotNumber]/page.tsx b/src/app/trace/[lotNumber]/page.tsx index ccf8917..101b428 100644 --- a/src/app/trace/[lotNumber]/page.tsx +++ b/src/app/trace/[lotNumber]/page.tsx @@ -2,6 +2,8 @@ import { notFound } from "next/navigation"; import { getTraceChain, getLotIdByNumber } from "@/actions/route-trace/lots"; import ShareTraceButton from "@/components/route-trace/ShareTraceButton"; +const STATUS_ORDER = ["active", "in_transit", "at_shed", "packed", "delivered"]; + export async function generateMetadata({ params }: { params: Promise<{ lotNumber: string }> }) { const { lotNumber } = await params; @@ -77,8 +79,7 @@ export default async function TracePage({ params }: { params: Promise<{ lotNumbe const { lot, events, orders } = result.chain; - const statusOrder = ["active", "in_transit", "at_shed", "packed", "delivered"]; - const currentStepIndex = statusOrder.indexOf(lot.status); + const currentStepIndex = STATUS_ORDER.indexOf(lot.status); const progressPercent = currentStepIndex >= 0 ? ((currentStepIndex + 1) / 5) * 100 : 0; const totalQuantity = Number(lot.quantity_lbs ?? 0); @@ -230,12 +231,12 @@ export default async function TracePage({ params }: { params: Promise<{ lotNumbe
{done ? : icon}
-
+
); })} diff --git a/src/app/tuxedo/page.tsx b/src/app/tuxedo/page.tsx index ff95552..f13939e 100644 --- a/src/app/tuxedo/page.tsx +++ b/src/app/tuxedo/page.tsx @@ -13,6 +13,16 @@ import StorefrontFooter from "@/components/storefront/StorefrontFooter"; import BrandStylesProvider from "@/components/storefront/BrandStylesProvider"; import { ScrollReveal, ParallaxLayer, FadeOnScroll } from "@/components/ui/ScrollAnimations"; import { supabase } from "@/lib/supabase"; + +function scrollToStops() { + document.getElementById("stops")?.scrollIntoView({ behavior: "smooth" }); +} +function scrollToStory() { + document.getElementById("story")?.scrollIntoView({ behavior: "smooth" }); +} +function scrollToProducts() { + document.getElementById("products")?.scrollIntoView({ behavior: "smooth" }); +} import { getBrandSettingsPublic } from "@/actions/brand-settings"; import { gsap } from "gsap"; import { ScrollTrigger } from "gsap/ScrollTrigger"; @@ -495,16 +505,6 @@ export default function TuxedoPage() { load(); }, []); - function scrollToStops() { - document.getElementById("stops")?.scrollIntoView({ behavior: "smooth" }); - } - function scrollToStory() { - document.getElementById("story")?.scrollIntoView({ behavior: "smooth" }); - } - function scrollToProducts() { - document.getElementById("products")?.scrollIntoView({ behavior: "smooth" }); - } - // ─── GSAP SCROLL ANIMATIONS ────────────────────────────────────────────── // Now explicitly scoped + resilient guards (no more "invalid scope" warnings) useEffect(() => { diff --git a/src/app/wholesale/employee/EmployeePortalClient.tsx b/src/app/wholesale/employee/EmployeePortalClient.tsx index d57ff7a..8c819e5 100644 --- a/src/app/wholesale/employee/EmployeePortalClient.tsx +++ b/src/app/wholesale/employee/EmployeePortalClient.tsx @@ -22,16 +22,17 @@ type Props = { type Queue = "past_due" | "today" | "upcoming"; +const STATUS_BADGE_MAP: Record = { + pending: "bg-yellow-100 text-yellow-700", + awaiting_deposit: "bg-purple-100 text-purple-700", + confirmed: "bg-blue-100 text-blue-700", + fulfilled: "bg-green-100 text-green-700", + cancelled: "bg-red-100 text-red-700", +}; + function StatusBadge({ status }: { status: string }) { - const map: Record = { - pending: "bg-yellow-100 text-yellow-700", - awaiting_deposit: "bg-purple-100 text-purple-700", - confirmed: "bg-blue-100 text-blue-700", - fulfilled: "bg-green-100 text-green-700", - cancelled: "bg-red-100 text-red-700", - }; return ( - + {status.replace(/_/g, " ")} ); diff --git a/src/components/admin/AIProviderPanel.tsx b/src/components/admin/AIProviderPanel.tsx index d0961e9..d2789df 100644 --- a/src/components/admin/AIProviderPanel.tsx +++ b/src/components/admin/AIProviderPanel.tsx @@ -226,11 +226,11 @@ export default function AIProviderPanel({ brandId }: Props) {
-