diff --git a/src/actions/reports.ts b/src/actions/reports.ts
index d80eea4..e54f912 100644
--- a/src/actions/reports.ts
+++ b/src/actions/reports.ts
@@ -234,12 +234,15 @@ await getSession(); const adminUser = await getAdminUser();
// `customers` replaced `communication_contacts`. `source` column is gone,
// so `imports` is always 0 and `new_contacts` is the day's net add.
+ // Wrap COUNT in SUM() so the window function operates on an aggregate,
+ // not the raw `c.id` column (which is not in GROUP BY).
const { rows } = await pool.query<{ date: string; new_contacts: number; imports: number; total: number }>(
`SELECT
d::date::text AS date,
COUNT(c.id) FILTER (WHERE c.created_at::date = d::date)::int AS new_contacts,
0::int AS imports,
- COUNT(c.id) OVER (ORDER BY d::date)::int AS total
+ SUM(COUNT(c.id) FILTER (WHERE c.created_at::date = d::date))
+ OVER (ORDER BY d::date)::int AS total
FROM generate_series($1::date, $2::date, '1 day'::interval) d
LEFT JOIN customers c
ON c.created_at::date = d::date
diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx
index 240989f..e888c9c 100644
--- a/src/app/admin/layout.tsx
+++ b/src/app/admin/layout.tsx
@@ -105,7 +105,7 @@ export default async function AdminLayout({ children }: { children: React.ReactN
{children}
diff --git a/src/components/admin/ReportsDashboard.tsx b/src/components/admin/ReportsDashboard.tsx
index c65cc16..f9f4c50 100644
--- a/src/components/admin/ReportsDashboard.tsx
+++ b/src/components/admin/ReportsDashboard.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useReducer, useCallback, useEffect } from "react";
+import { useReducer, useCallback, useEffect, useMemo } from "react";
import {
getReportsSummary,
getOrdersByStopReport,
@@ -317,7 +317,19 @@ export default function ReportsDashboard({
}) {
const [state, dispatch] = useReducer(reducer, initialBrandId, makeInitialState);
- const range: DateRange = buildRange(state.preset, state.customStart, state.customEnd);
+ // Memoize range — `buildRange` returns a new object each call (it uses
+ // `new Date()` for the end-date). Without this memo, `range` would have a
+ // fresh identity every render, which makes `fetchAll` a fresh identity
+ // every render (it's a useCallback depending on `range`), which makes the
+ // `useEffect(() => fetchAll(), [fetchAll])` below refire every render and
+ // dispatch SET_LOADING/SET_REPORTS, which re-renders → infinite loop.
+ // Memoizing on the primitive inputs pins range to user-driven changes
+ // only; the day-rollover case is intentionally not handled here (would
+ // require a separate timer to refresh `today`).
+ const range: DateRange = useMemo(
+ () => buildRange(state.preset, state.customStart, state.customEnd),
+ [state.preset, state.customStart, state.customEnd]
+ );
// Year is read from the clock; compute in the browser only to avoid
// hydration mismatches with the server. The setState is wrapped in an
diff --git a/src/components/admin/SettingsClient.tsx b/src/components/admin/SettingsClient.tsx
index 38027d8..9ac9f56 100644
--- a/src/components/admin/SettingsClient.tsx
+++ b/src/components/admin/SettingsClient.tsx
@@ -135,14 +135,17 @@ export default function SettingsClient({
paymentSettings,
currentUser,
}: Props) {
- // Initial tab is determined by URL hash if present, otherwise "general".
- // No useEffect needed — we read window.location.hash during the lazy
- // initializer on the client. On the server we default to "general".
- const [activeTab, setActiveTab] = useState
(() => {
- if (typeof window === "undefined") return "general";
+ // Server renders the default tab; client picks up the URL hash after mount
+ // so the initial server HTML matches the first client render (avoids
+ // hydration mismatch). Reading window.location.hash in a lazy initializer
+ // is unsafe because the server has no hash and would render a different
+ // tab than the client.
+ const [activeTab, setActiveTab] = useState("general");
+ useEffect(() => {
const hash = window.location.hash.slice(1);
- return TABS.find((t) => t.hash === hash)?.id ?? "general";
- });
+ const tab = TABS.find((t) => t.hash === hash)?.id;
+ if (tab) setActiveTab(tab);
+ }, []);
return (
diff --git a/src/components/admin/design-system/AdminFilterTabs.tsx b/src/components/admin/design-system/AdminFilterTabs.tsx
index cac1aaa..04d8bf6 100644
--- a/src/components/admin/design-system/AdminFilterTabs.tsx
+++ b/src/components/admin/design-system/AdminFilterTabs.tsx
@@ -50,9 +50,9 @@ export default function AdminFilterTabs({
const sizes = sizeClasses[size];
return (
- (() => {
- if (typeof window === "undefined") return false;
- return window.matchMedia(query).matches;
- });
+ const [matches, setMatches] = useState(false);
useEffect(() => {
if (typeof window === "undefined") return;
const mq = window.matchMedia(query);
+ setMatches(mq.matches);
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler);