From 6b409c10fe286336c047510b7984a7d3542e13b3 Mon Sep 17 00:00:00 2001 From: KavyaCSH Date: Mon, 8 Jun 2026 12:32:20 +0530 Subject: [PATCH] add remaining fleet operator functionality --- package-lock.json | 161 ++++++++-- src/api/fleet.ts | 55 +++- src/components/Pagination.tsx | 139 ++++++++ src/pages/FleetLogin.tsx | 4 +- src/pages/FleetOperatorDashboard.tsx | 9 +- src/pages/fleet/FleetActiveShifts.tsx | 146 ++++++--- src/pages/fleet/FleetAnalytics.tsx | 30 +- src/pages/fleet/FleetAssets.tsx | 248 +++++++++----- src/pages/fleet/FleetInventory.tsx | 337 ++++++++++---------- src/pages/fleet/FleetOrganization.tsx | 12 +- src/pages/fleet/FleetPendingRequests.tsx | 390 +++++++++++++---------- src/pages/fleet/FleetPersonnel.tsx | 117 +++---- src/pages/fleet/FleetScheduling.tsx | 8 +- src/pages/fleet/FleetTrips.tsx | 102 +++--- src/pages/fleet/FleetWarehouseStock.tsx | 100 +++--- src/pages/fleet/LiveDashboard.tsx | 79 +---- 16 files changed, 1176 insertions(+), 761 deletions(-) create mode 100644 src/components/Pagination.tsx diff --git a/package-lock.json b/package-lock.json index c36470d..c1bad68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,10 +15,12 @@ "react": "^19.2.4", "react-dom": "^19.2.4", "react-router-dom": "^7.14.1", - "recharts": "^3.8.1" + "recharts": "^3.8.1", + "socket.io-client": "^4.8.3" }, "devDependencies": { "@eslint/js": "^9.39.4", + "@types/google.maps": "^3.64.0", "@types/node": "^24.12.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -60,6 +62,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -256,27 +259,6 @@ "node": ">=6.9.0" } }, - "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", - "dev": true, - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", - "dev": true, - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -797,6 +779,29 @@ "node": ">=14.0.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@rolldown/binding-win32-arm64-msvc": { "version": "1.0.0-rc.15", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", @@ -835,6 +840,12 @@ "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", "dev": true }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -920,6 +931,13 @@ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==" }, + "node_modules/@types/google.maps": { + "version": "3.64.0", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.64.0.tgz", + "integrity": "sha512-dN0H6tB4lgLQLovcbPXFYYOEV41TpyyJghzb5jrzjB96FZmjeOghevVdC+BMGd6YqyCqXaggyEtqRXLRjzCBZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -939,6 +957,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "dev": true, + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -948,6 +967,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1008,6 +1028,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.2.tgz", "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", @@ -1275,6 +1296,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1375,6 +1397,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -1618,7 +1641,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "dependencies": { "ms": "^2.1.3" }, @@ -1657,6 +1679,28 @@ "integrity": "sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ==", "dev": true }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/es-toolkit": { "version": "1.45.1", "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", @@ -1688,6 +1732,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2521,8 +2566,7 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/nanoid": { "version": "3.3.11", @@ -2642,6 +2686,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -2699,6 +2744,7 @@ "version": "19.2.5", "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -2707,6 +2753,7 @@ "version": "19.2.5", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -2724,6 +2771,7 @@ "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -2807,7 +2855,8 @@ "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -2910,6 +2959,34 @@ "node": ">=8" } }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2998,6 +3075,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3108,6 +3186,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", "dev": true, + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -3204,6 +3283,35 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -3227,6 +3335,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/api/fleet.ts b/src/api/fleet.ts index 66f87d9..9145007 100644 --- a/src/api/fleet.ts +++ b/src/api/fleet.ts @@ -21,10 +21,18 @@ export const fleetApi = { return apiClient.get(url, { token }); }, + searchStationsByName: async (token: string, stationName: string) => { + return apiClient.get(`/v1/fleet/stations?station_name=${encodeURIComponent(stationName)}`, { token }); + }, + getStationVehicles: async (token: string, stationId: string) => { return apiClient.get(`/v1/fleet/vehicles?station_id=${stationId}`, { token }); }, + getVehiclesByStationName: async (token: string, stationName: string) => { + return apiClient.get(`/v1/fleet/vehicles?station_name=${encodeURIComponent(stationName)}`, { token }); + }, + createVehicle: async (vehicleData: any, token: string) => { return apiClient.post('/v1/fleet/vehicles', vehicleData, { token }); }, @@ -34,6 +42,10 @@ export const fleetApi = { return apiClient.get(url, { token }); }, + getVehiclesByRegistrationNumber: async (token: string, registrationNumber: string) => { + return apiClient.get(`/v1/fleet/vehicles?registration_number=${encodeURIComponent(registrationNumber)}`, { token }); + }, + updateVehicleDetails: async (vehicleId: string, vehicleData: any, token: string) => { return apiClient.patch(`/v1/fleet/vehicles/${vehicleId}`, vehicleData, { token }); }, @@ -47,6 +59,23 @@ export const fleetApi = { return apiClient.get(url, { token }); }, + searchStaff: async (token: string, params: { role?: string; name?: string }) => { + const query = new URLSearchParams(); + if (params.role) query.append('role', params.role); + if (params.name) query.append('name', params.name); + const qs = query.toString(); + const url = qs ? `/v1/fleet/staff?${qs}` : `/v1/fleet/staff`; + return apiClient.get(url, { token }); + }, + + getStaffByRole: async (token: string, role: string) => { + return apiClient.get(`/v1/fleet/staff?role=${encodeURIComponent(role)}`, { token }); + }, + + getStaffByName: async (token: string, name: string) => { + return apiClient.get(`/v1/fleet/staff?name=${encodeURIComponent(name)}`, { token }); + }, + getRoster: async (token: string) => { return apiClient.get('/v1/fleet/roster', { token }); }, @@ -63,6 +92,15 @@ export const fleetApi = { return apiClient.get('/v1/fleet/inventory/master', { token }); }, + searchInventoryMaster: async (token: string, params: { name?: string; category?: string }) => { + const query = new URLSearchParams(); + if (params.name) query.append('name', params.name); + if (params.category) query.append('category', params.category); + const qs = query.toString(); + const url = qs ? `/v1/fleet/inventory/master?${qs}` : `/v1/fleet/inventory/master`; + return apiClient.get(url, { token }); + }, + createInventoryMaster: async (payload: any[], token: string) => { return apiClient.post('/v1/fleet/inventory/master', payload, { token }); }, @@ -84,6 +122,15 @@ export const fleetApi = { return apiClient.get('/v1/fleet/inventory/warehouse', { token }); }, + searchWarehouseStock: async (token: string, params: { name?: string; category?: string }) => { + const query = new URLSearchParams(); + if (params.name) query.append('name', params.name); + if (params.category) query.append('category', params.category); + const qs = query.toString(); + const url = qs ? `/v1/fleet/inventory/warehouse?${qs}` : `/v1/fleet/inventory/warehouse`; + return apiClient.get(url, { token }); + }, + assignToVehicle: async (vehicleId: string, payload: any, token: string) => { return apiClient.post(`/v1/fleet/vehicles/${vehicleId}/inventory/bulk`, payload, { token }); }, @@ -92,8 +139,12 @@ export const fleetApi = { return apiClient.get('/v1/fleet/inventory/restock-requests?status=PENDING', { token }); }, - getRestockRequests: async (token: string, status?: string) => { - const url = status ? `/v1/fleet/inventory/restock-requests?status=${status}` : '/v1/fleet/inventory/restock-requests'; + getRestockRequests: async (token: string, params?: { status?: string; search?: string }) => { + const query = new URLSearchParams(); + if (params?.status) query.append('status', params.status); + if (params?.search) query.append('search', params.search); + const qs = query.toString(); + const url = qs ? `/v1/fleet/inventory/restock-requests?${qs}` : `/v1/fleet/inventory/restock-requests`; return apiClient.get(url, { token }); }, diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx new file mode 100644 index 0000000..403d327 --- /dev/null +++ b/src/components/Pagination.tsx @@ -0,0 +1,139 @@ +import React from 'react'; +import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react'; + +export interface PaginationProps { + currentPage: number; + totalItems: number; + itemsPerPage: number; + onPageChange: (page: number) => void; + onItemsPerPageChange?: (itemsPerPage: number) => void; + itemsPerPageOptions?: number[]; + itemsLabel?: string; + simpleMode?: boolean; +} + +export const Pagination: React.FC = ({ + currentPage, + totalItems, + itemsPerPage, + onPageChange, + onItemsPerPageChange, + itemsPerPageOptions = [5, 10, 20, 50], + itemsLabel = 'items', + simpleMode = false +}) => { + const totalPages = Math.max(1, Math.ceil(totalItems / itemsPerPage)); + const safePage = Math.min(currentPage, totalPages); + + if (simpleMode) { + const indexOfFirstItem = (safePage - 1) * itemsPerPage; + const indexOfLastItem = safePage * itemsPerPage; + + return ( +
+
+ Showing {totalItems === 0 ? 0 : indexOfFirstItem + 1} to {Math.min(indexOfLastItem, totalItems)} of {totalItems} {itemsLabel} +
+
+ +
+ {safePage} / {totalPages} +
+ +
+
+ ); + } + + return ( +
+
+ + Showing {totalItems === 0 ? 0 : (safePage - 1) * itemsPerPage + 1}–{Math.min(safePage * itemsPerPage, totalItems)} of {totalItems} {itemsLabel} + + {onItemsPerPageChange && ( +
+ Rows per page: + +
+ )} +
+
+ + + + {Array.from({ length: totalPages }, (_, i) => i + 1) + .filter(p => p === 1 || p === totalPages || Math.abs(p - safePage) <= 1) + .map((p, i, arr) => ( + + {i > 0 && arr[i - 1] !== p - 1 && ...} + + + ))} + + + +
+
+ ); +}; diff --git a/src/pages/FleetLogin.tsx b/src/pages/FleetLogin.tsx index 2a9ec56..c0d1545 100644 --- a/src/pages/FleetLogin.tsx +++ b/src/pages/FleetLogin.tsx @@ -98,7 +98,7 @@ export const FleetLogin = () => { localStorage.setItem('teleems_user', JSON.stringify({ ...fallback, roles: ['Fleet Operator'] })); } - navigate('/fleet-operator?tab=organization'); + navigate('/fleet-operator?tab=overview'); } } else { setShowError(loginJson?.message || 'Access Denied: Invalid Credentials'); @@ -128,7 +128,7 @@ export const FleetLogin = () => { const userToStore = { ...baseUser, roles, mfa_enabled: true }; localStorage.setItem('teleems_user', JSON.stringify(userToStore)); - navigate('/fleet-operator?tab=organization'); + navigate('/fleet-operator?tab=overview'); } else { setShowError('Invalid Security Token'); } diff --git a/src/pages/FleetOperatorDashboard.tsx b/src/pages/FleetOperatorDashboard.tsx index 62ccc13..b16c537 100644 --- a/src/pages/FleetOperatorDashboard.tsx +++ b/src/pages/FleetOperatorDashboard.tsx @@ -120,14 +120,7 @@ export const FleetOperatorDashboard: React.FC = () => {
-
- - -
+
{error && ( -
- {error} +
+ {error}
)} {loading ? ( -
- +
+ + Fetching active shift telemetries...
) : shifts.length === 0 ? ( -
- -

No active shifts at the moment. All shifts have been completed or none are currently scheduled.

+
+ +

All Clear

+

No active shifts at the moment. All shifts have been completed or none are currently scheduled.

) : ( -
+
{shifts.map((shift, i) => ( -
-
- {/* Vehicle identifier */} -
- - + {/* Top Accent Line */} +
+ +
+
+
+ +
+
+
{shift.vehicle?.registration_number || 'Unknown Vehicle'} - - - {shift.vehicle?.vehicle_type || 'N/A'} - -
- - {/* Status Badge */} -
- - {shift.status} -
- - {/* Time details */} -
- Started: {new Date(shift.startTime).toLocaleString()} +
+
+ {shift.vehicle?.vehicle_type || 'N/A Type'} +
+ + {/* Status Badge */} +
+ + {shift.status} +
+
+ + {/* Time details */} +
+
Shift Started At
+
+ {new Date(shift.startTime).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })} +
{/* Crew Members */} -
+
+
Assigned Crew
+ {/* Driver */} -
- +
+
+ +
-
Pilot Driver
-
+
{shift.driver?.user?.name || 'Driver Profile'}
+
Pilot / Driver
{/* Staff / EMT */} -
- +
+
+ +
-
- {shift.staff?.type === 'DOCTOR' ? 'Medical Doctor' : 'Emergency Medical Technician'} -
-
+
{shift.staff?.user?.name || 'EMT/Staff Profile'}
+
+ {shift.staff?.type === 'DOCTOR' ? 'Medical Doctor' : 'Emergency Medical Tech'} +
diff --git a/src/pages/fleet/FleetAnalytics.tsx b/src/pages/fleet/FleetAnalytics.tsx index 3beee44..7f680bf 100644 --- a/src/pages/fleet/FleetAnalytics.tsx +++ b/src/pages/fleet/FleetAnalytics.tsx @@ -31,6 +31,7 @@ import { } from 'recharts'; import { motion } from 'framer-motion'; import { fleetApi } from '../../api/fleet'; +import { Pagination } from '../../components/Pagination'; interface APIVehicle { id: string; @@ -53,6 +54,8 @@ export const FleetAnalytics: React.FC = () => { const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [timeRange, setTimeRange] = useState<'7D' | '30D' | 'ALL'>('ALL'); + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage, setItemsPerPage] = useState(5); const token = localStorage.getItem('teleems_token') || ''; @@ -220,19 +223,18 @@ export const FleetAnalytics: React.FC = () => { return null; }; + const totalPages = Math.ceil(vehicles.length / itemsPerPage); + const safePage = Math.min(currentPage, Math.max(1, totalPages)); + const indexOfLastVehicle = safePage * itemsPerPage; + const indexOfFirstVehicle = indexOfLastVehicle - itemsPerPage; + const currentVehicles = vehicles.slice(indexOfFirstVehicle, indexOfLastVehicle); + return (
{/* HEADER CONTROLS */}
-
-

- Fleet Telemetry & Resource Intelligence -

-

- Real-time analytics harvested from live active-shifts, scheduling rosters and vehicle metadata. -

-
+
@@ -542,7 +544,7 @@ export const FleetAnalytics: React.FC = () => { - {vehicles.map((v) => { + {currentVehicles.map((v) => { // Extract active driver/staff from shift or roster const driverName = v.activeShift?.driver?.user?.name || v.activeRoster?.driver?.user?.name || '—'; const driverPhone = v.activeShift?.driver?.user?.phone || v.activeRoster?.driver?.user?.phone || ''; @@ -604,6 +606,16 @@ export const FleetAnalytics: React.FC = () => {
+ {vehicles.length > 0 && ( + + )}
diff --git a/src/pages/fleet/FleetAssets.tsx b/src/pages/fleet/FleetAssets.tsx index fe2c955..b106342 100644 --- a/src/pages/fleet/FleetAssets.tsx +++ b/src/pages/fleet/FleetAssets.tsx @@ -4,6 +4,7 @@ import { AlertCircle, ChevronDown, Edit2 } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; +import { fleetApi } from '../../api/fleet'; interface Station { id: string; @@ -17,6 +18,10 @@ interface Vehicle { brand?: string; model?: string; chassis_number?: string; + engine_number?: string; + make_year?: number; + stretcher_capacity?: number; + ownership_type?: string; station_id?: string; status?: string; } @@ -28,9 +33,14 @@ interface VehicleForm { brand: string; model: string; station_id: string; + engine_number: string; + make_year: number | ''; + stretcher_capacity: number | ''; + ownership_type: string; } const VEHICLE_TYPES = ['ALS', 'BLS', 'TRANSPORT', 'ICU', 'NEONATAL']; +const OWNERSHIP_TYPES = ['self-owned', 'hospital-owned', 'leased']; const EMPTY_FORM: VehicleForm = { registration_number: '', @@ -39,6 +49,10 @@ const EMPTY_FORM: VehicleForm = { brand: '', model: '', station_id: '', + engine_number: '', + make_year: '', + stretcher_capacity: '', + ownership_type: 'self-owned', }; const cardStyle: React.CSSProperties = { @@ -104,10 +118,7 @@ export const FleetAssets: React.FC = () => { const fetchStations = useCallback(async () => { setStationsLoading(true); try { - const res = await fetch('https://teleems-api-gateway.onrender.com/v1/fleet/stations', { - headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, - }); - const json = await res.json(); + const json = await fleetApi.getStations(token); let list: Station[] = []; if (json?.data?.data && Array.isArray(json.data.data)) list = json.data.data; else if (json?.data && Array.isArray(json.data)) list = json.data; @@ -123,14 +134,10 @@ export const FleetAssets: React.FC = () => { const fetchVehicles = useCallback(async () => { setLoading(true); try { - const url = selectedStationFilter - ? `https://teleems-api-gateway.onrender.com/v1/fleet/vehicles?station_id=${selectedStationFilter}` - : 'https://teleems-api-gateway.onrender.com/v1/fleet/vehicles'; + const json = selectedStationFilter + ? await fleetApi.getVehiclesByStationName(token, selectedStationFilter) + : await fleetApi.getVehicles(token); - const res = await fetch(url, { - headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, - }); - const json = await res.json(); let list: Vehicle[] = []; if (json?.data?.data && Array.isArray(json.data.data)) list = json.data.data; else if (json?.data && Array.isArray(json.data)) list = json.data; @@ -145,6 +152,27 @@ export const FleetAssets: React.FC = () => { useEffect(() => { fetchVehicles(); fetchStations(); }, [fetchVehicles, fetchStations]); + // Debounced search by registration_number — fires 400ms after user stops typing + useEffect(() => { + if (!searchQuery.trim()) return; // empty → fetchVehicles handles it via selectedStationFilter + const timer = setTimeout(async () => { + setLoading(true); + try { + const json = await fleetApi.getVehiclesByRegistrationNumber(token, searchQuery.trim()); + let list: Vehicle[] = []; + if (json?.data?.data && Array.isArray(json.data.data)) list = json.data.data; + else if (json?.data && Array.isArray(json.data)) list = json.data; + else if (Array.isArray(json)) list = json; + setVehicles(list); + } catch (e) { + console.error('Failed to search vehicles:', e); + } finally { + setLoading(false); + } + }, 400); + return () => clearTimeout(timer); + }, [searchQuery, token]); + const openModal = () => { setForm(EMPTY_FORM); setShowModal(true); @@ -159,6 +187,10 @@ export const FleetAssets: React.FC = () => { brand: v.brand || '', model: v.model || '', station_id: v.station_id || '', + engine_number: v.engine_number || '', + make_year: v.make_year || '', + stretcher_capacity: v.stretcher_capacity || '', + ownership_type: v.ownership_type || 'self-owned', }); setShowEditModal(true); }; @@ -175,23 +207,20 @@ export const FleetAssets: React.FC = () => { chassis_number: editForm.chassis_number || undefined, brand: editForm.brand, model: editForm.model, + engine_number: editForm.engine_number || undefined, + make_year: editForm.make_year ? Number(editForm.make_year) : undefined, + stretcher_capacity: editForm.stretcher_capacity ? Number(editForm.stretcher_capacity) : undefined, + ownership_type: editForm.ownership_type || undefined, }; - const res = await fetch( - `https://teleems-api-gateway.onrender.com/v1/fleet/vehicles/${editingVehicle.id}`, - { - method: 'PATCH', - headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - } - ); - const json = await res.json(); - if (res.ok || json?.id || json?.data?.id) { + const json = await fleetApi.updateVehicleDetails(editingVehicle.id, payload, token); + if (!json) return; + if (!json.status || json.status < 400 || json?.id || json?.data?.id) { showToast('success', 'Vehicle updated successfully!'); setShowEditModal(false); setEditingVehicle(null); fetchVehicles(); } else { - showToast('error', json?.message || `Error ${res.status}: Failed to update vehicle.`); + showToast('error', json?.message || `Error ${json.status || ''}: Failed to update vehicle.`); } } catch (err: any) { showToast('error', err?.message || 'Network error.'); @@ -215,25 +244,21 @@ export const FleetAssets: React.FC = () => { chassis_number: form.chassis_number || undefined, brand: form.brand, model: form.model, + engine_number: form.engine_number || undefined, + make_year: form.make_year ? Number(form.make_year) : undefined, + stretcher_capacity: form.stretcher_capacity ? Number(form.stretcher_capacity) : undefined, + ownership_type: form.ownership_type || undefined, }; - const res = await fetch('https://teleems-api-gateway.onrender.com/v1/fleet/vehicles', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }); - - const json = await res.json(); - if (res.ok || res.status === 201 || json?.id || json?.data?.id) { + const json = await fleetApi.createVehicle(payload, token); + if (!json) return; + if (!json.status || json.status < 400 || json?.id || json?.data?.id) { showToast('success', `Vehicle "${form.registration_number}" registered successfully!`); setShowModal(false); setForm(EMPTY_FORM); fetchVehicles(); } else { - showToast('error', json?.message || `Error ${res.status}: Failed to register vehicle.`); + showToast('error', json?.message || `Error ${json.status || ''}: Failed to register vehicle.`); } } catch (err: any) { showToast('error', err?.message || 'Network error. Please check your connection.'); @@ -242,11 +267,12 @@ export const FleetAssets: React.FC = () => { } }; - const filtered = vehicles.filter(v => - v.registration_number?.toLowerCase().includes(searchQuery.toLowerCase()) || - v.brand?.toLowerCase().includes(searchQuery.toLowerCase()) || - v.model?.toLowerCase().includes(searchQuery.toLowerCase()) - ); + // When search is cleared, reload via fetchVehicles (respects station filter) + useEffect(() => { + if (!searchQuery.trim()) { + fetchVehicles(); + } + }, [searchQuery, fetchVehicles]); const getStatusColor = (status?: string) => { if (status === 'ACTIVE') return { bg: 'rgba(34,197,94,0.1)', border: 'rgba(34,197,94,0.2)', color: '#22C55E' }; @@ -302,22 +328,35 @@ export const FleetAssets: React.FC = () => {
-
- - setForm(f => ({ ...f, registration_number: e.target.value }))} /> +
+
+ + setForm(f => ({ ...f, registration_number: e.target.value }))} /> +
+
+ + + +
-
- - -
-
- - +
+
+ + + +
+
+ + + +
@@ -329,9 +368,25 @@ export const FleetAssets: React.FC = () => { setForm(f => ({ ...f, model: e.target.value }))} />
-
- - setForm(f => ({ ...f, chassis_number: e.target.value }))} /> +
+
+ + setForm(f => ({ ...f, make_year: e.target.value ? Number(e.target.value) : '' }))} /> +
+
+ + setForm(f => ({ ...f, stretcher_capacity: e.target.value ? Number(e.target.value) : '' }))} /> +
+
+
+
+ + setForm(f => ({ ...f, chassis_number: e.target.value }))} /> +
+
+ + setForm(f => ({ ...f, engine_number: e.target.value }))} /> +
@@ -367,22 +422,35 @@ export const FleetAssets: React.FC = () => {
-
- - setEditForm(f => ({ ...f, registration_number: e.target.value }))} /> +
+
+ + setEditForm(f => ({ ...f, registration_number: e.target.value }))} /> +
+
+ + + +
-
- - -
-
- - +
+
+ + + +
+
+ + + +
@@ -394,9 +462,25 @@ export const FleetAssets: React.FC = () => { setEditForm(f => ({ ...f, model: e.target.value }))} />
-
- - setEditForm(f => ({ ...f, chassis_number: e.target.value }))} /> +
+
+ + setEditForm(f => ({ ...f, make_year: e.target.value ? Number(e.target.value) : '' }))} /> +
+
+ + setEditForm(f => ({ ...f, stretcher_capacity: e.target.value ? Number(e.target.value) : '' }))} /> +
+
+
+
+ + setEditForm(f => ({ ...f, chassis_number: e.target.value }))} /> +
+
+ + setEditForm(f => ({ ...f, engine_number: e.target.value }))} /> +
@@ -488,7 +572,7 @@ export const FleetAssets: React.FC = () => { > {stations.map(s => ( - + ))} {
)} - {!loading && filtered.length === 0 && ( + {!loading && vehicles.length === 0 && (

No Vehicles Found

)} - {!loading && filtered.length > 0 && ( + {!loading && vehicles.length > 0 && (
- {filtered.map(v => { + {vehicles.map(v => { const sc = getStatusColor(v.status); return ( {
{v.vehicle_type} UNIT

{v.registration_number}

-
{v.brand} {v.model}
+
{v.brand} {v.model} {v.make_year ? `(${v.make_year})` : ''}
{v.status}
+
+ {v.ownership_type && Ownership: {v.ownership_type}} + {v.stretcher_capacity ? Capacity: {v.stretcher_capacity} Pts : null} +
)}
+ + + + +
@@ -1006,42 +1017,14 @@ export const FleetInventory: React.FC = () => { {/* Pagination Controls */} -
-
- - Showing {filteredInventory.length === 0 ? 0 : (safePage - 1) * itemsPerPage + 1}–{Math.min(safePage * itemsPerPage, filteredInventory.length)} of {filteredInventory.length} items - -
- Rows per page: - -
-
-
- - - - {Array.from({ length: totalPages }, (_, i) => i + 1) - .filter(p => p === 1 || p === totalPages || Math.abs(p - safePage) <= 1) - .map((p, i, arr) => ( - - {i > 0 && arr[i - 1] !== p - 1 && ...} - - - ))} - - - -
-
+ )}
diff --git a/src/pages/fleet/FleetOrganization.tsx b/src/pages/fleet/FleetOrganization.tsx index e088ed8..3187389 100644 --- a/src/pages/fleet/FleetOrganization.tsx +++ b/src/pages/fleet/FleetOrganization.tsx @@ -114,6 +114,7 @@ export const FleetOrganization: React.FC = () => { } const url = 'https://teleems-api-gateway.onrender.com/v1/fleet/stations'; + console.log('[Stations] Calling:', url, '| token starts with:', authToken.substring(0, 15)); const res = await fetch(url, { @@ -186,7 +187,16 @@ export const FleetOrganization: React.FC = () => { }) ); - setStations(mergedList); + // Normalize field names: API may return station_name instead of name + const normalizedList = mergedList.map((s: any) => ({ + ...s, + name: s.name || s.station_name || '', + address: s.address || s.station_address || '', + incharge_name: s.incharge_name || s.incharge || '', + phone: s.phone || s.contact_phone || s.contact || '', + })); + + setStations(normalizedList); } catch (e) { setFetchError('Network error — unable to reach the server. Check your connection.'); console.error('Failed to fetch stations:', e); diff --git a/src/pages/fleet/FleetPendingRequests.tsx b/src/pages/fleet/FleetPendingRequests.tsx index 4b1e121..d285371 100644 --- a/src/pages/fleet/FleetPendingRequests.tsx +++ b/src/pages/fleet/FleetPendingRequests.tsx @@ -7,15 +7,15 @@ import { Package, CheckCircle, Clock, - ChevronLeft, - ChevronRight, Search, Check, - Truck + Truck, + X } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { Card } from '../../components/Common'; import { fleetApi } from '../../api/fleet'; +import { Pagination } from '../../components/Pagination'; export const FleetPendingRequests: React.FC = () => { const [, setSearchParams] = useSearchParams(); @@ -28,12 +28,12 @@ export const FleetPendingRequests: React.FC = () => { const [searchQuery, setSearchQuery] = useState(''); const [isSearchFocused, setIsSearchFocused] = useState(false); const [currentPage, setCurrentPage] = useState(1); - const itemsPerPage = 8; + const [itemsPerPage, setItemsPerPage] = useState(8); // Inline loading when clicking action buttons const [processingId, setProcessingId] = useState(null); - const fetchRequests = async () => { + const fetchRequests = async (searchFilter?: string) => { setLoading(true); setError(null); try { @@ -42,8 +42,11 @@ export const FleetPendingRequests: React.FC = () => { setError('Session token expired. Please log in.'); return; } - // Fetch with specific status filter (PENDING, APPROVED, COMPLETED, or '' for all) - const res = await fleetApi.getRestockRequests(token, statusFilter || undefined); + const params: any = {}; + if (statusFilter) params.status = statusFilter; + if (searchFilter) params.search = searchFilter; + + const res = await fleetApi.getRestockRequests(token, params); const data = res?.data?.data || res?.data || []; setRequests(Array.isArray(data) ? data : []); setCurrentPage(1); @@ -55,9 +58,26 @@ export const FleetPendingRequests: React.FC = () => { } }; + const isFirstMount = React.useRef(true); + + // Initial load useEffect(() => { fetchRequests(); - }, [statusFilter]); + }, []); + + // Debounced search and status filter + useEffect(() => { + if (isFirstMount.current) { + isFirstMount.current = false; + return; + } + + const timer = setTimeout(() => { + fetchRequests(searchQuery.trim() || undefined); + }, 400); + + return () => clearTimeout(timer); + }, [searchQuery, statusFilter]); const handleUpdateStatus = async (requestId: string, nextStatus: 'APPROVED' | 'COMPLETED') => { if (!requestId) return; @@ -76,20 +96,8 @@ export const FleetPendingRequests: React.FC = () => { } }; - // Filter local requests on top of status filter using search query - const filteredRequests = requests.filter(req => { - const itemName = req.item_master?.name || ''; - const itemCategory = req.item_master?.category || ''; - const requestedBy = typeof req.requested_by === 'object' && req.requested_by !== null - ? (req.requested_by.name || req.requested_by.username || '') - : String(req.requested_by || ''); - - return ( - itemName.toLowerCase().includes(searchQuery.toLowerCase()) || - itemCategory.toLowerCase().includes(searchQuery.toLowerCase()) || - requestedBy.toLowerCase().includes(searchQuery.toLowerCase()) - ); - }); + // The API now handles filtering by search and status + const filteredRequests = requests; // Pagination calculation const totalPages = Math.ceil(filteredRequests.length / itemsPerPage); @@ -145,7 +153,6 @@ export const FleetPendingRequests: React.FC = () => { value={searchQuery} onChange={e => { setSearchQuery(e.target.value); - setCurrentPage(1); }} onFocus={() => setIsSearchFocused(true)} onBlur={() => setIsSearchFocused(false)} @@ -156,9 +163,32 @@ export const FleetPendingRequests: React.FC = () => { color: '#0F172A', fontSize: '0.875rem', outline: 'none', - width: '100%' + width: '100%', + paddingRight: searchQuery ? '24px' : '0' }} /> + {searchQuery && ( + + )}
{/* Status filters */} @@ -226,166 +256,202 @@ export const FleetPendingRequests: React.FC = () => {

There are no restock requests matching this state filter right now.

) : ( - - - - - - - - - - - - +
+ {currentItems.map((req) => { const requestedUser = typeof req.requested_by === 'object' && req.requested_by !== null ? (req.requested_by.name || req.requested_by.username || 'Unknown User') : (req.requested_by || 'N/A'); return ( -
- - - - - - - + +
+ {statusFilter === 'PENDING' && ( + handleUpdateStatus(req.id, 'APPROVED')} + style={{ + background: 'linear-gradient(135deg, #10B981 0%, #059669 100%)', + border: 'none', + color: '#FFFFFF', + borderRadius: '10px', + padding: '8px 18px', + fontSize: '0.8rem', + fontWeight: 700, + cursor: processingId !== null ? 'not-allowed' : 'pointer', + display: 'inline-flex', + alignItems: 'center', + gap: '6px', + opacity: processingId !== null ? 0.7 : 1, + boxShadow: '0 4px 12px rgba(16, 185, 129, 0.25)' + }} + > + {processingId === req.id ? ( + + ) : ( + + )} + APPROVE + + )} + {statusFilter === 'APPROVED' && ( + handleUpdateStatus(req.id, 'COMPLETED')} + style={{ + background: 'linear-gradient(135deg, #3B82F6 0%, #2563EB 100%)', + border: 'none', + color: '#FFFFFF', + borderRadius: '10px', + padding: '8px 18px', + fontSize: '0.8rem', + fontWeight: 700, + cursor: processingId !== null ? 'not-allowed' : 'pointer', + display: 'inline-flex', + alignItems: 'center', + gap: '6px', + opacity: processingId !== null ? 0.7 : 1, + boxShadow: '0 4px 12px rgba(59, 130, 246, 0.25)' + }} + > + {processingId === req.id ? ( + + ) : ( + + )} + HAND OVER + + )} + {statusFilter === 'COMPLETED' && ( +
+ COMPLETED +
+ )} +
+ + ); })} - -
Requested Supply ItemQuantity DemandedRequested ByRequest DateStatus BadgeActions
-
- {req.item_master?.name || 'Unknown Item'} + + {/* Subtle status indicator line on the left */} +
+ +
+
+
-
- Category: {req.item_master?.category || 'General'} +
+
+ {req.items && req.items.length > 0 ? req.items[0].name : 'Unknown Item'} +
+
+ {req.vehicle?.registration_number || 'Medical Supply'} +
-
-
- {req.quantity} Units +
+ +
+
Quantity
+
+ {req.items && req.items.length > 0 ? req.items[0].quantityRequested : 0} Units
-
-
+
+ +
+
Requested By
+
+
+ {requestedUser.charAt(0).toUpperCase()} +
{requestedUser}
-
-
+
+ {new Date(req.createdAt || req.updatedAt).toLocaleDateString()}
-
+ + +
{req.status} -
- {statusFilter === 'PENDING' && ( - - )} - {statusFilter === 'APPROVED' && ( - - )} - {statusFilter === 'COMPLETED' && ( -
- TRANSACTION CLOSED -
- )} -
+ +
)} - {!loading && !error && filteredRequests.length > itemsPerPage && ( -
-
- Showing {indexOfFirstItem + 1} to {Math.min(indexOfLastItem, filteredRequests.length)} of {filteredRequests.length} transactions -
-
- -
- {currentPage} / {totalPages} -
- -
-
+ {!loading && !error && ( + )}
diff --git a/src/pages/fleet/FleetPersonnel.tsx b/src/pages/fleet/FleetPersonnel.tsx index d1c0701..6ac0f78 100644 --- a/src/pages/fleet/FleetPersonnel.tsx +++ b/src/pages/fleet/FleetPersonnel.tsx @@ -1,6 +1,8 @@ import React, { useState, useEffect, useCallback } from 'react'; -import { UserPlus, Search, ShieldCheck, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Loader2, AlertCircle, Phone, Mail, X, Eye, EyeOff } from 'lucide-react'; +import { UserPlus, Search, ShieldCheck, ChevronLeft, ChevronRight, Loader2, AlertCircle, Phone, Mail, X, Eye, EyeOff } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; +import { Pagination } from '../../components/Pagination'; +import { fleetApi } from '../../api/fleet'; interface Staff { id: string; type?: string; status?: string; createdAt?: string; aadhaar_number?: string; @@ -19,6 +21,13 @@ const STATUS_CFG: Record = { ON_LEAVE: { label: 'On Leave', color: '#F59E0B' }, ACTIVE: { label: 'Active', color: '#10B981' }, INACTIVE: { label: 'Inactive', color: '#EF4444' }, }; const FILTERS = ['ALL', 'DRIVER', 'EMT', 'DOCTOR'] as const; + +// Maps the UI filter key → the role string the API expects +const ROLE_API_MAP: Record = { + DRIVER: 'Ambulance Pilot (Driver)', + EMT: 'EMT / Paramedic', + DOCTOR: 'Doctor', +}; const th: React.CSSProperties = { padding: '14px 18px', fontSize: '0.68rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700, textAlign: 'left', borderBottom: '1px solid #CBD5E1', background: '#F1F5F9', whiteSpace: 'nowrap' }; const norm = (s: Staff) => { @@ -59,19 +68,24 @@ export const FleetPersonnel: React.FC = () => { const resetModal = () => { setForm({ name:'', phone:'', password:'', aadhaar_number:'', license_number:'', license_category:'', license_expiry:'', qualification:'', certification_body:'', certificate_number:'', certificate_expiry:'', medical_registration_number:'', specialization:'', teleconsult_available: false }); setStaffType('DRIVER'); setSubmitErr(''); setSubmitOk(''); }; const token = localStorage.getItem('teleems_token') || ''; - const fetchStaff = useCallback(async () => { + const fetchStaff = useCallback(async (role?: string, staffName?: string) => { setLoading(true); setFetchError(''); try { - const res = await fetch('https://teleems-api-gateway.onrender.com/v1/fleet/staff', { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); - const json = await res.json(); - if (res.status === 401 || res.status === 403) { setFetchError('Session expired.'); return; } - if (!res.ok) { setFetchError(json?.message || `Error ${res.status}`); return; } + console.log('[Staff] Calling fleetApi with role:', role || 'ALL', 'and name:', staffName || 'none'); + const res = await fleetApi.searchStaff(token, { role, name: staffName }); + + const json = res; + let list: Staff[] = []; if (json?.data?.data && Array.isArray(json.data.data)) list = json.data.data; else if (json?.data && Array.isArray(json.data)) list = json.data; else if (Array.isArray(json)) list = json; setStaffList(list); - } catch (e) { setFetchError('Network error.'); } finally { setLoading(false); } + } catch (e: any) { + setFetchError(e?.message || 'Network error.'); + } finally { + setLoading(false); + } }, [token]); const submitStaff = async () => { @@ -83,26 +97,48 @@ export const FleetPersonnel: React.FC = () => { if (staffType === 'DOCTOR') professional_details = { medical_registration_number: form.medical_registration_number, specialization: form.specialization, qualification: form.qualification, teleconsult_available: form.teleconsult_available }; const body = { name: form.name, phone: form.phone, password: form.password, type: staffType, aadhaar_number: form.aadhaar_number, professional_details }; try { - const res = await fetch('https://teleems-api-gateway.onrender.com/v1/fleet/staff', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); - const json = await res.json(); - if (!res.ok) { setSubmitErr(json?.message || `Error ${res.status}`); return; } + await fleetApi.createStaff(body, token); setSubmitOk(`${staffType} registered successfully!`); resetModal(); fetchStaff(); setTimeout(() => { setShowModal(false); setSubmitOk(''); }, 1500); - } catch { setSubmitErr('Network error.'); } finally { setSubmitting(false); } + } catch (e: any) { + setSubmitErr(e?.message || 'Network error.'); + } finally { + setSubmitting(false); + } }; + // Initial load useEffect(() => { fetchStaff(); }, [fetchStaff]); + // Re-fetch from API whenever the role filter tab changes or search clears (immediately) + useEffect(() => { + const apiRole = ROLE_API_MAP[filter]; + if (!search.trim()) { + fetchStaff(apiRole); + } + }, [filter, search, fetchStaff]); + + // Debounced API search for name — fires 400ms after user stops typing + useEffect(() => { + if (!search.trim()) return; + const timer = setTimeout(() => { + const apiRole = ROLE_API_MAP[filter]; + fetchStaff(apiRole, search.trim()); + setPage(1); + }, 400); + return () => clearTimeout(timer); + }, [search, filter, fetchStaff]); + const normalized = staffList.map(norm); - const filtered = normalized.filter(s => (filter === 'ALL' || s.role === filter) && (s.name.toLowerCase().includes(search.toLowerCase()) || s.role.toLowerCase().includes(search.toLowerCase()))); + // Local filtering is removed since search and role are handled by API. We just assign normalized. + const filtered = normalized; const totalPages = Math.max(1, Math.ceil(filtered.length / itemsPerPage)); const safePage = Math.min(page, totalPages); const pageData = filtered.slice((safePage - 1) * itemsPerPage, safePage * itemsPerPage); const onDuty = normalized.filter(s => ['ON_DUTY', 'ACTIVE'].includes(s.status)).length; const offDuty = normalized.filter(s => s.status === 'OFF_DUTY').length; - const onLeave = normalized.filter(s => s.status === 'ON_LEAVE').length; // ── DETAIL PAGE ── if (selectedRaw) { @@ -325,8 +361,8 @@ export const FleetPersonnel: React.FC = () => { {/* Stats */} -
- {[{ label: 'Total Staff', value: normalized.length, color: '#06B6D4' }, { label: 'On Duty', value: onDuty, color: '#10B981' }, { label: 'Off Duty', value: offDuty, color: '#64748B' }, { label: 'On Leave', value: onLeave, color: '#F59E0B' }].map(s => ( +
+ {[{ label: 'Total Staff', value: normalized.length, color: '#06B6D4' }, { label: 'On Duty', value: onDuty, color: '#10B981' }, { label: 'Off Duty', value: offDuty, color: '#64748B' }].map(s => (
{loading ? '—' : s.value}
{s.label}
@@ -362,7 +398,7 @@ export const FleetPersonnel: React.FC = () => { type="text" placeholder="Search staff..." value={search} - onChange={(e) => { setSearch(e.target.value); setPage(1); }} + onChange={(e) => { setSearch(e.target.value); }} onFocus={() => setIsSearchFocused(true)} onBlur={() => setIsSearchFocused(false)} className="stations-search-input" @@ -378,7 +414,7 @@ export const FleetPersonnel: React.FC = () => { /> {search && ( +
)} @@ -466,43 +502,14 @@ export const FleetPersonnel: React.FC = () => { })} - {/* Pagination */} -
-
- - Showing {filtered.length === 0 ? 0 : (safePage - 1) * itemsPerPage + 1}–{Math.min(safePage * itemsPerPage, filtered.length)} of {filtered.length} staff - -
- Rows per page: - -
-
-
- - - - {Array.from({ length: totalPages }, (_, i) => i + 1) - .filter(p => p === 1 || p === totalPages || Math.abs(p - safePage) <= 1) - .map((p, i, arr) => ( - - {i > 0 && arr[i - 1] !== p - 1 && ...} - - - ))} - - - -
-
+
)} diff --git a/src/pages/fleet/FleetScheduling.tsx b/src/pages/fleet/FleetScheduling.tsx index 2419c79..ded97a1 100644 --- a/src/pages/fleet/FleetScheduling.tsx +++ b/src/pages/fleet/FleetScheduling.tsx @@ -375,10 +375,10 @@ export const FleetScheduling: React.FC = () => {
{/* Vehicle identifier */} -
- - {a.vehicleReg || a.vehicleId} - {a.vehicleType} +
+ + {a.vehicleReg || a.vehicleId} + {a.vehicleType}
{/* Shift Badge */} diff --git a/src/pages/fleet/FleetTrips.tsx b/src/pages/fleet/FleetTrips.tsx index a9a252a..46d8e41 100644 --- a/src/pages/fleet/FleetTrips.tsx +++ b/src/pages/fleet/FleetTrips.tsx @@ -2,9 +2,10 @@ import React, { useState, useEffect, useCallback } from 'react'; import { Search, MapPin, Truck, Activity, Navigation, User, Calendar, Clock, AlertTriangle, Filter, - ChevronDown, X, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight + ChevronDown, X, ChevronLeft, ChevronRight } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; +import { Pagination } from '../../components/Pagination'; interface Vehicle { id: string; @@ -22,6 +23,7 @@ interface Vehicle { export const FleetTrips: React.FC = () => { const [vehicles, setVehicles] = useState([]); + const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(''); @@ -32,20 +34,26 @@ export const FleetTrips: React.FC = () => { const [page, setPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(5); - const fetchVehicles = useCallback(async () => { + const fetchVehicles = useCallback(async (regNumber: string = '') => { setLoading(true); setError(null); try { const token = localStorage.getItem('teleems_token') || ''; - const res = await fetch('https://teleems-api-gateway.onrender.com/v1/fleet/vehicles', { + let url = 'https://teleems-api-gateway.onrender.com/v1/fleet/vehicles/stats'; + if (regNumber) { + url += `?registration_number=${encodeURIComponent(regNumber)}`; + } + const res = await fetch(url, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, }); const json = await res.json(); - let list: Vehicle[] = []; - if (json?.data?.data && Array.isArray(json.data.data)) list = json.data.data; - else if (json?.data && Array.isArray(json.data)) list = json.data; - else if (Array.isArray(json)) list = json; - setVehicles(list); + + const data = json?.data || {}; + setStats(data); + + const activeList = data.activeVehiclesList || []; + const inactiveList = data.inactiveVehiclesList || []; + setVehicles([...activeList, ...inactiveList]); } catch (e: any) { console.error('Failed to fetch vehicles for trip management:', e); setError(e?.message || 'Failed to fetch vehicles'); @@ -55,8 +63,12 @@ export const FleetTrips: React.FC = () => { }, []); useEffect(() => { - fetchVehicles(); - }, [fetchVehicles]); + const delayDebounceFn = setTimeout(() => { + fetchVehicles(searchQuery); + }, 500); + + return () => clearTimeout(delayDebounceFn); + }, [searchQuery, fetchVehicles]); const filteredVehicles = vehicles.filter(v => { const searchMatch = v.registration_number?.toLowerCase().includes(searchQuery.toLowerCase()) || @@ -92,29 +104,29 @@ export const FleetTrips: React.FC = () => {
-
{loading ? '...' : vehicles.length}
+
{loading ? '...' : stats?.totalVehicles ?? vehicles.length}
Total Vehicles
-
{loading ? '...' : activeTripsCount}
+
{loading ? '...' : stats?.busyVehiclesCount ?? activeTripsCount}
Active Trips (Busy)
-
{loading ? '...' : availableCount}
+
{loading ? '...' : stats?.availableVehiclesCount ?? availableCount}
Available Units
-
{loading ? '...' : vehicles.filter(v => !v.activeShift).length}
-
No Active Shift
+
{loading ? '...' : stats?.inactiveVehiclesCount ?? vehicles.filter(v => !v.activeShift).length}
+
Inactive Vehicles
@@ -279,57 +291,17 @@ export const FleetTrips: React.FC = () => { {/* Pagination Controls */} {!loading && filteredVehicles.length > 0 && ( -
-
- Rows per page: - -
- -
- - {Math.min((safePage - 1) * itemsPerPage + 1, filteredVehicles.length)} - {Math.min(safePage * itemsPerPage, filteredVehicles.length)} of {filteredVehicles.length} - -
- - - - -
+
+
-
- )} + )}