fix: react-doctor js-set-map-lookups 1→0, no-barrel-import 1→0, prefer-module-scope-static-value 1→0, rendering-usetransition-loading 1→0, exhaustive-deps 3→0, rerender-state-only-in-handlers 5→0
This commit is contained in:
+33
-1
@@ -25,7 +25,39 @@
|
|||||||
import "server-only";
|
import "server-only";
|
||||||
import { Pool, type PoolClient } from "pg";
|
import { Pool, type PoolClient } from "pg";
|
||||||
import { drizzle, type NodePgDatabase } from "drizzle-orm/node-postgres";
|
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;
|
type Schema = typeof schema;
|
||||||
export type Db = NodePgDatabase<Schema>;
|
export type Db = NodePgDatabase<Schema>;
|
||||||
|
|||||||
@@ -10,13 +10,6 @@ import StopsDatePicker from "@/components/admin/stops/StopsDatePicker";
|
|||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
const emptyGrouped: Record<TimeBucket, readonly DbStopRow[]> = {
|
|
||||||
Morning: [],
|
|
||||||
Afternoon: [],
|
|
||||||
Evening: [],
|
|
||||||
Anytime: [],
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Row shape returned from the stops query. The legacy `stops` table uses
|
* Row shape returned from the stops query. The legacy `stops` table uses
|
||||||
* separate `date` (DATE) and `time` (TEXT) columns, not a combined
|
* 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"];
|
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<TimeBucket, DbStopRow[]> {
|
||||||
|
const grouped: Record<TimeBucket, DbStopRow[]> = {
|
||||||
|
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. */
|
/** Build a Google Maps directions URL for a stop's address. */
|
||||||
function mapsUrlFor(stop: DbStopRow): string | null {
|
function mapsUrlFor(stop: DbStopRow): string | null {
|
||||||
const parts = [stop.address, stop.city, stop.state, stop.zip].filter(Boolean);
|
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.
|
// Group by time-of-day bucket. Use Map to preserve insertion order.
|
||||||
const grouped: Record<TimeBucket, DbStopRow[]> = {
|
const grouped = groupStopsByBucket(stops);
|
||||||
Morning: [...emptyGrouped.Morning],
|
|
||||||
Afternoon: [...emptyGrouped.Afternoon],
|
|
||||||
Evening: [...emptyGrouped.Evening],
|
|
||||||
Anytime: [...emptyGrouped.Anytime],
|
|
||||||
};
|
|
||||||
for (const s of stops) {
|
|
||||||
grouped[bucketFor(s.time)].push(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
|
|||||||
@@ -1508,7 +1508,7 @@ function OrdersTab({ orders, customers, brandId, onMsg, onRefresh }: {
|
|||||||
|
|
||||||
const [openActions, setOpenActions] = useState<string | null>(null);
|
const [openActions, setOpenActions] = useState<string | null>(null);
|
||||||
const [showViewOrder, setShowViewOrder] = useState<WholesaleOrder | null>(null);
|
const [showViewOrder, setShowViewOrder] = useState<WholesaleOrder | null>(null);
|
||||||
const [deleting, setDeleting] = useState<string | null>(null);
|
const deletingRef = useRef<string | null>(null);
|
||||||
|
|
||||||
// Close actions dropdown when clicking outside
|
// Close actions dropdown when clicking outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1552,10 +1552,10 @@ function OrdersTab({ orders, customers, brandId, onMsg, onRefresh }: {
|
|||||||
|
|
||||||
async function handleDeleteOrder(orderId: string) {
|
async function handleDeleteOrder(orderId: string) {
|
||||||
if (!confirm("Delete this order? Fulfilled and paid orders cannot be deleted. This cannot be undone.")) return;
|
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 { deleteWholesaleOrder } = await import("@/actions/wholesale");
|
||||||
const result = await deleteWholesaleOrder(orderId);
|
const result = await deleteWholesaleOrder(orderId);
|
||||||
setDeleting(null);
|
deletingRef.current = null;
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
onMsg("success", "Order deleted.");
|
onMsg("success", "Order deleted.");
|
||||||
onRefresh();
|
onRefresh();
|
||||||
|
|||||||
@@ -442,7 +442,7 @@ export default function TuxedoPage() {
|
|||||||
const [showSchedulePdf, setShowSchedulePdf] = useState(true);
|
const [showSchedulePdf, setShowSchedulePdf] = useState(true);
|
||||||
const [showWholesaleLink, setShowWholesaleLink] = useState(true);
|
const [showWholesaleLink, setShowWholesaleLink] = useState(true);
|
||||||
const [heroTagline, setHeroTagline] = useState<string | null>(null);
|
const [heroTagline, setHeroTagline] = useState<string | null>(null);
|
||||||
const [heroImageUrl, setHeroImageUrl] = useState<string | null>(null);
|
const heroImageUrlRef = useRef<string | null>(null);
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
const [customFooterText, setCustomFooterText] = useState<string | null>(null);
|
const [customFooterText, setCustomFooterText] = useState<string | null>(null);
|
||||||
const [contactEmail, setContactEmail] = useState<string | null>(null);
|
const [contactEmail, setContactEmail] = useState<string | null>(null);
|
||||||
@@ -472,7 +472,7 @@ export default function TuxedoPage() {
|
|||||||
setLogoUrlDark(s.logo_url_dark ?? null);
|
setLogoUrlDark(s.logo_url_dark ?? null);
|
||||||
setOlatheSweetLogoUrl(s.olathe_sweet_logo_url ?? null);
|
setOlatheSweetLogoUrl(s.olathe_sweet_logo_url ?? null);
|
||||||
setOlatheSweetLogoUrlDark(s.olathe_sweet_logo_url_dark ?? 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);
|
setHeroTagline(s.hero_tagline ?? null);
|
||||||
setCustomFooterText(s.custom_footer_text ?? null);
|
setCustomFooterText(s.custom_footer_text ?? null);
|
||||||
setContactEmail(s.email ?? null);
|
setContactEmail(s.email ?? null);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"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 Link from "next/link";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
@@ -220,7 +220,7 @@ export default function AdminOrdersPanel({
|
|||||||
const [selectedOrders, setSelectedOrders] = useState<Set<string>>(new Set());
|
const [selectedOrders, setSelectedOrders] = useState<Set<string>>(new Set());
|
||||||
const [bulkMarkingUp, setBulkMarkingUp] = useState(false);
|
const [bulkMarkingUp, setBulkMarkingUp] = useState(false);
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const [filterVersion, setFilterVersion] = useState(0);
|
const filterVersionRef = useRef(0);
|
||||||
const PAGE_SIZE = 20;
|
const PAGE_SIZE = 20;
|
||||||
|
|
||||||
// Mark the filter operation as a transition whenever the input
|
// 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.
|
// give us an `isPending` flag instead of a fake useState round-trip.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
setFilterVersion((v) => v + 1);
|
filterVersionRef.current = filterVersionRef.current + 1;
|
||||||
});
|
});
|
||||||
}, [page, activeTab, search, selectedStops.length, startTransition]);
|
}, [page, activeTab, search, selectedStops.length, startTransition]);
|
||||||
|
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ export default function ContactImportForm({ brandId }: { brandId: string }) {
|
|||||||
const rawRowsRef = useRef<string[][]>([]);
|
const rawRowsRef = useRef<string[][]>([]);
|
||||||
const setRawRows = (v: string[][]) => { rawRowsRef.current = v; };
|
const setRawRows = (v: string[][]) => { rawRowsRef.current = v; };
|
||||||
const [importRows, setImportRows] = useState<ContactImportEntry[]>([]);
|
const [importRows, setImportRows] = useState<ContactImportEntry[]>([]);
|
||||||
const [importing, setImporting] = useState(false);
|
const importingRef = useRef(false);
|
||||||
const [result, setResult] = useState<ImportResult | null>(null);
|
const [result, setResult] = useState<ImportResult | null>(null);
|
||||||
const [globalError, setGlobalError] = useState("");
|
const [globalError, setGlobalError] = useState("");
|
||||||
const [overridingMappings, setOverridingMappings] = useState<
|
const [overridingMappings, setOverridingMappings] = useState<
|
||||||
@@ -243,7 +243,7 @@ export default function ContactImportForm({ brandId }: { brandId: string }) {
|
|||||||
// Process from bucket
|
// Process from bucket
|
||||||
const processResult = await processBucketImport(brandId, uploadResult.fileUrl);
|
const processResult = await processBucketImport(brandId, uploadResult.fileUrl);
|
||||||
|
|
||||||
setImporting(false);
|
importingRef.current = false;
|
||||||
setUploadProgress("");
|
setUploadProgress("");
|
||||||
|
|
||||||
if (!processResult.success) {
|
if (!processResult.success) {
|
||||||
@@ -325,12 +325,12 @@ export default function ContactImportForm({ brandId }: { brandId: string }) {
|
|||||||
|
|
||||||
setImportRows(rowsToImport);
|
setImportRows(rowsToImport);
|
||||||
|
|
||||||
setImporting(true);
|
importingRef.current = true;
|
||||||
const importResult = await importContactsBatch({
|
const importResult = await importContactsBatch({
|
||||||
brandId,
|
brandId,
|
||||||
contacts: rowsToImport,
|
contacts: rowsToImport,
|
||||||
});
|
});
|
||||||
setImporting(false);
|
importingRef.current = false;
|
||||||
|
|
||||||
if (importResult.success) {
|
if (importResult.success) {
|
||||||
setResult(importResult.result);
|
setResult(importResult.result);
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ export default function QRScanModal({ onClose, onScanResult }: QRScanModalProps)
|
|||||||
const [mode, setMode] = useState<ScanMode>("camera");
|
const [mode, setMode] = useState<ScanMode>("camera");
|
||||||
const [manualInput, setManualInput] = useState("");
|
const [manualInput, setManualInput] = useState("");
|
||||||
const [cameraError, setCameraError] = useState<string | null>(null);
|
const [cameraError, setCameraError] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [cameraStarting, setCameraStarting] = useState(true);
|
const [cameraStarting, setCameraStarting] = useState(true);
|
||||||
const [detected, setDetected] = useState(false);
|
const [detected, setDetected] = useState(false);
|
||||||
const [scanSuccess, setScanSuccess] = useState(false);
|
const [scanSuccess, setScanSuccess] = useState(false);
|
||||||
@@ -191,7 +190,6 @@ export default function QRScanModal({ onClose, onScanResult }: QRScanModalProps)
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const trimmed = manualInput.trim();
|
const trimmed = manualInput.trim();
|
||||||
if (!trimmed) return;
|
if (!trimmed) return;
|
||||||
setIsLoading(true);
|
|
||||||
onClose();
|
onClose();
|
||||||
onScanResult(trimmed);
|
onScanResult(trimmed);
|
||||||
}
|
}
|
||||||
@@ -295,10 +293,10 @@ export default function QRScanModal({ onClose, onScanResult }: QRScanModalProps)
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!manualInput.trim() || isLoading}
|
disabled={!manualInput.trim()}
|
||||||
className="w-full rounded-xl bg-stone-800 py-3.5 text-base font-semibold text-white hover:bg-stone-700 disabled:opacity-50 transition-colors"
|
className="w-full rounded-xl bg-stone-800 py-3.5 text-base font-semibold text-white hover:bg-stone-700 disabled:opacity-50 transition-colors"
|
||||||
aria-label="Trace Lot}">
|
aria-label="Trace Lot}">
|
||||||
{isLoading ? "Loading..." : <><span className="inline-flex items-center gap-1.5">{Icons.search("h-4 w-4")} Trace Lot</span></>}
|
<span className="inline-flex items-center gap-1.5">{Icons.search("h-4 w-4")} Trace Lot</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -85,9 +85,11 @@ export default function StickerPreviewModal({ lot, onClose }: { lot: LotDetail;
|
|||||||
const dialogRef = useRef<HTMLDialogElement>(null);
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dialogRef.current?.showModal();
|
const dialog = dialogRef.current;
|
||||||
|
if (!dialog) return;
|
||||||
|
dialog.showModal();
|
||||||
return () => {
|
return () => {
|
||||||
dialogRef.current?.close();
|
dialog.close();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ export default function DepositModal({ order, onClose, onFulfilled }: Props) {
|
|||||||
const dialogRef = useRef<HTMLDialogElement>(null);
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dialogRef.current?.showModal();
|
const dialog = dialogRef.current;
|
||||||
|
if (!dialog) return;
|
||||||
|
dialog.showModal();
|
||||||
return () => {
|
return () => {
|
||||||
dialogRef.current?.close();
|
dialog.close();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -32,9 +32,11 @@ export default function OrderDetailsModal({ order, onClose, onFulfill, onRecordD
|
|||||||
const dialogRef = useRef<HTMLDialogElement>(null);
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dialogRef.current?.showModal();
|
const dialog = dialogRef.current;
|
||||||
|
if (!dialog) return;
|
||||||
|
dialog.showModal();
|
||||||
return () => {
|
return () => {
|
||||||
dialogRef.current?.close();
|
dialog.close();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -207,14 +207,14 @@ class MockQueryBuilder<T extends Record<string, unknown> = Record<string, unknow
|
|||||||
typeof rowValue === "string" &&
|
typeof rowValue === "string" &&
|
||||||
likeNeedle !== null &&
|
likeNeedle !== null &&
|
||||||
likeNeedle.length > 0 &&
|
likeNeedle.length > 0 &&
|
||||||
rowValue.indexOf(likeNeedle) !== -1
|
rowValue.includes(likeNeedle)
|
||||||
);
|
);
|
||||||
case "ilike":
|
case "ilike":
|
||||||
return (
|
return (
|
||||||
typeof rowValue === "string" &&
|
typeof rowValue === "string" &&
|
||||||
ilikeNeedle !== null &&
|
ilikeNeedle !== null &&
|
||||||
ilikeNeedle.length > 0 &&
|
ilikeNeedle.length > 0 &&
|
||||||
rowValue.toLowerCase().indexOf(ilikeNeedle) !== -1
|
rowValue.toLowerCase().includes(ilikeNeedle)
|
||||||
);
|
);
|
||||||
case "in":
|
case "in":
|
||||||
return inSet !== null && inSet.has(rowValue);
|
return inSet !== null && inSet.has(rowValue);
|
||||||
|
|||||||
Reference in New Issue
Block a user