- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
112 lines
3.7 KiB
TypeScript
112 lines
3.7 KiB
TypeScript
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<T> {
|
|
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<T extends CachedTaskDetail>(
|
|
taskId: number | undefined,
|
|
): UseTaskWithCacheResult<T> {
|
|
const { isOnline } = useOfflineSyncContext();
|
|
|
|
const [cachedData, setCachedData] = useState<T | null>(null);
|
|
const [cacheTimestamp, setCacheTimestamp] = useState<number | null>(null);
|
|
|
|
const query = useQuery<T>({
|
|
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<string, unknown> | undefined)?.__related__;
|
|
const merged = prevRelated
|
|
? { ...(query.data as Record<string, unknown>), __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,
|
|
};
|
|
}
|