From c2746d78c90ec80e7ebb9b4340593c65eba5e087 Mon Sep 17 00:00:00 2001 From: BOHA Date: Wed, 10 Jun 2026 11:40:06 +0200 Subject: [PATCH] fix(dashboard): stale punch button/KPIs after attendance changes on other pages Verified flow: clock in on dashboard -> delete the record in /attendance/admin -> navigate back: 'Zaznamenat odchod', the Pritomni KPI and the presence card still showed the pre-delete state until F5. Root cause: every attendance mutation outside the dashboard invalidated only ["attendance"], never ["dashboard"], so within the dashboard query's 60s staleTime the remount was served from cache. - useAttendanceAdmin create/bulk/edit/delete, /attendance punch + leave-request, AttendanceCreate, LeaveApproval approve/reject now also invalidate ["dashboard"] (CLAUDE.md rule: mutations invalidate every domain that embeds their data) - systemic backstop: dashboardOptions uses refetchOnMount "always" - the dashboard aggregates attendance/offers/invoices/orders/projects/leave, and domain pages can't all be expected to invalidate it; returning to the dashboard must never show pre-mutation data Co-Authored-By: Claude Fable 5 --- src/admin/hooks/useAttendanceAdmin.ts | 16 ++++++++++++++++ src/admin/lib/queries/dashboard.ts | 5 +++++ src/admin/pages/Attendance.tsx | 6 ++++-- src/admin/pages/AttendanceCreate.tsx | 2 +- src/admin/pages/LeaveApproval.tsx | 4 ++-- 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/admin/hooks/useAttendanceAdmin.ts b/src/admin/hooks/useAttendanceAdmin.ts index cea3370..5870eb1 100644 --- a/src/admin/hooks/useAttendanceAdmin.ts +++ b/src/admin/hooks/useAttendanceAdmin.ts @@ -1048,6 +1048,10 @@ export default function useAttendanceAdmin({ alert }: AlertContext) { if (result.success) { setShowCreateModal(false); queryClient.invalidateQueries({ queryKey: ["attendance"] }); + // The dashboard embeds attendance (Přítomní dnes / Docházka dnes / + // punch-button state) — without this it serves a stale cache for up + // to its staleTime after records change here. + queryClient.invalidateQueries({ queryKey: ["dashboard"] }); await fetchData(false); await new Promise((resolve) => setTimeout(resolve, 300)); alert.success( @@ -1120,6 +1124,10 @@ export default function useAttendanceAdmin({ alert }: AlertContext) { if (result.success) { setShowBulkModal(false); queryClient.invalidateQueries({ queryKey: ["attendance"] }); + // The dashboard embeds attendance (Přítomní dnes / Docházka dnes / + // punch-button state) — without this it serves a stale cache for up + // to its staleTime after records change here. + queryClient.invalidateQueries({ queryKey: ["dashboard"] }); await fetchData(false); await new Promise((resolve) => setTimeout(resolve, 300)); alert.success( @@ -1255,6 +1263,10 @@ export default function useAttendanceAdmin({ alert }: AlertContext) { if (result.success) { setShowEditModal(false); queryClient.invalidateQueries({ queryKey: ["attendance"] }); + // The dashboard embeds attendance (Přítomní dnes / Docházka dnes / + // punch-button state) — without this it serves a stale cache for up + // to its staleTime after records change here. + queryClient.invalidateQueries({ queryKey: ["dashboard"] }); await fetchData(false); await new Promise((resolve) => setTimeout(resolve, 300)); alert.success( @@ -1285,6 +1297,10 @@ export default function useAttendanceAdmin({ alert }: AlertContext) { if (result.success) { setDeleteConfirm({ show: false, record: null }); queryClient.invalidateQueries({ queryKey: ["attendance"] }); + // The dashboard embeds attendance (Přítomní dnes / Docházka dnes / + // punch-button state) — without this it serves a stale cache for up + // to its staleTime after records change here. + queryClient.invalidateQueries({ queryKey: ["dashboard"] }); await fetchData(false); alert.success( result.message || result.data?.message || "Záznam smazán", diff --git a/src/admin/lib/queries/dashboard.ts b/src/admin/lib/queries/dashboard.ts index 4696a2a..2aeb650 100644 --- a/src/admin/lib/queries/dashboard.ts +++ b/src/admin/lib/queries/dashboard.ts @@ -6,6 +6,11 @@ export const dashboardOptions = () => queryKey: ["dashboard"], queryFn: () => jsonQuery>("/api/admin/dashboard"), staleTime: 60_000, + // The dashboard aggregates MANY domains (attendance, offers, invoices, + // orders, projects, leave). Mutations in those domains can't all be + // expected to invalidate ["dashboard"], so always refetch on mount — + // navigating back to the dashboard must never show pre-mutation data. + refetchOnMount: "always", }); // require2FAOptions lives in ./settings.ts (the single definition consumers diff --git a/src/admin/pages/Attendance.tsx b/src/admin/pages/Attendance.tsx index a0a5833..df6ab70 100644 --- a/src/admin/pages/Attendance.tsx +++ b/src/admin/pages/Attendance.tsx @@ -274,7 +274,8 @@ export default function Attendance() { >({ url: () => `${API_BASE}/attendance`, method: () => "POST", - invalidate: ["attendance"], + // dashboard included: the punch-button state + presence cards live there. + invalidate: ["attendance", "dashboard"], }); const notesMutation = useApiMutation<{ notes: string }, { message?: string }>( @@ -300,7 +301,8 @@ export default function Attendance() { >({ url: () => `${API_BASE}/leave-requests`, method: () => "POST", - invalidate: ["attendance", "leave-requests", "leave", "users"], + // dashboard included: approvers see the pending-requests KPI there. + invalidate: ["attendance", "leave-requests", "leave", "users", "dashboard"], }); const [submitting, setSubmitting] = useState(false); diff --git a/src/admin/pages/AttendanceCreate.tsx b/src/admin/pages/AttendanceCreate.tsx index 217c966..faf9ac2 100644 --- a/src/admin/pages/AttendanceCreate.tsx +++ b/src/admin/pages/AttendanceCreate.tsx @@ -74,7 +74,7 @@ export default function AttendanceCreate() { const createMutation = useApiMutation({ url: () => `${API_BASE}/attendance`, method: () => "POST", - invalidate: ["attendance", "users"], + invalidate: ["attendance", "users", "dashboard"], }); const [form, setForm] = useState(() => { diff --git a/src/admin/pages/LeaveApproval.tsx b/src/admin/pages/LeaveApproval.tsx index cadacac..a2b3f05 100644 --- a/src/admin/pages/LeaveApproval.tsx +++ b/src/admin/pages/LeaveApproval.tsx @@ -161,7 +161,7 @@ export default function LeaveApproval() { >({ url: ({ id }) => `${API_BASE}/leave-requests/${id}`, method: () => "PUT", - invalidate: ["leave-requests", "leave", "attendance", "users"], + invalidate: ["leave-requests", "leave", "attendance", "users", "dashboard"], onSuccess: () => { setApproveModal({ open: false, request: null }); alert.success("Žádost byla schválena"); @@ -174,7 +174,7 @@ export default function LeaveApproval() { >({ url: ({ id }) => `${API_BASE}/leave-requests/${id}`, method: () => "PUT", - invalidate: ["leave-requests", "leave", "attendance", "users"], + invalidate: ["leave-requests", "leave", "attendance", "users", "dashboard"], onSuccess: () => { setRejectModal({ open: false, request: null }); setRejectNote("");