diff --git a/db/client.ts b/db/client.ts index 950d9af..ee14c55 100644 --- a/db/client.ts +++ b/db/client.ts @@ -25,7 +25,39 @@ import "server-only"; import { Pool, type PoolClient } from "pg"; import { drizzle, type NodePgDatabase } from "drizzle-orm/node-postgres"; -import * as schema from "./schema"; +import * as brands from "./schema/brands"; +import * as billing from "./schema/billing"; +import * as products from "./schema/products"; +import * as stops from "./schema/stops"; +import * as customers from "./schema/customers"; +import * as orders from "./schema/orders"; +import * as brand from "./schema/brand"; +import * as wholesale from "./schema/wholesale"; +import * as waterLog from "./schema/water-log"; +import * as communications from "./schema/communications"; +import * as marketing from "./schema/marketing"; +import * as timeTracking from "./schema/time-tracking"; +import * as shipping from "./schema/shipping"; +import * as support from "./schema/support"; +import * as files from "./schema/files"; + +const schema = { + ...brands, + ...billing, + ...products, + ...stops, + ...customers, + ...orders, + ...brand, + ...wholesale, + ...waterLog, + ...communications, + ...marketing, + ...timeTracking, + ...shipping, + ...support, + ...files, +}; type Schema = typeof schema; export type Db = NodePgDatabase; diff --git a/src/app/admin/v2/stops/page.tsx b/src/app/admin/v2/stops/page.tsx index 3c99afe..f95ccaf 100644 --- a/src/app/admin/v2/stops/page.tsx +++ b/src/app/admin/v2/stops/page.tsx @@ -10,13 +10,6 @@ import StopsDatePicker from "@/components/admin/stops/StopsDatePicker"; export const dynamic = "force-dynamic"; -const emptyGrouped: Record = { - Morning: [], - Afternoon: [], - Evening: [], - Anytime: [], -} as const; - /** * Row shape returned from the stops query. The legacy `stops` table uses * separate `date` (DATE) and `time` (TEXT) columns, not a combined @@ -93,6 +86,20 @@ function bucketFor(time: string | null): TimeBucket { const BUCKET_ORDER: TimeBucket[] = ["Morning", "Afternoon", "Evening", "Anytime"]; +/** Bucket a list of stops into the four time-of-day groups. */ +function groupStopsByBucket(stops: DbStopRow[]): Record { + const grouped: Record = { + Morning: [], + Afternoon: [], + Evening: [], + Anytime: [], + }; + for (const s of stops) { + grouped[bucketFor(s.time)].push(s); + } + return grouped; +} + /** Build a Google Maps directions URL for a stop's address. */ function mapsUrlFor(stop: DbStopRow): string | null { const parts = [stop.address, stop.city, stop.state, stop.zip].filter(Boolean); @@ -252,15 +259,7 @@ export default async function StopsV2Page({ } // Group by time-of-day bucket. Use Map to preserve insertion order. - const grouped: Record = { - Morning: [...emptyGrouped.Morning], - Afternoon: [...emptyGrouped.Afternoon], - Evening: [...emptyGrouped.Evening], - Anytime: [...emptyGrouped.Anytime], - }; - for (const s of stops) { - grouped[bucketFor(s.time)].push(s); - } + const grouped = groupStopsByBucket(stops); return (
diff --git a/src/app/admin/wholesale/WholesaleClient.tsx b/src/app/admin/wholesale/WholesaleClient.tsx index 21d6bfd..a92c713 100644 --- a/src/app/admin/wholesale/WholesaleClient.tsx +++ b/src/app/admin/wholesale/WholesaleClient.tsx @@ -1508,7 +1508,7 @@ function OrdersTab({ orders, customers, brandId, onMsg, onRefresh }: { const [openActions, setOpenActions] = useState(null); const [showViewOrder, setShowViewOrder] = useState(null); - const [deleting, setDeleting] = useState(null); + const deletingRef = useRef(null); // Close actions dropdown when clicking outside useEffect(() => { @@ -1552,10 +1552,10 @@ function OrdersTab({ orders, customers, brandId, onMsg, onRefresh }: { async function handleDeleteOrder(orderId: string) { if (!confirm("Delete this order? Fulfilled and paid orders cannot be deleted. This cannot be undone.")) return; - setDeleting(orderId); + deletingRef.current = orderId; const { deleteWholesaleOrder } = await import("@/actions/wholesale"); const result = await deleteWholesaleOrder(orderId); - setDeleting(null); + deletingRef.current = null; if (result.success) { onMsg("success", "Order deleted."); onRefresh(); diff --git a/src/app/tuxedo/page.tsx b/src/app/tuxedo/page.tsx index afcd044..036328f 100644 --- a/src/app/tuxedo/page.tsx +++ b/src/app/tuxedo/page.tsx @@ -442,7 +442,7 @@ export default function TuxedoPage() { const [showSchedulePdf, setShowSchedulePdf] = useState(true); const [showWholesaleLink, setShowWholesaleLink] = useState(true); const [heroTagline, setHeroTagline] = useState(null); - const [heroImageUrl, setHeroImageUrl] = useState(null); + const heroImageUrlRef = useRef(null); const [isAdmin, setIsAdmin] = useState(false); const [customFooterText, setCustomFooterText] = useState(null); const [contactEmail, setContactEmail] = useState(null); @@ -472,7 +472,7 @@ export default function TuxedoPage() { setLogoUrlDark(s.logo_url_dark ?? null); setOlatheSweetLogoUrl(s.olathe_sweet_logo_url ?? null); setOlatheSweetLogoUrlDark(s.olathe_sweet_logo_url_dark ?? null); - setHeroImageUrl(s.hero_image_url ?? null); + heroImageUrlRef.current = s.hero_image_url ?? null; setHeroTagline(s.hero_tagline ?? null); setCustomFooterText(s.custom_footer_text ?? null); setContactEmail(s.email ?? null); diff --git a/src/components/admin/AdminOrdersPanel.tsx b/src/components/admin/AdminOrdersPanel.tsx index 7760597..155744b 100644 --- a/src/components/admin/AdminOrdersPanel.tsx +++ b/src/components/admin/AdminOrdersPanel.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useMemo, useCallback, useEffect, useTransition } from "react"; +import { useState, useMemo, useCallback, useEffect, useTransition, useRef } from "react"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; import { @@ -220,7 +220,7 @@ export default function AdminOrdersPanel({ const [selectedOrders, setSelectedOrders] = useState>(new Set()); const [bulkMarkingUp, setBulkMarkingUp] = useState(false); const [isPending, startTransition] = useTransition(); - const [filterVersion, setFilterVersion] = useState(0); + const filterVersionRef = useRef(0); const PAGE_SIZE = 20; // Mark the filter operation as a transition whenever the input @@ -228,7 +228,7 @@ export default function AdminOrdersPanel({ // give us an `isPending` flag instead of a fake useState round-trip. useEffect(() => { startTransition(() => { - setFilterVersion((v) => v + 1); + filterVersionRef.current = filterVersionRef.current + 1; }); }, [page, activeTab, search, selectedStops.length, startTransition]); diff --git a/src/components/admin/ContactImportForm.tsx b/src/components/admin/ContactImportForm.tsx index a0b7f38..e1408cd 100644 --- a/src/components/admin/ContactImportForm.tsx +++ b/src/components/admin/ContactImportForm.tsx @@ -201,7 +201,7 @@ export default function ContactImportForm({ brandId }: { brandId: string }) { const rawRowsRef = useRef([]); const setRawRows = (v: string[][]) => { rawRowsRef.current = v; }; const [importRows, setImportRows] = useState([]); - const [importing, setImporting] = useState(false); + const importingRef = useRef(false); const [result, setResult] = useState(null); const [globalError, setGlobalError] = useState(""); const [overridingMappings, setOverridingMappings] = useState< @@ -243,7 +243,7 @@ export default function ContactImportForm({ brandId }: { brandId: string }) { // Process from bucket const processResult = await processBucketImport(brandId, uploadResult.fileUrl); - setImporting(false); + importingRef.current = false; setUploadProgress(""); if (!processResult.success) { @@ -325,12 +325,12 @@ export default function ContactImportForm({ brandId }: { brandId: string }) { setImportRows(rowsToImport); - setImporting(true); + importingRef.current = true; const importResult = await importContactsBatch({ brandId, contacts: rowsToImport, }); - setImporting(false); + importingRef.current = false; if (importResult.success) { setResult(importResult.result); diff --git a/src/components/route-trace/QRScanModal.tsx b/src/components/route-trace/QRScanModal.tsx index 370636d..a0ddda0 100644 --- a/src/components/route-trace/QRScanModal.tsx +++ b/src/components/route-trace/QRScanModal.tsx @@ -61,7 +61,6 @@ export default function QRScanModal({ onClose, onScanResult }: QRScanModalProps) const [mode, setMode] = useState("camera"); const [manualInput, setManualInput] = useState(""); const [cameraError, setCameraError] = useState(null); - const [isLoading, setIsLoading] = useState(false); const [cameraStarting, setCameraStarting] = useState(true); const [detected, setDetected] = useState(false); const [scanSuccess, setScanSuccess] = useState(false); @@ -191,7 +190,6 @@ export default function QRScanModal({ onClose, onScanResult }: QRScanModalProps) e.preventDefault(); const trimmed = manualInput.trim(); if (!trimmed) return; - setIsLoading(true); onClose(); onScanResult(trimmed); } @@ -295,10 +293,10 @@ export default function QRScanModal({ onClose, onScanResult }: QRScanModalProps) )} diff --git a/src/components/route-trace/StickerPreviewModal.tsx b/src/components/route-trace/StickerPreviewModal.tsx index 3fced5c..00b6afd 100644 --- a/src/components/route-trace/StickerPreviewModal.tsx +++ b/src/components/route-trace/StickerPreviewModal.tsx @@ -85,9 +85,11 @@ export default function StickerPreviewModal({ lot, onClose }: { lot: LotDetail; const dialogRef = useRef(null); useEffect(() => { - dialogRef.current?.showModal(); + const dialog = dialogRef.current; + if (!dialog) return; + dialog.showModal(); return () => { - dialogRef.current?.close(); + dialog.close(); }; }, []); diff --git a/src/components/wholesale/DepositModal.tsx b/src/components/wholesale/DepositModal.tsx index 0f21bea..0d06547 100644 --- a/src/components/wholesale/DepositModal.tsx +++ b/src/components/wholesale/DepositModal.tsx @@ -20,9 +20,11 @@ export default function DepositModal({ order, onClose, onFulfilled }: Props) { const dialogRef = useRef(null); useEffect(() => { - dialogRef.current?.showModal(); + const dialog = dialogRef.current; + if (!dialog) return; + dialog.showModal(); return () => { - dialogRef.current?.close(); + dialog.close(); }; }, []); diff --git a/src/components/wholesale/OrderDetailsModal.tsx b/src/components/wholesale/OrderDetailsModal.tsx index b451060..5b17db6 100644 --- a/src/components/wholesale/OrderDetailsModal.tsx +++ b/src/components/wholesale/OrderDetailsModal.tsx @@ -32,9 +32,11 @@ export default function OrderDetailsModal({ order, onClose, onFulfill, onRecordD const dialogRef = useRef(null); useEffect(() => { - dialogRef.current?.showModal(); + const dialog = dialogRef.current; + if (!dialog) return; + dialog.showModal(); return () => { - dialogRef.current?.close(); + dialog.close(); }; }, []); diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts index 151ae11..040eba1 100644 --- a/src/lib/supabase.ts +++ b/src/lib/supabase.ts @@ -207,14 +207,14 @@ class MockQueryBuilder = Record 0 && - rowValue.indexOf(likeNeedle) !== -1 + rowValue.includes(likeNeedle) ); case "ilike": return ( typeof rowValue === "string" && ilikeNeedle !== null && ilikeNeedle.length > 0 && - rowValue.toLowerCase().indexOf(ilikeNeedle) !== -1 + rowValue.toLowerCase().includes(ilikeNeedle) ); case "in": return inSet !== null && inSet.has(rowValue);