import { useCallback, useEffect, useState } from 'react' import * as api from '@/services/api' import { useLatestRequest } from './useLatestRequest' import type { LeaveWish, LeaveWishCreateRequest, LeaveWishScope, LeaveWishUpdateRequest, } from '@/types' interface UseLeaveWishesResult { data: LeaveWish[] /** 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`; a consumer that renders a blocking * spinner should NOT key off this — it exists so a mutation's list refetch * doesn't unmount whatever is currently showing `data`. */ isRefreshing: boolean error: string | null refresh: () => Promise add: (req: LeaveWishCreateRequest) => Promise /** `scope` decides how far the edit reaches on a repeating wish: the * clicked year only (default) or every year sharing its `seriesId`. */ update: (id: string, req: LeaveWishUpdateRequest, scope?: LeaveWishScope) => Promise /** `scope: 'series'` removes every year of a repeating wish. */ remove: (id: string, scope?: LeaveWishScope) => Promise } /** CRUD over a workspace's recurring leave wishes (`/api/leave-wishes`). * * `fetch` is called both on initial mount/workspace-change and after every * mutation (to keep `data` in sync with the server). Those two cases need * different loading semantics: the initial load should block the page, but * a post-mutation refresh must not — it happens while the user is mid-edit * of a form built from `data`, and flipping `isLoading` would unmount that * form. `background: true` routes the fetch through `isRefreshing` instead * of `isLoading` so callers can tell the two apart. */ export function useLeaveWishes(workspaceId: string | null): UseLeaveWishesResult { const [data, setData] = useState([]) const [isLoading, setIsLoading] = useState(false) const [isRefreshing, setIsRefreshing] = useState(false) const [error, setError] = useState(null) const { start, isCurrent } = useLatestRequest() 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.getLeaveWishes(workspaceId) if (!isCurrent(requestId)) return setData(result) } catch (err: unknown) { if (!isCurrent(requestId)) return setError(err instanceof Error ? err.message : 'Failed to load leave wishes') } 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 add = useCallback( async (req: LeaveWishCreateRequest) => { const created = await api.createLeaveWish(req) await fetch({ background: true }) return created }, [fetch], ) const update = useCallback( async (id: string, req: LeaveWishUpdateRequest, scope: LeaveWishScope = 'occurrence') => { const updated = await api.updateLeaveWish(id, req, scope) await fetch({ background: true }) return updated }, [fetch], ) const remove = useCallback( async (id: string, scope: LeaveWishScope = 'occurrence') => { await api.deleteLeaveWish(id, scope) await fetch({ background: true }) }, [fetch], ) return { data, isLoading, isRefreshing, error, refresh: fetch, add, update, remove } }