import { useState, useEffect, useCallback, useRef } from 'react'; import workOrderService from '../services/workOrderService'; import type { WorkOrder, WorkOrderFilters, CreateWorkOrderData } from '../services/workOrderService'; /** * Merge user filters with permission filters * Permission filters take precedence for security */ const mergeFilters = ( userFilters: WorkOrderFilters | undefined, permissionFilters: Record ): WorkOrderFilters => { const merged: WorkOrderFilters = { ...(userFilters || {}) }; // Apply permission filters (they take precedence for security) for (const [field, value] of Object.entries(permissionFilters)) { if (!merged[field as keyof WorkOrderFilters]) { // No user filter on this field, apply permission filter directly (merged as any)[field] = value; } else if (Array.isArray(value) && value[0] === 'in') { // Permission filter is ["in", [...values]] const permittedValues = value[1] as string[]; const userValue = merged[field as keyof WorkOrderFilters]; if (typeof userValue === 'string') { // User selected a specific value, check if it's permitted if (!permittedValues.includes(userValue)) { // User selected a value they don't have permission for // Set to empty array to return no results (merged as any)[field] = ['in', []]; } // If permitted, keep the user's specific selection } else if (Array.isArray(userValue) && userValue[0] === 'in') { // Both are ["in", [...]] format, intersect them const userValues = userValue[1] as string[]; const intersection = userValues.filter(v => permittedValues.includes(v)); (merged as any)[field] = ['in', intersection]; } else { // Other filter types, apply permission filter (merged as any)[field] = value; } } } return merged; }; /** * Hook to fetch list of work orders with filters, pagination, and permission-based filtering */ export function useWorkOrders( filters?: WorkOrderFilters, limit: number = 20, offset: number = 0, orderBy?: string, permissionFilters: Record = {} // ← NEW: Permission filters parameter ) { const [workOrders, setWorkOrders] = useState([]); const [totalCount, setTotalCount] = useState(0); const [hasMore, setHasMore] = useState(false); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [refetchTrigger, setRefetchTrigger] = useState(0); const hasAttemptedRef = useRef(false); // Stringify filters to prevent object reference changes from causing re-renders const filtersJson = JSON.stringify(filters); const permissionFiltersJson = JSON.stringify(permissionFilters); // ← NEW useEffect(() => { // Prevent fetching if already attempted and has error if (hasAttemptedRef.current && error) { return; } let isCancelled = false; hasAttemptedRef.current = true; const fetchWorkOrders = async () => { try { setLoading(true); // ✅ NEW: Merge user filters with permission filters const mergedFilters = mergeFilters(filters, permissionFilters); console.log('[useWorkOrders] User filters:', filters); console.log('[useWorkOrders] Permission filters:', permissionFilters); console.log('[useWorkOrders] Merged filters:', mergedFilters); const response = await workOrderService.getWorkOrders(mergedFilters, undefined, limit, offset, orderBy); if (!isCancelled) { setWorkOrders(response.work_orders); setTotalCount(response.total_count); setHasMore(response.has_more); setError(null); } } catch (err) { if (!isCancelled) { const errorMessage = err instanceof Error ? err.message : 'Failed to fetch work orders'; // Check if it's a 417 error (API not deployed) if (errorMessage.includes('417') || errorMessage.includes('Expectation Failed') || errorMessage.includes('has no attribute')) { setError('API endpoint not deployed or misconfigured. Please check FIX_417_ERROR.md for solutions.'); } else { setError(errorMessage); } // Set empty arrays setWorkOrders([]); setTotalCount(0); setHasMore(false); } } finally { if (!isCancelled) { setLoading(false); } } }; fetchWorkOrders(); return () => { isCancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [filtersJson, permissionFiltersJson, limit, offset, orderBy, refetchTrigger]); // ← Added permissionFiltersJson const refetch = useCallback(() => { hasAttemptedRef.current = false; // Reset to allow refetch setRefetchTrigger(prev => prev + 1); }, []); return { workOrders, totalCount, hasMore, loading, error, refetch }; } /** * Hook to fetch a single work order by name */ export function useWorkOrderDetails(workOrderName: string | null) { const [workOrder, setWorkOrder] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const fetchWorkOrder = useCallback(async () => { if (!workOrderName) { setWorkOrder(null); setLoading(false); return; } try { setLoading(true); setError(null); const data = await workOrderService.getWorkOrderDetails(workOrderName); setWorkOrder(data); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to fetch work order details'); } finally { setLoading(false); } }, [workOrderName]); useEffect(() => { fetchWorkOrder(); }, [fetchWorkOrder]); const refetch = useCallback(() => { fetchWorkOrder(); }, [fetchWorkOrder]); return { workOrder, loading, error, refetch }; } /** * Hook to manage work order operations (create, update, delete) */ export function useWorkOrderMutations() { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const createWorkOrder = async (workOrderData: CreateWorkOrderData) => { try { setLoading(true); setError(null); console.log('[useWorkOrderMutations] Creating work order with data:', workOrderData); const response = await workOrderService.createWorkOrder(workOrderData); console.log('[useWorkOrderMutations] Create work order response:', response); if (response.success) { return response.work_order; } else { // Include the backend error message if available const backendError = (response as any).error || 'Failed to create work order'; throw new Error(backendError); } } catch (err) { console.error('[useWorkOrderMutations] Create work order error:', err); const errorMessage = err instanceof Error ? err.message : 'Failed to create work order'; setError(errorMessage); throw err; } finally { setLoading(false); } }; const updateWorkOrder = async (workOrderName: string, workOrderData: Partial) => { try { setLoading(true); setError(null); console.log('[useWorkOrderMutations] Updating work order:', workOrderName, 'with data:', workOrderData); const response = await workOrderService.updateWorkOrder(workOrderName, workOrderData); console.log('[useWorkOrderMutations] Update work order response:', response); if (response.success) { return response.work_order; } else { // Include the backend error message if available const backendError = (response as any).error || 'Failed to update work order'; throw new Error(backendError); } } catch (err) { console.error('[useWorkOrderMutations] Update work order error:', err); const errorMessage = err instanceof Error ? err.message : 'Failed to update work order'; setError(errorMessage); throw err; } finally { setLoading(false); } }; const deleteWorkOrder = async (workOrderName: string) => { try { setLoading(true); setError(null); const response = await workOrderService.deleteWorkOrder(workOrderName); if (!response.success) { throw new Error('Failed to delete work order'); } return response; } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to delete work order'; setError(errorMessage); throw err; } finally { setLoading(false); } }; const submitWorkOrder = async (workOrderName: string) => { try { setLoading(true); setError(null); console.log('[useWorkOrderMutations] Submitting work order:', workOrderName); const response = await workOrderService.submitWorkOrder(workOrderName); console.log('[useWorkOrderMutations] Submit work order response:', response); return response; } catch (err) { console.error('[useWorkOrderMutations] Submit work order error:', err); const errorMessage = err instanceof Error ? err.message : 'Failed to submit work order'; setError(errorMessage); throw err; } finally { setLoading(false); } }; const updateStatus = async (workOrderName: string, repairStatus?: string, workflowState?: string) => { try { setLoading(true); setError(null); const response = await workOrderService.updateWorkOrderStatus(workOrderName, repairStatus, workflowState); if (response.success) { return response.work_order; } else { throw new Error('Failed to update work order status'); } } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to update status'; setError(errorMessage); throw err; } finally { setLoading(false); } }; return { createWorkOrder, updateWorkOrder, deleteWorkOrder, submitWorkOrder, updateStatus, loading, error }; } /** * Hook to fetch work order filter options */ export function useWorkOrderFilters() { const [filters, setFilters] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const fetchFilters = useCallback(async () => { try { setLoading(true); setError(null); const data = await workOrderService.getWorkOrderFilters(); setFilters(data); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to fetch filters'); } finally { setLoading(false); } }, []); useEffect(() => { fetchFilters(); }, [fetchFilters]); const refetch = useCallback(() => { fetchFilters(); }, [fetchFilters]); return { filters, loading, error, refetch }; } /** * Hook to fetch work order statistics */ export function useWorkOrderStats() { const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const fetchStats = useCallback(async () => { try { setLoading(true); setError(null); const data = await workOrderService.getWorkOrderStats(); setStats(data); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to fetch statistics'); } finally { setLoading(false); } }, []); useEffect(() => { fetchStats(); }, [fetchStats]); const refetch = useCallback(() => { fetchStats(); }, [fetchStats]); return { stats, loading, error, refetch }; } /** * Hook for work order search */ export function useWorkOrderSearch() { const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const search = useCallback(async (searchTerm: string, limit: number = 10) => { if (!searchTerm.trim()) { setResults([]); return; } try { setLoading(true); setError(null); const data = await workOrderService.searchWorkOrders(searchTerm, limit); setResults(data); } catch (err) { setError(err instanceof Error ? err.message : 'Search failed'); setResults([]); } finally { setLoading(false); } }, []); const clearResults = useCallback(() => { setResults([]); setError(null); }, []); return { results, loading, error, search, clearResults }; }