/** * Draft/publish hooks for attribute values. * * Provider (escort) entity types use a draft layer: auto-save writes drafts, * user reviews, then explicitly publishes. Other entity types save directly. */ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { attributeValueKeys, type EntityType, type AttributeValues } from './index'; // API base URL - same as main hooks const API_BASE = typeof window !== 'undefined' ? (window as { __API_BASE__?: string }).__API_BASE__ || '/api' : '/api'; // --- Types --- export interface DraftDiffItem { code: string; oldValue: unknown; draftValue: unknown; updatedAt: string; } export interface PublishResult { publishedCount: number; } // --- Query Keys --- export const draftKeys = { all: ['attribute-value-drafts'] as const, check: (entityType: EntityType) => [...draftKeys.all, 'check', entityType] as const, list: (entityType: EntityType, entityId: string, userId: string) => [...draftKeys.all, 'list', entityType, entityId, userId] as const, diff: (entityType: EntityType, entityId: string, userId: string) => [...draftKeys.all, 'diff', entityType, entityId, userId] as const, count: (entityType: EntityType, entityId: string, userId: string) => [...draftKeys.all, 'count', entityType, entityId, userId] as const, }; // --- API Functions (exported for imperative use, e.g. ensureQueryData) --- export async function fetchDraftCheck(entityType: EntityType): Promise<{ usesDrafts: boolean }> { const response = await fetch( `${API_BASE}/attribute-value-drafts/check?entityType=${entityType}` ); if (!response.ok) { throw new Error(`Failed to check draft support: ${response.statusText}`); } return response.json(); } async function fetchDrafts( entityType: EntityType, entityId: string, userId: string, ): Promise { const params = new URLSearchParams({ entityType, entityId, userId }); const response = await fetch(`${API_BASE}/attribute-value-drafts?${params}`); if (!response.ok) { throw new Error(`Failed to fetch drafts: ${response.statusText}`); } return response.json(); } async function fetchDraftDiff( entityType: EntityType, entityId: string, userId: string, ): Promise { const params = new URLSearchParams({ entityType, entityId, userId }); const response = await fetch(`${API_BASE}/attribute-value-drafts/diff?${params}`); if (!response.ok) { throw new Error(`Failed to fetch draft diff: ${response.statusText}`); } return response.json(); } async function fetchDraftCount( entityType: EntityType, entityId: string, userId: string, ): Promise<{ count: number }> { const params = new URLSearchParams({ entityType, entityId, userId }); const response = await fetch(`${API_BASE}/attribute-value-drafts/count?${params}`); if (!response.ok) { throw new Error(`Failed to fetch draft count: ${response.statusText}`); } return response.json(); } export async function setDrafts( entityType: EntityType, entityId: string, userId: string, values: AttributeValues, ): Promise { const params = new URLSearchParams({ entityType, entityId, userId }); const response = await fetch(`${API_BASE}/attribute-value-drafts?${params}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(values), }); if (!response.ok) { throw new Error(`Failed to save drafts: ${response.statusText}`); } } async function publishDrafts( entityType: EntityType, entityId: string, userId: string, ): Promise { const params = new URLSearchParams({ entityType, entityId, userId }); const response = await fetch(`${API_BASE}/attribute-value-drafts/publish?${params}`, { method: 'POST', }); if (!response.ok) { throw new Error(`Failed to publish drafts: ${response.statusText}`); } return response.json(); } async function publishSelectedDrafts( entityType: EntityType, entityId: string, userId: string, codes: string[], ): Promise { const response = await fetch(`${API_BASE}/attribute-value-drafts/publish-selected`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ entityType, entityId, userId, codes }), }); if (!response.ok) { throw new Error(`Failed to publish selected drafts: ${response.statusText}`); } return response.json(); } async function discardAllDrafts( entityType: EntityType, entityId: string, userId: string, ): Promise<{ discarded: number }> { const params = new URLSearchParams({ entityType, entityId, userId }); const response = await fetch(`${API_BASE}/attribute-value-drafts?${params}`, { method: 'DELETE', }); if (!response.ok) { throw new Error(`Failed to discard drafts: ${response.statusText}`); } return response.json(); } async function discardOneDraft( entityType: EntityType, entityId: string, userId: string, code: string, ): Promise { const params = new URLSearchParams({ entityType, entityId, userId }); const response = await fetch(`${API_BASE}/attribute-value-drafts/${code}?${params}`, { method: 'DELETE', }); if (!response.ok) { throw new Error(`Failed to discard draft: ${response.statusText}`); } } // --- Hooks --- /** * Check if an entity type uses the draft/publish workflow. * Result is cached with staleTime: Infinity since it's config-level. */ export function useCheckDraftSupport(entityType: EntityType) { const query = useQuery({ queryKey: draftKeys.check(entityType), queryFn: () => fetchDraftCheck(entityType), staleTime: Infinity, retry: false, enabled: !!entityType, }); return { usesDrafts: query.data?.usesDrafts ?? false, isLoading: query.isLoading, error: query.error, }; } /** * Fetch pending draft values as a code→value map. * Only enabled when draft mode is active. */ export function useDraftValues( entityType: EntityType, entityId: string, userId: string, options?: { enabled?: boolean }, ) { const query = useQuery({ queryKey: draftKeys.list(entityType, entityId, userId), queryFn: () => fetchDrafts(entityType, entityId, userId), retry: false, enabled: (options?.enabled ?? true) && !!entityType && !!entityId && !!userId, }); return { data: query.data ?? {}, isLoading: query.isLoading, error: query.error, refetch: query.refetch, }; } /** * Fetch draft diff items for the summary panel. * Only enabled when draft mode is active. */ export function useDraftDiff( entityType: EntityType, entityId: string, userId: string, options?: { enabled?: boolean }, ) { const query = useQuery({ queryKey: draftKeys.diff(entityType, entityId, userId), queryFn: () => fetchDraftDiff(entityType, entityId, userId), retry: false, enabled: (options?.enabled ?? true) && !!entityType && !!entityId && !!userId, }); return { data: query.data ?? [], isLoading: query.isLoading, error: query.error, refetch: query.refetch, }; } /** * Fetch count of pending drafts. * Only enabled when draft mode is active. */ export function useDraftCount( entityType: EntityType, entityId: string, userId: string, options?: { enabled?: boolean }, ) { const query = useQuery({ queryKey: draftKeys.count(entityType, entityId, userId), queryFn: () => fetchDraftCount(entityType, entityId, userId), retry: false, enabled: (options?.enabled ?? true) && !!entityType && !!entityId && !!userId, }); return { count: query.data?.count ?? 0, isLoading: query.isLoading, error: query.error, }; } /** * Mutation to upsert draft values. */ export function useSetDrafts( entityType: EntityType, entityId: string, userId: string, ) { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (values: AttributeValues) => setDrafts(entityType, entityId, userId, values), onSuccess: () => { queryClient.invalidateQueries({ queryKey: draftKeys.list(entityType, entityId, userId) }); queryClient.invalidateQueries({ queryKey: draftKeys.diff(entityType, entityId, userId) }); queryClient.invalidateQueries({ queryKey: draftKeys.count(entityType, entityId, userId) }); }, }); return { mutate: mutation.mutate, mutateAsync: mutation.mutateAsync, isPending: mutation.isPending, error: mutation.error, }; } /** * Mutation to publish all drafts. */ export function usePublishDrafts( entityType: EntityType, entityId: string, userId: string, ) { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: () => publishDrafts(entityType, entityId, userId), onSuccess: () => { // Invalidate drafts (now empty) queryClient.invalidateQueries({ queryKey: draftKeys.all }); // Invalidate live values (now updated) queryClient.invalidateQueries({ queryKey: attributeValueKeys.list(entityType, entityId) }); }, }); return { mutate: mutation.mutate, mutateAsync: mutation.mutateAsync, isPending: mutation.isPending, error: mutation.error, }; } /** * Mutation to publish selected drafts by code. */ export function usePublishSelectedDrafts( entityType: EntityType, entityId: string, userId: string, ) { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (codes: string[]) => publishSelectedDrafts(entityType, entityId, userId, codes), onSuccess: () => { queryClient.invalidateQueries({ queryKey: draftKeys.all }); queryClient.invalidateQueries({ queryKey: attributeValueKeys.list(entityType, entityId) }); }, }); return { mutate: mutation.mutate, mutateAsync: mutation.mutateAsync, isPending: mutation.isPending, error: mutation.error, }; } /** * Mutation to discard all drafts. */ export function useDiscardAllDrafts( entityType: EntityType, entityId: string, userId: string, ) { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: () => discardAllDrafts(entityType, entityId, userId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: draftKeys.all }); }, }); return { mutate: mutation.mutate, mutateAsync: mutation.mutateAsync, isPending: mutation.isPending, error: mutation.error, }; } /** * Mutation to discard a single draft. */ export function useDiscardOneDraft( entityType: EntityType, entityId: string, userId: string, ) { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (code: string) => discardOneDraft(entityType, entityId, userId, code), onSuccess: () => { queryClient.invalidateQueries({ queryKey: draftKeys.all }); }, }); return { mutate: mutation.mutate, mutateAsync: mutation.mutateAsync, isPending: mutation.isPending, error: mutation.error, }; }