import { useCallback, useEffect, useState } from 'react' import * as api from '@/services/api' import { useLatestRequest } from './useLatestRequest' import { useWorkspace } from '@/context' import type { DayOverride, DayOverrideCreateRequest } from '@/types' interface UseDayOverridesResult { data: DayOverride[] /** True only while the initial (or workspace-switch) load is in flight — * safe to gate a full-page blocking spinner on. */ isLoading: boolean /** True while a post-mutation background refresh is in flight. Never true * at the same time as `isLoading` — see `useLeaveWishes.ts`'s doc comment * for why a mutation's refetch must not flip the initial-load flag. */ isRefreshing: boolean error: string | null upsert: (req: DayOverrideCreateRequest) => Promise remove: (id: string) => Promise } export function useDayOverrides(workspaceId: string): UseDayOverridesResult { const [data, setData] = useState([]) const [isLoading, setIsLoading] = useState(false) const [isRefreshing, setIsRefreshing] = useState(false) const [error, setError] = useState(null) const { start, isCurrent } = useLatestRequest() const { bumpPlanVersion } = useWorkspace() const fetch = useCallback( async (options?: { background?: boolean }) => { const requestId = start() if (!workspaceId) return const background = options?.background ?? false const setBusy = background ? setIsRefreshing : setIsLoading setBusy(true) setError(null) try { const result = await api.listDayOverridesByWorkspace(workspaceId) if (!isCurrent(requestId)) return setData(result) } catch (err: unknown) { if (!isCurrent(requestId)) return setError(err instanceof Error ? err.message : 'Failed to load day overrides') } finally { // Unguarded: `isCurrent` decides which response's DATA wins, not // whether a superseded call's own busy flag gets cleared — a stale // call must still release the flag IT raised, or it can strand // that flag true forever (see usePlannedDays.ts for the hang this // exact guard caused there). setBusy(false) } }, [workspaceId, start, isCurrent], ) useEffect(() => { void fetch() }, [fetch]) const upsert = useCallback( async (req: DayOverrideCreateRequest) => { await api.upsertDayOverride(req) await fetch({ background: true }) // An override changes which days count as free/working, which feeds // the at-risk-days analytics — bump the shared invalidation counter // so other hook instances (e.g. useAnalytics) refetch. See // `WorkspaceContext.planVersion`. bumpPlanVersion() }, [fetch, bumpPlanVersion], ) const remove = useCallback( async (id: string) => { await api.deleteDayOverride(id) await fetch({ background: true }) bumpPlanVersion() }, [fetch, bumpPlanVersion], ) return { data, isLoading, isRefreshing, error, upsert, remove } }