400 lines
12 KiB
TypeScript
400 lines
12 KiB
TypeScript
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<string, any>
|
|
): 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<string, any> = {} // ← NEW: Permission filters parameter
|
|
) {
|
|
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
|
|
const [totalCount, setTotalCount] = useState(0);
|
|
const [hasMore, setHasMore] = useState(false);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(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<WorkOrder | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(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<string | null>(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<CreateWorkOrderData>) => {
|
|
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<any | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(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<any | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(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<WorkOrder[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(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 };
|
|
} |