fix(admin): SQL GROUP BY in reports, hydration in settings, mobile tabs scroll
- reports.ts: wrap window-function arg in SUM() so it operates on the per-group aggregate instead of the raw c.id column (PostgreSQL rejects window funcs over non-grouped, non-aggregated columns). - SettingsClient.tsx: read URL hash in useEffect instead of a lazy useState initializer to avoid server/client first-render mismatch on tab content (was triggering full client re-render in /admin/settings). - layout.tsx: add pt-16 lg:pt-0 to page-content for mobile header clearance. - ReportsDashboard.tsx: memoize DateRange on primitive inputs to break useEffect → setState → re-render loop caused by buildRange returning a new object every call. - AdminFilterTabs.tsx: add overflow-x-auto so wide tab rows scroll on mobile instead of overflowing. - use-media-query.ts: always start as false for hydration safety; callers handle the mobile/unknown branch first.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<Tab>(() => {
|
||||
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<Tab>("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 (
|
||||
<main className="min-h-screen bg-[var(--admin-bg)]">
|
||||
|
||||
@@ -50,9 +50,9 @@ export default function AdminFilterTabs({
|
||||
const sizes = sizeClasses[size];
|
||||
|
||||
return (
|
||||
<div
|
||||
<div
|
||||
className={`
|
||||
flex rounded-lg border border-[var(--admin-border)] bg-[var(--admin-card-bg-alt)]
|
||||
flex overflow-x-auto rounded-lg border border-[var(--admin-border)] bg-[var(--admin-card-bg-alt)]
|
||||
${sizes.container} ${className}
|
||||
`}
|
||||
role="tablist"
|
||||
|
||||
Reference in New Issue
Block a user