diff --git a/index.html b/index.html index f15a777..af7a0f5 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + frappe-frontend diff --git a/public/seera-logo.png b/public/seera-logo.png new file mode 100644 index 0000000..a978072 Binary files /dev/null and b/public/seera-logo.png differ diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index ba32ed3..cf594da 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,12 @@ import UsersList from './pages/UsersList'; import EventsList from './pages/EventsList'; import AssetList from './pages/AssetList'; import AssetDetail from './pages/AssetDetail'; +import WorkOrderList from './pages/WorkOrderList'; +import WorkOrderDetail from './pages/WorkOrderDetail'; +import AssetMaintenanceList from './pages/AssetMaintenanceList'; +import AssetMaintenanceDetail from './pages/AssetMaintenanceDetail'; +import PPMList from './pages/PPMList'; +import PPMDetail from './pages/PPMDetail'; import Sidebar from './components/Sidebar'; // Layout with Sidebar @@ -71,6 +77,72 @@ const App: React.FC = () => { } /> + + + + + + } + /> + + + + + + + } + /> + + + + + + + } + /> + + + + + + + } + /> + + + + + + + } + /> + + + + + + + } + /> + = ({ userEmail }) => { { id: 'dashboard', title: 'Dashboard', - icon: , + icon: , path: '/dashboard', visible: true }, { id: 'assets', title: 'Assets', - icon: , + icon: , path: '/assets', visible: showAsset }, { id: 'work-orders', title: 'Work Orders', - icon: , + icon: , path: '/work-orders', visible: showGeneralWO }, { - id: 'ppm', - title: 'PPM', - icon: , - path: '/ppm', + id: 'maintenance', + title: 'Asset Maintenance', + icon: , + path: '/maintenance', visible: showPreventiveMaintenance }, { - id: 'inventory', - title: 'Inventory', - icon: , - path: '/inventory', - visible: showInventory + id: 'ppm', + title: 'PPM', + icon: , + path: '/ppm', + visible: showPreventiveMaintenance }, - { - id: 'vendors', - title: 'Vendors', - icon: , - path: '/vendors', - visible: showSupplierDashboard - }, - { - id: 'dashboard-view', - title: 'Dashboard', - icon: , - path: '/dashboard-view', - visible: showProjectDashboard - }, - { - id: 'sites', - title: 'Sites', - icon: , - path: '/sites', - visible: showSiteDashboards - }, - { - id: 'active-map', - title: 'Active Map', - icon: , - path: '/active-map', - visible: showSiteInfo - }, - { - id: 'users', - title: 'Users', - icon: , - path: '/users', - visible: showAMTeam - }, - { - id: 'account', - title: 'Account', - icon: , - path: '/account', - visible: showSLA - } + // { + // id: 'inventory', + // title: 'Inventory', + // icon: , + // path: '/inventory', + // visible: showInventory + // }, + // { + // id: 'vendors', + // title: 'Vendors', + // icon: , + // path: '/vendors', + // visible: showSupplierDashboard + // }, + // { + // id: 'dashboard-view', + // title: 'Dashboard', + // icon: , + // path: '/dashboard-view', + // visible: showProjectDashboard + // }, + // { + // id: 'sites', + // title: 'Sites', + // icon: , + // path: '/sites', + // visible: showSiteDashboards + // }, + // { + // id: 'active-map', + // title: 'Active Map', + // icon: , + // path: '/active-map', + // visible: showSiteInfo + // }, + // { + // id: 'users', + // title: 'Users', + // icon: , + // path: '/users', + // visible: showAMTeam + // }, + // { + // id: 'account', + // title: 'Account', + // icon: , + // path: '/account', + // visible: showSLA + // } ]; const visibleLinks = links.filter(link => link.visible); @@ -175,18 +182,51 @@ const Sidebar: React.FC = ({ userEmail }) => { {/* Sidebar Header */}
{!isCollapsed && ( -
-
- AL +
+
+ {/* Seera Arabia Logo */} + Seera Arabia { + // Fallback to SVG if image not found + e.currentTarget.style.display = 'none'; + e.currentTarget.nextElementSibling?.classList.remove('hidden'); + }} + /> + + + + +
-

Asset Lite

+

Seera Arabia

+
+ )} + {isCollapsed && ( +
+ Seera Arabia { + e.currentTarget.style.display = 'none'; + e.currentTarget.nextElementSibling?.classList.remove('hidden'); + }} + /> + + + + +
)}
@@ -211,7 +251,7 @@ const Sidebar: React.FC = ({ userEmail }) => { `} title={isCollapsed ? link.title : ''} > - {link.icon} + {link.icon} {!isCollapsed && ( {link.title} )} @@ -227,7 +267,7 @@ const Sidebar: React.FC = ({ userEmail }) => { className="w-full flex items-center justify-center px-4 py-2 rounded-lg bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors text-gray-700 dark:text-gray-300" title={isCollapsed ? (theme === 'light' ? 'Dark Mode' : 'Light Mode') : ''} > - {theme === 'light' ? : } + {theme === 'light' ? : } {!isCollapsed && ( {theme === 'light' ? 'Dark Mode' : 'Light Mode'} @@ -241,7 +281,7 @@ const Sidebar: React.FC = ({ userEmail }) => { className="w-full flex items-center justify-center px-4 py-2 rounded-lg bg-red-50 dark:bg-red-900/20 hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors text-red-600 dark:text-red-400" title={isCollapsed ? 'Logout' : ''} > - + {!isCollapsed && ( Logout )} @@ -260,7 +300,7 @@ const Sidebar: React.FC = ({ userEmail }) => { {!isCollapsed && (
- Asset Lite v1.0 + Seera Arabia AMS v1.0
)}
diff --git a/src/config/api.ts b/src/config/api.ts index 2717047..afc9602 100644 --- a/src/config/api.ts +++ b/src/config/api.ts @@ -40,6 +40,35 @@ const API_CONFIG: ApiConfig = { GET_ASSET_STATS: '/api/method/asset_lite.api.asset_api.get_asset_stats', SEARCH_ASSETS: '/api/method/asset_lite.api.asset_api.search_assets', + // Work Order Management + GET_WORK_ORDERS: '/api/method/asset_lite.api.work_order_api.get_work_orders', + GET_WORK_ORDER_DETAILS: '/api/method/asset_lite.api.work_order_api.get_work_order_details', + CREATE_WORK_ORDER: '/api/method/asset_lite.api.work_order_api.create_work_order', + UPDATE_WORK_ORDER: '/api/method/asset_lite.api.work_order_api.update_work_order', + DELETE_WORK_ORDER: '/api/method/asset_lite.api.work_order_api.delete_work_order', + UPDATE_WORK_ORDER_STATUS: '/api/method/asset_lite.api.work_order_api.update_work_order_status', + + // Asset Maintenance Management + GET_ASSET_MAINTENANCE_LOGS: '/api/method/asset_lite.api.asset_maintenance_api.get_asset_maintenance_logs', + GET_ASSET_MAINTENANCE_LOG_DETAILS: '/api/method/asset_lite.api.asset_maintenance_api.get_asset_maintenance_log_details', + CREATE_ASSET_MAINTENANCE_LOG: '/api/method/asset_lite.api.asset_maintenance_api.create_asset_maintenance_log', + UPDATE_ASSET_MAINTENANCE_LOG: '/api/method/asset_lite.api.asset_maintenance_api.update_asset_maintenance_log', + DELETE_ASSET_MAINTENANCE_LOG: '/api/method/asset_lite.api.asset_maintenance_api.delete_asset_maintenance_log', + UPDATE_MAINTENANCE_STATUS: '/api/method/asset_lite.api.asset_maintenance_api.update_maintenance_status', + GET_MAINTENANCE_LOGS_BY_ASSET: '/api/method/asset_lite.api.asset_maintenance_api.get_maintenance_logs_by_asset', + GET_OVERDUE_MAINTENANCE_LOGS: '/api/method/asset_lite.api.asset_maintenance_api.get_overdue_maintenance_logs', + + // PPM (Asset Maintenance) Management + GET_ASSET_MAINTENANCES: '/api/method/asset_lite.api.ppm_api.get_asset_maintenances', + GET_ASSET_MAINTENANCE_DETAILS: '/api/method/asset_lite.api.ppm_api.get_asset_maintenance_details', + CREATE_ASSET_MAINTENANCE: '/api/method/asset_lite.api.ppm_api.create_asset_maintenance', + UPDATE_ASSET_MAINTENANCE: '/api/method/asset_lite.api.ppm_api.update_asset_maintenance', + DELETE_ASSET_MAINTENANCE: '/api/method/asset_lite.api.ppm_api.delete_asset_maintenance', + GET_MAINTENANCE_TASKS: '/api/method/asset_lite.api.ppm_api.get_maintenance_tasks', + GET_SERVICE_COVERAGE: '/api/method/asset_lite.api.ppm_api.get_service_coverage', + GET_MAINTENANCES_BY_ASSET: '/api/method/asset_lite.api.ppm_api.get_maintenances_by_asset', + GET_ACTIVE_SERVICE_CONTRACTS: '/api/method/asset_lite.api.ppm_api.get_active_service_contracts', + // Authentication LOGIN: '/api/method/login', LOGOUT: '/api/method/logout', diff --git a/src/hooks/useAssetMaintenance.ts b/src/hooks/useAssetMaintenance.ts new file mode 100644 index 0000000..2741139 --- /dev/null +++ b/src/hooks/useAssetMaintenance.ts @@ -0,0 +1,288 @@ +import { useState, useEffect, useCallback, useRef } from 'react'; +import assetMaintenanceService from '../services/assetMaintenanceService'; +import type { AssetMaintenanceLog, MaintenanceFilters, CreateMaintenanceData } from '../services/assetMaintenanceService'; + +/** + * Hook to fetch list of asset maintenance logs with filters and pagination + */ +export function useAssetMaintenanceLogs( + filters?: MaintenanceFilters, + limit: number = 20, + offset: number = 0, + orderBy?: string +) { + const [logs, setLogs] = 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); + + const filtersJson = JSON.stringify(filters); + + useEffect(() => { + if (hasAttemptedRef.current && error) { + return; + } + + let isCancelled = false; + hasAttemptedRef.current = true; + + const fetchLogs = async () => { + try { + setLoading(true); + + const response = await assetMaintenanceService.getMaintenanceLogs(filters, undefined, limit, offset, orderBy); + + if (!isCancelled) { + setLogs(response.asset_maintenance_logs); + setTotalCount(response.total_count); + setHasMore(response.has_more); + setError(null); + } + } catch (err) { + if (!isCancelled) { + const errorMessage = err instanceof Error ? err.message : 'Failed to fetch maintenance logs'; + + if (errorMessage.includes('417') || errorMessage.includes('Expectation Failed') || errorMessage.includes('has no attribute')) { + setError('API endpoint not deployed. Please deploy asset_maintenance_api.py to your Frappe server.'); + } else { + setError(errorMessage); + } + + setLogs([]); + setTotalCount(0); + setHasMore(false); + } + } finally { + if (!isCancelled) { + setLoading(false); + } + } + }; + + fetchLogs(); + + return () => { + isCancelled = true; + }; + }, [filtersJson, limit, offset, orderBy, refetchTrigger]); + + const refetch = useCallback(() => { + hasAttemptedRef.current = false; + setRefetchTrigger(prev => prev + 1); + }, []); + + return { logs, totalCount, hasMore, loading, error, refetch }; +} + +/** + * Hook to fetch a single maintenance log by name + */ +export function useMaintenanceLogDetails(logName: string | null) { + const [log, setLog] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchLog = useCallback(async () => { + if (!logName) { + setLog(null); + setLoading(false); + return; + } + + try { + setLoading(true); + setError(null); + + const data = await assetMaintenanceService.getMaintenanceLogDetails(logName); + setLog(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch maintenance log details'); + } finally { + setLoading(false); + } + }, [logName]); + + useEffect(() => { + fetchLog(); + }, [fetchLog]); + + const refetch = useCallback(() => { + fetchLog(); + }, [fetchLog]); + + return { log, loading, error, refetch }; +} + +/** + * Hook to manage maintenance log operations + */ +export function useMaintenanceMutations() { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const createLog = async (logData: CreateMaintenanceData) => { + try { + setLoading(true); + setError(null); + + console.log('[useMaintenanceMutations] Creating maintenance log:', logData); + const response = await assetMaintenanceService.createMaintenanceLog(logData); + console.log('[useMaintenanceMutations] Create response:', response); + + if (response.success) { + return response.asset_maintenance_log; + } else { + const backendError = (response as any).error || 'Failed to create maintenance log'; + throw new Error(backendError); + } + } catch (err) { + console.error('[useMaintenanceMutations] Create error:', err); + const errorMessage = err instanceof Error ? err.message : 'Failed to create maintenance log'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + const updateLog = async (logName: string, logData: Partial) => { + try { + setLoading(true); + setError(null); + + console.log('[useMaintenanceMutations] Updating maintenance log:', logName, logData); + const response = await assetMaintenanceService.updateMaintenanceLog(logName, logData); + console.log('[useMaintenanceMutations] Update response:', response); + + if (response.success) { + return response.asset_maintenance_log; + } else { + const backendError = (response as any).error || 'Failed to update maintenance log'; + throw new Error(backendError); + } + } catch (err) { + console.error('[useMaintenanceMutations] Update error:', err); + const errorMessage = err instanceof Error ? err.message : 'Failed to update maintenance log'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + const deleteLog = async (logName: string) => { + try { + setLoading(true); + setError(null); + + const response = await assetMaintenanceService.deleteMaintenanceLog(logName); + + if (!response.success) { + throw new Error('Failed to delete maintenance log'); + } + + return response; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to delete maintenance log'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + const updateStatus = async (logName: string, maintenanceStatus?: string, workflowState?: string) => { + try { + setLoading(true); + setError(null); + + const response = await assetMaintenanceService.updateMaintenanceStatus(logName, maintenanceStatus, workflowState); + + if (response.success) { + return response.asset_maintenance_log; + } else { + throw new Error('Failed to update maintenance status'); + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to update status'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + return { createLog, updateLog, deleteLog, updateStatus, loading, error }; +} + +/** + * Hook to fetch maintenance logs for a specific asset + */ +export function useAssetMaintenanceHistory(assetName: string | null) { + const [logs, setLogs] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchHistory = useCallback(async () => { + if (!assetName) { + setLogs([]); + setLoading(false); + return; + } + + try { + setLoading(true); + setError(null); + + const response = await assetMaintenanceService.getMaintenanceLogsByAsset(assetName); + setLogs(response.asset_maintenance_logs); + setTotalCount(response.total_count); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch maintenance history'); + } finally { + setLoading(false); + } + }, [assetName]); + + useEffect(() => { + fetchHistory(); + }, [fetchHistory]); + + return { logs, totalCount, loading, error, refetch: fetchHistory }; +} + +/** + * Hook to fetch overdue maintenance logs + */ +export function useOverdueMaintenanceLogs() { + const [logs, setLogs] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchOverdue = useCallback(async () => { + try { + setLoading(true); + setError(null); + + const response = await assetMaintenanceService.getOverdueMaintenanceLogs(); + setLogs(response.asset_maintenance_logs); + setTotalCount(response.total_count); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch overdue maintenance'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchOverdue(); + }, [fetchOverdue]); + + return { logs, totalCount, loading, error, refetch: fetchOverdue }; +} + diff --git a/src/hooks/usePPM.ts b/src/hooks/usePPM.ts new file mode 100644 index 0000000..dca0d66 --- /dev/null +++ b/src/hooks/usePPM.ts @@ -0,0 +1,174 @@ +import { useState, useEffect, useCallback, useRef } from 'react'; +import ppmService from '../services/ppmService'; +import type { AssetMaintenance, PPMFilters, CreatePPMData } from '../services/ppmService'; + +/** + * Hook to fetch list of asset maintenances (PPM schedules) with filters and pagination + */ +export function usePPMs( + filters?: PPMFilters, + limit: number = 20, + offset: number = 0, + orderBy?: string +) { + const [ppms, setPPMs] = 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); + + const filtersJson = JSON.stringify(filters); + + useEffect(() => { + if (hasAttemptedRef.current && error) { + return; + } + + let isCancelled = false; + hasAttemptedRef.current = true; + + const fetchPPMs = async () => { + try { + setLoading(true); + + const response = await ppmService.getAssetMaintenances(filters, undefined, limit, offset, orderBy); + + if (!isCancelled) { + setPPMs(response.asset_maintenances); + setTotalCount(response.total_count); + setHasMore(response.has_more); + setError(null); + } + } catch (err) { + if (!isCancelled) { + const errorMessage = err instanceof Error ? err.message : 'Failed to fetch PPM schedules'; + + if (errorMessage.includes('417') || errorMessage.includes('Expectation Failed') || errorMessage.includes('has no attribute')) { + setError('API endpoint not deployed. Please deploy ppm_api.py to your Frappe server.'); + } else { + setError(errorMessage); + } + + setPPMs([]); + setTotalCount(0); + setHasMore(false); + } + } finally { + if (!isCancelled) { + setLoading(false); + } + } + }; + + fetchPPMs(); + + return () => { + isCancelled = true; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filtersJson, limit, offset, orderBy, refetchTrigger]); + + const refetch = useCallback(() => { + hasAttemptedRef.current = false; + setRefetchTrigger(prev => prev + 1); + }, []); + + return { ppms, totalCount, hasMore, loading, error, refetch }; +} + +/** + * Hook to fetch a single PPM schedule by name + */ +export function usePPMDetails(ppmName: string | null) { + const [ppm, setPPM] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchPPM = useCallback(async () => { + if (!ppmName) { + setPPM(null); + setLoading(false); + return; + } + + try { + setLoading(true); + setError(null); + + const data = await ppmService.getAssetMaintenanceDetails(ppmName); + setPPM(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch PPM details'); + } finally { + setLoading(false); + } + }, [ppmName]); + + useEffect(() => { + fetchPPM(); + }, [fetchPPM]); + + const refetch = useCallback(() => { + fetchPPM(); + }, [fetchPPM]); + + return { ppm, loading, error, refetch }; +} + +/** + * Hook to manage PPM operations (create, update, delete) + */ +export function usePPMMutations() { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const createPPM = useCallback(async (data: CreatePPMData) => { + try { + setLoading(true); + setError(null); + const result = await ppmService.createAssetMaintenance(data); + return result; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to create PPM schedule'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }, []); + + const updatePPM = useCallback(async (ppmName: string, data: Partial) => { + try { + setLoading(true); + setError(null); + const result = await ppmService.updateAssetMaintenance(ppmName, data); + return result; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to update PPM schedule'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }, []); + + const deletePPM = useCallback(async (ppmName: string) => { + try { + setLoading(true); + setError(null); + const result = await ppmService.deleteAssetMaintenance(ppmName); + return result; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to delete PPM schedule'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }, []); + + return { createPPM, updatePPM, deletePPM, loading, error }; +} + diff --git a/src/hooks/useWorkOrder.ts b/src/hooks/useWorkOrder.ts new file mode 100644 index 0000000..5aaab75 --- /dev/null +++ b/src/hooks/useWorkOrder.ts @@ -0,0 +1,220 @@ +import { useState, useEffect, useCallback, useRef } from 'react'; +import workOrderService from '../services/workOrderService'; +import type { WorkOrder, WorkOrderFilters, CreateWorkOrderData } from '../services/workOrderService'; + +/** + * Hook to fetch list of work orders with filters and pagination + */ +export function useWorkOrders( + filters?: WorkOrderFilters, + limit: number = 20, + offset: number = 0, + orderBy?: string +) { + 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); + + const filtersJson = JSON.stringify(filters); + + useEffect(() => { + if (hasAttemptedRef.current && error) { + return; + } + + let isCancelled = false; + hasAttemptedRef.current = true; + + const fetchWorkOrders = async () => { + try { + setLoading(true); + + const response = await workOrderService.getWorkOrders(filters, 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'; + + if (errorMessage.includes('417') || errorMessage.includes('Expectation Failed') || errorMessage.includes('has no attribute')) { + setError('API endpoint not deployed. Please deploy work_order_api.py to your Frappe server.'); + } else { + setError(errorMessage); + } + + setWorkOrders([]); + setTotalCount(0); + setHasMore(false); + } + } finally { + if (!isCancelled) { + setLoading(false); + } + } + }; + + fetchWorkOrders(); + + return () => { + isCancelled = true; + }; + }, [filtersJson, limit, offset, orderBy, refetchTrigger]); + + const refetch = useCallback(() => { + hasAttemptedRef.current = false; + 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 { + 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 { + 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 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, updateStatus, loading, error }; +} + diff --git a/src/index.css b/src/index.css index 0d70891..3cb6b70 100644 --- a/src/index.css +++ b/src/index.css @@ -1,3 +1,5 @@ +@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@100;200;300;400;500;600;700;800;900&display=swap'); + @tailwind base; @tailwind components; @tailwind utilities; diff --git a/src/pages/AssetDetail.tsx b/src/pages/AssetDetail.tsx index 1a8cbf5..bfc70e1 100644 --- a/src/pages/AssetDetail.tsx +++ b/src/pages/AssetDetail.tsx @@ -13,7 +13,6 @@ const AssetDetail: React.FC = () => { const isNewAsset = assetName === 'new'; const isDuplicating = isNewAsset && !!duplicateFromAsset; - // If duplicating, fetch the source asset const { asset, loading, error } = useAssetDetails( isDuplicating ? duplicateFromAsset : (isNewAsset ? null : assetName || null) ); @@ -41,13 +40,12 @@ const AssetDetail: React.FC = () => { custom_total_amount: 0 }); - // Load asset data for editing or duplicating useEffect(() => { if (asset) { setFormData({ asset_name: isDuplicating ? `${asset.asset_name} (Copy)` : (asset.asset_name || ''), company: asset.company || '', - custom_serial_number: isDuplicating ? '' : (asset.custom_serial_number || ''), // Clear serial number for duplicates + custom_serial_number: isDuplicating ? '' : (asset.custom_serial_number || ''), location: asset.location || '', custom_manufacturer: asset.custom_manufacturer || '', department: asset.department || '', @@ -78,7 +76,6 @@ const AssetDetail: React.FC = () => { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - // Validate required fields if (!formData.asset_name) { alert('Please enter an Asset Name'); return; @@ -89,7 +86,6 @@ const AssetDetail: React.FC = () => { return; } - // Show console log for debugging console.log('Submitting asset data:', formData); try { @@ -107,10 +103,8 @@ const AssetDetail: React.FC = () => { } } catch (err) { console.error('Asset save error:', err); - const errorMessage = err instanceof Error ? err.message : 'Unknown error'; - // Check if it's an API deployment issue if (errorMessage.includes('404') || errorMessage.includes('not found') || errorMessage.includes('has no attribute') || errorMessage.includes('417')) { alert( @@ -156,8 +150,7 @@ const AssetDetail: React.FC = () => {
); } - - // Show error for duplicate if source asset not found + if (error && isDuplicating) { return (
@@ -166,7 +159,7 @@ const AssetDetail: React.FC = () => { Source Asset Not Found

- The asset you're trying to duplicate could not be found. It may have been deleted or you may not have permission to access it. + The asset you're trying to duplicate could not be found.

@@ -222,7 +215,7 @@ const AssetDetail: React.FC = () => { setIsEditing(false); } }} - className="bg-gray-300 hover:bg-gray-400 text-gray-700 px-6 py-2 rounded-lg" + className="bg-gray-300 hover:bg-gray-400 text-gray-700 dark:text-gray-800 px-6 py-2 rounded-lg" disabled={saving} > Cancel @@ -240,578 +233,300 @@ const AssetDetail: React.FC = () => {
-
-
- {/* Left Column - Asset Information & Technical Specs & Location */} -
- {/* Asset Information */} -
-

Asset Information

-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - - {isDuplicating && ( -

- 💡 Duplicating from: {duplicateFromAsset} -

- )} -
+ + {/* 4-Column Grid Layout */} +
+ + {/* COLUMN 1: Asset Information */} +
+

+ Asset Information +

+
+
+ +
-
- {/* Technical Specs */} -
-

Technical Specs

-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
+
+ +
-
- {/* Location */} -
-

Location

-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
+
+ +
-
- {/* Coverage */} -
-

Coverage

-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -