diff --git a/QUICK_START_FOR_TEAM.md b/QUICK_START_FOR_TEAM.md new file mode 100644 index 0000000..e54ee9f --- /dev/null +++ b/QUICK_START_FOR_TEAM.md @@ -0,0 +1,220 @@ +# Quick Start Guide for Team Members + +## 📋 For Akhib and Dundu + +### Step 1: Accept Invitation ✉️ + +Check your email for GitHub invitation and click **Accept invitation**. + +### Step 2: Clone Repository 📥 + +```bash +# Open terminal/command prompt and run: +git clone https://github.com/YOUR_USERNAME/frappe-frontend.git +cd frappe-frontend +``` + +### Step 3: Install Dependencies 📦 + +```bash +npm install +``` + +This will take a few minutes. Wait for it to complete. + +### Step 4: Checkout Your Branch 🌿 + +**For Akhib:** +```bash +git checkout akhib +``` + +**For Dundu:** +```bash +git checkout dundu +``` + +### Step 5: Start Development 🚀 + +```bash +npm run dev +``` + +Open browser: http://localhost:3000 + +--- + +## 📝 Daily Workflow + +### Morning (Start Work) + +```bash +# Get latest changes +git pull origin akhib # or dundu + +# Start dev server +npm run dev +``` + +### During Work (Save Changes) + +```bash +# Check what changed +git status + +# Add all changes +git add . + +# Commit with message +git commit -m "Your description here" + +# Push to remote +git push origin akhib # or dundu +``` + +### Evening (End of Day) + +```bash +# Make sure everything is saved +git status + +# Push if needed +git push origin akhib # or dundu +``` + +--- + +## 🆘 Common Issues + +### Issue: "Permission denied" + +**Solution:** Make sure you accepted the GitHub invitation. + +### Issue: "npm install" fails + +**Solution:** +```bash +# Delete node_modules +rm -rf node_modules +npm cache clean --force +npm install +``` + +### Issue: Branch doesn't exist + +**Solution:** +```bash +git fetch origin +git checkout akhib # or dundu +``` + +### Issue: "Cannot push to remote" + +**Solution:** +```bash +# Pull first, then push +git pull origin akhib +git push origin akhib +``` + +--- + +## 🎯 Important Commands + +| Command | What it does | +|---------|--------------| +| `git status` | See what changed | +| `git add .` | Stage all changes | +| `git commit -m "msg"` | Save changes | +| `git push` | Upload to GitHub | +| `git pull` | Download from GitHub | +| `npm run dev` | Start dev server | +| `npm install` | Install packages | + +--- + +## 📞 Need Help? + +1. Check if dev server is running: http://localhost:3000 +2. Check terminal for error messages +3. Try restarting: Stop server (Ctrl+C) and run `npm run dev` again +4. Contact team lead + +--- + +## ✅ Checklist for First Day + +- [ ] Accepted GitHub invitation +- [ ] Cloned repository +- [ ] Ran `npm install` successfully +- [ ] Switched to my branch (akhib or dundu) +- [ ] Started dev server (`npm run dev`) +- [ ] Saw application in browser +- [ ] Made test change +- [ ] Committed and pushed test change +- [ ] Saw my change on GitHub + +--- + +## 🎨 Project Structure + +``` +frappe-frontend/ +├── src/ +│ ├── pages/ # All pages (Login, Dashboard, etc) +│ ├── components/ # Reusable components +│ ├── services/ # API calls +│ ├── hooks/ # Custom React hooks +│ └── contexts/ # React contexts (Theme, etc) +├── public/ # Static files +└── package.json # Project dependencies +``` + +--- + +## 🌟 Best Practices + +1. **Commit often** - Don't wait until end of day +2. **Write clear messages** - "Fixed login bug" not "fixed stuff" +3. **Pull before push** - Always get latest changes first +4. **Test before commit** - Make sure it works +5. **Ask questions** - Better to ask than break things! + +--- + +## 🔐 Git Config (One-time Setup) + +```bash +# Set your name and email +git config --global user.name "Your Name" +git config --global user.email "your.email@example.com" + +# Check settings +git config --list +``` + +--- + +## 💻 VS Code Extensions (Recommended) + +- **ES7+ React/Redux/React-Native snippets** +- **GitLens** - Better Git integration +- **Prettier** - Code formatter +- **ESLint** - Code quality +- **Auto Rename Tag** - HTML/JSX helper + +--- + +## 🎓 Learning Resources + +- **React:** https://react.dev/learn +- **TypeScript:** https://www.typescriptlang.org/docs/ +- **Git Basics:** https://git-scm.com/book/en/v2 +- **Tailwind CSS:** https://tailwindcss.com/docs + +--- + +**Remember:** Your branch (akhib/dundu) is YOUR workspace. Feel free to experiment! + +Good luck! 🚀 + 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 c3064fc..9c7339b 100644 --- a/src/pages/AssetDetail.tsx +++ b/src/pages/AssetDetail.tsx @@ -16,7 +16,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) ); @@ -44,13 +43,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 || '', @@ -81,7 +79,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; @@ -92,7 +89,6 @@ const AssetDetail: React.FC = () => { return; } - // Show console log for debugging console.log('Submitting asset data:', formData); try { @@ -110,10 +106,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( @@ -159,8 +153,7 @@ const AssetDetail: React.FC = () => {
); } - - // Show error for duplicate if source asset not found + if (error && isDuplicating) { return (
@@ -169,7 +162,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.

@@ -225,7 +218,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 @@ -322,85 +315,87 @@ const AssetDetail: React.FC = () => { /> -
- - -
+
+ + +
-
- - - {isDuplicating && ( -

- 💡 Duplicating from: {duplicateFromAsset} -

- )} -
+
+ + + {isDuplicating && ( +

+ 💡 Duplicating from: {duplicateFromAsset} +

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

Technical Specs

-
-
- - -
+ {/* COLUMN 2: Technical Specs */} +
+

+ Technical Specs +

+
+
+ + +
-
- - -
+
+ + +
-
- - -
+
+ + +
{/*
+
{/* Location */}
@@ -506,351 +501,67 @@ const AssetDetail: React.FC = () => { onChange={(val) => setFormData({ ...formData, department: val })} /> -
- - -
- -
- - -
- -
- - -
- -
- - -
+
+ +
-
- {/* Coverage */} -
-

Coverage

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