import { useState, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query'; import { getTask, saveTask, getFormData } from '@/lib/tasksCache'; import type { CachedFormData } from '@/lib/tasksCache'; import { useOfflineSyncContext } from '@/contexts/OfflineSyncContext'; /** * Minimum shape the generic T must satisfy so we can supplement * task detail data from the form-cache without unsafe any-casts. */ export interface CachedTaskDetail { form?: { id: number }; fields?: unknown[]; statuses?: unknown[]; transitions?: unknown[]; tabs?: unknown[]; users?: unknown[]; } export interface UseTaskWithCacheResult { data: T | null; isLoading: boolean; isFromCache: boolean; cacheTimestamp: number | null; error: Error | null; } /** * Thin wrapper around useQuery for task detail that: * 1. Persists fetched data to IDB on every successful load. * 2. Falls back to IDB (task-cache first, then form-cache) when offline. * 3. Exposes isFromCache / cacheTimestamp for banner display. * * Internally owns the single useQuery for ['/api/tasks', taskId, 'detail']. * TaskDetail.tsx must NOT create a separate useQuery on the same key. */ export function useTaskWithCache( taskId: number | undefined, ): UseTaskWithCacheResult { const { isOnline } = useOfflineSyncContext(); const [cachedData, setCachedData] = useState(null); const [cacheTimestamp, setCacheTimestamp] = useState(null); const query = useQuery({ queryKey: ['/api/tasks', taskId, 'detail'], enabled: !!taskId, staleTime: 30_000, }); // Persist to IDB when live data loads. // Merges __related__ from existing entry so background-prefetched related-task // data is not overwritten by a regular live detail fetch. useEffect(() => { if (query.data && taskId) { getTask(taskId) .then(existing => { const prevRelated = (existing?.data as Record | undefined)?.__related__; const merged = prevRelated ? { ...(query.data as Record), __related__: prevRelated } : query.data; saveTask(taskId, merged).catch(() => {}); }) .catch(() => { saveTask(taskId, query.data).catch(() => {}); }); } }, [query.data, taskId]); // Load from IDB when offline useEffect(() => { if (!isOnline && !query.data && taskId) { getTask(taskId).then(async cached => { if (cached) { let data = cached.data as T; // Supplement with form-cache data if fields/statuses/transitions are missing const formId = data.form?.id; if (formId) { const formCached: CachedFormData | null = await getFormData(formId); if (formCached) { data = { ...data, fields: data.fields?.length ? data.fields : formCached.fields, statuses: data.statuses?.length ? data.statuses : formCached.statuses, transitions: data.transitions?.length ? data.transitions : formCached.transitions, tabs: data.tabs?.length ? data.tabs : formCached.tabs, users: data.users?.length ? data.users : formCached.users, }; } } setCachedData(data); setCacheTimestamp(cached.cachedAt); } }); } if (isOnline) { setCachedData(null); setCacheTimestamp(null); } }, [isOnline, query.data, taskId]); const isFromCache = !isOnline && !query.data && cachedData !== null; return { data: isFromCache ? cachedData : (query.data ?? null), isLoading: !isFromCache && query.isLoading, isFromCache, cacheTimestamp, error: query.error as Error | null, }; }