From 037c11f06ab5f24c9c65a3006ed8ab7492bf0003 Mon Sep 17 00:00:00 2001 From: "Akhib.Shaik" Date: Wed, 12 Nov 2025 14:22:51 +0530 Subject: [PATCH] add latest changes and new pages updated dashboard --- index.html | 2 +- public/seera-logo.png | Bin 0 -> 37634 bytes public/vite.svg | 1 - src/App.tsx | 72 ++ src/components/Sidebar.tsx | 200 ++-- src/config/api.ts | 29 + src/hooks/useAssetMaintenance.ts | 288 +++++ src/hooks/usePPM.ts | 174 +++ src/hooks/useWorkOrder.ts | 220 ++++ src/index.css | 2 + src/pages/AssetDetail.tsx | 1133 +++++++++--------- src/pages/AssetMaintenanceDetail.tsx | 423 +++++++ src/pages/AssetMaintenanceList.tsx | 499 ++++++++ src/pages/Login.tsx | 35 +- src/pages/ModernDashboard.tsx | 1435 ++++++++++++++++++----- src/pages/PPMDetail.tsx | 406 +++++++ src/pages/PPMList.tsx | 441 +++++++ src/pages/WorkOrderDetail.tsx | 571 +++++++++ src/pages/WorkOrderList.tsx | 513 ++++++++ src/services/assetMaintenanceService.ts | 220 ++++ src/services/ppmService.ts | 242 ++++ src/services/workOrderService.ts | 205 ++++ 22 files changed, 6186 insertions(+), 925 deletions(-) create mode 100644 public/seera-logo.png delete mode 100644 public/vite.svg create mode 100644 src/hooks/useAssetMaintenance.ts create mode 100644 src/hooks/usePPM.ts create mode 100644 src/hooks/useWorkOrder.ts create mode 100644 src/pages/AssetMaintenanceDetail.tsx create mode 100644 src/pages/AssetMaintenanceList.tsx create mode 100644 src/pages/PPMDetail.tsx create mode 100644 src/pages/PPMList.tsx create mode 100644 src/pages/WorkOrderDetail.tsx create mode 100644 src/pages/WorkOrderList.tsx create mode 100644 src/services/assetMaintenanceService.ts create mode 100644 src/services/ppmService.ts create mode 100644 src/services/workOrderService.ts 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 0000000000000000000000000000000000000000..a978072243c399f51c3e872663ad21a4e1dbcd54 GIT binary patch literal 37634 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 literal 0 HcmV?d00001 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

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