From 5274a58bd1f737192937b0008005be346251ec22 Mon Sep 17 00:00:00 2001 From: Vipeesh Date: Tue, 13 Jan 2026 14:53:14 +0000 Subject: [PATCH] latest changes of ppm --- asm_app/src/App.tsx | 31 +- asm_app/src/components/Sidebar.tsx | 4 +- asm_app/src/hooks/useIssue.ts | 133 ++ asm_app/src/hooks/useMaintenanceTeam.ts | 142 ++ asm_app/src/locales/en/translation.json | 287 ++- asm_app/src/pages/ActiveMap.tsx | 496 +++-- asm_app/src/pages/AssetDetail.tsx | 32 +- asm_app/src/pages/AssetList.tsx | 1 + asm_app/src/pages/AssetMaintenanceDetail.tsx | 2 + asm_app/src/pages/AssetMaintenanceList.tsx | 28 +- asm_app/src/pages/IssueDetail.tsx | 686 ++++++ asm_app/src/pages/IssueList.tsx | 638 ++++++ asm_app/src/pages/MaintenanceTeamDetail.tsx | 623 ++++++ asm_app/src/pages/MaintenanceTeamList.tsx | 570 +++++ asm_app/src/pages/PPMPlanner.tsx | 25 +- asm_app/src/pages/PPMPlannerDetail.tsx | 1958 ++++++++++++----- asm_app/src/pages/PPMPlannerList.tsx | 1087 ++++++++- asm_app/src/pages/WorkOrderDetail.tsx | 90 +- asm_app/src/services/issueService.ts | 241 ++ .../src/services/maintenanceTeamService.ts | 247 +++ asm_app/src/services/ppmPlannerService.ts | 53 +- asm_app/src/services/workOrderService.ts | 1 + asm_app/src/services/workflowService.ts | 5 + .../public/asm_app/assets/index-BRxE8PT9.css | 1 - .../public/asm_app/assets/index-DJlPs5uL.js | 1718 +++++++++++++++ .../public/asm_app/assets/index-DrNfoAwJ.js | 1667 -------------- .../public/asm_app/assets/index-vHgZyeJv.css | 1 + asm_ui_app/public/asm_app/index.html | 4 +- asm_ui_app/www/asm_app.html | 4 +- 29 files changed, 8225 insertions(+), 2550 deletions(-) create mode 100644 asm_app/src/hooks/useIssue.ts create mode 100644 asm_app/src/hooks/useMaintenanceTeam.ts create mode 100644 asm_app/src/pages/IssueDetail.tsx create mode 100644 asm_app/src/pages/IssueList.tsx create mode 100644 asm_app/src/pages/MaintenanceTeamDetail.tsx create mode 100644 asm_app/src/pages/MaintenanceTeamList.tsx create mode 100644 asm_app/src/services/issueService.ts create mode 100644 asm_app/src/services/maintenanceTeamService.ts delete mode 100644 asm_ui_app/public/asm_app/assets/index-BRxE8PT9.css create mode 100644 asm_ui_app/public/asm_app/assets/index-DJlPs5uL.js delete mode 100644 asm_ui_app/public/asm_app/assets/index-DrNfoAwJ.js create mode 100644 asm_ui_app/public/asm_app/assets/index-vHgZyeJv.css diff --git a/asm_app/src/App.tsx b/asm_app/src/App.tsx index 944eab6..04f8cb1 100644 --- a/asm_app/src/App.tsx +++ b/asm_app/src/App.tsx @@ -44,6 +44,10 @@ import ItemDetail from './pages/ItemDetail'; import ComingSoon from './pages/ComingSoon'; import Sidebar from './components/Sidebar'; import Header from './components/Header'; +import IssueList from './pages/IssueList'; +import IssueDetail from './pages/IssueDetail'; +import MaintenanceTeamList from './pages/MaintenanceTeamList'; +import MaintenanceTeamDetail from './pages/MaintenanceTeamDetail'; // Layout with Sidebar and Header const LayoutWithSidebar: React.FC<{ children: React.ReactNode }> = ({ children }) => { @@ -201,10 +205,21 @@ const App: React.FC = () => { } /> - + } /> */} + + + + } /> + + + + } /> { } /> - + } /> */} + + + } /> + + + + + } /> + {/* Default redirect */} } /> diff --git a/asm_app/src/components/Sidebar.tsx b/asm_app/src/components/Sidebar.tsx index 245f9de..dd5b5b7 100644 --- a/asm_app/src/components/Sidebar.tsx +++ b/asm_app/src/components/Sidebar.tsx @@ -142,10 +142,10 @@ const Sidebar: React.FC = ({ userEmail }) => { visible: true }, { - id: 'maintenance-team', + id: 'maintenance-teams', title: 'Maintenance Team', icon: , - path: '/maintenance-team', + path: '/maintenance-teams', visible: true }, { diff --git a/asm_app/src/hooks/useIssue.ts b/asm_app/src/hooks/useIssue.ts new file mode 100644 index 0000000..860a39c --- /dev/null +++ b/asm_app/src/hooks/useIssue.ts @@ -0,0 +1,133 @@ +import { useState, useEffect, useCallback } from 'react'; +import issueService, { type Issue, type CreateIssueData, type IssueListParams } from '../services/issueService'; + +// Hook for fetching issue list +export const useIssueList = (params: IssueListParams = {}) => { + const [issues, setIssues] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [totalCount, setTotalCount] = useState(0); + + const fetchIssues = useCallback(async () => { + try { + setLoading(true); + setError(null); + const response = await issueService.getIssues(params); + setIssues(response.data); + + // Get total count for pagination + const count = await issueService.getIssueCount(params.filters); + setTotalCount(count); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch issues'); + } finally { + setLoading(false); + } + }, [JSON.stringify(params)]); + + useEffect(() => { + fetchIssues(); + }, [fetchIssues]); + + return { + issues, + loading, + error, + totalCount, + refetch: fetchIssues, + }; +}; + +// Hook for fetching single issue details +export const useIssueDetails = (issueName: string | null) => { + const [issue, setIssue] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchIssue = useCallback(async () => { + if (!issueName) { + setIssue(null); + return; + } + + try { + setLoading(true); + setError(null); + const data = await issueService.getIssue(issueName); + setIssue(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch issue details'); + } finally { + setLoading(false); + } + }, [issueName]); + + useEffect(() => { + fetchIssue(); + }, [fetchIssue]); + + return { + issue, + loading, + error, + refetch: fetchIssue, + }; +}; + +// Hook for issue mutations (create, update, delete) +export const useIssueMutations = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const createIssue = async (data: CreateIssueData): Promise => { + try { + setLoading(true); + setError(null); + const result = await issueService.createIssue(data); + return result; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to create issue'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + const updateIssue = async (name: string, data: Partial): Promise => { + try { + setLoading(true); + setError(null); + const result = await issueService.updateIssue(name, data); + return result; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to update issue'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + const deleteIssue = async (name: string): Promise => { + try { + setLoading(true); + setError(null); + await issueService.deleteIssue(name); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to delete issue'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + return { + createIssue, + updateIssue, + deleteIssue, + loading, + error, + }; +}; \ No newline at end of file diff --git a/asm_app/src/hooks/useMaintenanceTeam.ts b/asm_app/src/hooks/useMaintenanceTeam.ts new file mode 100644 index 0000000..7768293 --- /dev/null +++ b/asm_app/src/hooks/useMaintenanceTeam.ts @@ -0,0 +1,142 @@ +import { useState, useEffect, useCallback } from 'react'; +import maintenanceTeamService, { + type MaintenanceTeam, + type CreateMaintenanceTeamData, + type MaintenanceTeamListParams +} from '../services/maintenanceTeamService'; + +// Hook for fetching maintenance team list +export const useMaintenanceTeamList = (params: MaintenanceTeamListParams = {}) => { + const [teams, setTeams] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [totalCount, setTotalCount] = useState(0); + + const fetchTeams = useCallback(async () => { + try { + setLoading(true); + setError(null); + const response = await maintenanceTeamService.getMaintenanceTeams(params); + setTeams(response.data); + + // Get total count for pagination + const count = await maintenanceTeamService.getMaintenanceTeamCount(params.filters); + setTotalCount(count); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch maintenance teams'); + } finally { + setLoading(false); + } + }, [JSON.stringify(params)]); + + useEffect(() => { + fetchTeams(); + }, [fetchTeams]); + + return { + teams, + loading, + error, + totalCount, + refetch: fetchTeams, + }; +}; + +// Hook for fetching single maintenance team details +export const useMaintenanceTeamDetails = (teamName: string | null) => { + const [team, setTeam] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchTeam = useCallback(async () => { + if (!teamName) { + setTeam(null); + return; + } + + try { + setLoading(true); + setError(null); + const data = await maintenanceTeamService.getMaintenanceTeam(teamName); + setTeam(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch maintenance team details'); + } finally { + setLoading(false); + } + }, [teamName]); + + useEffect(() => { + fetchTeam(); + }, [fetchTeam]); + + return { + team, + loading, + error, + refetch: fetchTeam, + }; +}; + +// Hook for maintenance team mutations (create, update, delete) +export const useMaintenanceTeamMutations = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const createTeam = async (data: CreateMaintenanceTeamData): Promise => { + try { + setLoading(true); + setError(null); + const result = await maintenanceTeamService.createMaintenanceTeam(data); + return result; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to create maintenance team'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + const updateTeam = async (name: string, data: Partial): Promise => { + try { + setLoading(true); + setError(null); + const result = await maintenanceTeamService.updateMaintenanceTeam(name, data); + return result; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to update maintenance team'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + const deleteTeam = async (name: string): Promise => { + try { + setLoading(true); + setError(null); + await maintenanceTeamService.deleteMaintenanceTeam(name); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to delete maintenance team'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + const getUserFullName = async (email: string): Promise => { + return await maintenanceTeamService.getUserFullName(email); + }; + + return { + createTeam, + updateTeam, + deleteTeam, + getUserFullName, + loading, + error, + }; +}; \ No newline at end of file diff --git a/asm_app/src/locales/en/translation.json b/asm_app/src/locales/en/translation.json index 6be7219..c02c10c 100644 --- a/asm_app/src/locales/en/translation.json +++ b/asm_app/src/locales/en/translation.json @@ -13,6 +13,7 @@ "cancel": "Cancel", "save": "Save", "delete": "Delete", + "deleting": "Deleting...", "edit": "Edit", "create": "Create", "search": "Search", @@ -27,12 +28,21 @@ "lightMode": "Light Mode", "language": "Language", "english": "English", - "arabic": "Arabic" + "arabic": "Arabic", + "backToDashboard": "Back to Dashboard" }, "sidebar": { "title": "Seera-ASM", "loggedInAs": "Logged in as:", - "version": "Seera-ASM v1.0" + "version": "Seera-ASM v1.0", + "inventory": "Inventory", + "ppmPlanner": "PPM Planner", + "maintenanceCalendar": "Maintenance Calendar", + "activeMap": "Active Map", + "maintenanceTeam": "Maintenance Team", + "procurement": "Procurement", + "sla": "Service Level Agreement (SLA)", + "support": "Support" }, "login": { "title": "Seera-ASM", @@ -93,7 +103,38 @@ "description": "Description", "assignedTo": "Assigned To", "scheduledDate": "Scheduled Date", - "completedDate": "Completed Date" + "completedDate": "Completed Date", + "hospital": "Hospital", + "assetType": "Asset Type", + "siteName": "Site Name", + "assignedSupervisor": "Assigned Supervisor", + "assignedContractor": "Assigned Contractor", + "serialNumberShort": "Serial", + "departmentShort": "Dept", + "manufacturerShort": "Mfr", + "workOrderIdShort": "WO ID", + "assetShort": "Asset", + "typeShort": "Type", + "nameShort": "Name", + "pmId": "PM ID", + "name": "Name" + }, + "filters": { + "assetId": "Asset ID", + "hospital": "Hospital", + "name": "Name", + "serial": "Serial", + "status": "Status", + "location": "Location", + "dept": "Dept", + "modality": "Modality", + "mfr": "Mfr", + "supplier": "Supplier", + "workOrderId": "WO ID", + "asset": "Asset", + "type": "Type", + "priority": "Priority", + "allManufacturers": "All Manufacturers" }, "listPages": { "addNew": "Add New", @@ -126,12 +167,33 @@ "exportComplete": "Export Complete", "close": "Close", "loading": "Loading...", - "refresh": "Refresh" + "refresh": "Refresh", + "typing": "typing...", + "allStatuses": "All Statuses" }, "assets": { "title": "Assets", "addAsset": "Add New Asset", - "assetDetails": "Asset Details" + "assetDetails": "Asset Details", + "assetInformation": "Asset Information", + "newAsset": "New Asset", + "duplicateAsset": "Duplicate Asset", + "fromAsset": "From Asset", + "creatingFromAsset": "Creating Work Order from Asset", + "assetInfoPrefilled": "Asset information prefilled from", + "pleaseSelectWorkOrderType": "Please select a Work Order type and add any additional details", + "loadingAssetDetails": "Loading asset details...", + "pleaseEnterAssetName": "Please enter an Asset Name", + "pleaseSelectCategory": "Please select a Category", + "assetDuplicatedSuccessfully": "Asset duplicated successfully!", + "assetCreatedSuccessfully": "Asset created successfully!", + "assetUpdatedSuccessfully": "Asset updated successfully!", + "sourceAssetNotFound": "Source Asset Not Found", + "assetNotFoundMessage": "The asset you're trying to duplicate could not be found.", + "backToAssetsList": "Back to Assets List", + "newAssetDetails": "New Asset Details", + "noAssetsFound": "No assets found", + "createFirstAsset": "Create your first asset" }, "workOrders": { "title": "Work Orders", @@ -150,8 +212,15 @@ "ppm": { "title": "PPM", "ppmDetails": "PPM Details", - "addPPM": "Add New PPM" + "addPPM": "Add New PPM", + "periodicity": "Periodicity", + "dueDate": "Due Date", + "manageSchedules": "Manage PM Schedules", + "pmId": "PM ID", + "name": "Name", + "manufacturer": "Manufacturer" }, + "exportModal": { "title": "Export", "whatToExport": "What to Export", @@ -170,6 +239,212 @@ "exportingAll": "Exporting all {count} row(s)", "selected": "selected", "rows": "rows" + }, + "items": { + "title": "Items", + "itemDetails": "Item Details", + "newItem": "New Item", + "addItem": "Add New Item", + "itemId": "Item ID", + "itemCode": "Item Code", + "itemName": "Item Name", + "itemGroup": "Item Group", + "stockUOM": "Stock UOM", + "partDescription": "Part Description", + "brand": "Brand", + "valuationRate": "Valuation Rate", + "openingStock": "Opening Stock", + "lastCalibrationDate": "Last Calibration Date", + "nextCalibrationDate": "Next Calibration Date", + "selectItem": "Select Item", + "selectItemGroup": "Select Item Group", + "selectHospital": "Select Hospital", + "viewDetails": "View Details", + "editItem": "Edit Item", + "duplicateItem": "Duplicate Item", + "deleteItem": "Delete Item", + "basicInformation": "Basic Information", + "stockInformation": "Stock Information", + "isStockItem": "Is Stock Item", + "balanceQty": "Balance Qty", + "calibrationInformation": "Calibration Information", + "additionalInformation": "Additional Information", + "refreshBalanceQty": "Refresh Balance Qty", + "warrantyMonths": "Warranty (Months)" + }, + "issues": { + "title": "Issues", + "issueDetails": "Issue Details", + "newIssue": "New Issue", + "addIssue": "Add New Issue", + "issueId": "Issue ID", + "subject": "Subject", + "raisedBy": "Raised By", + "contact": "Contact", + "issueType": "Issue Type", + "openingDate": "Opening Date", + "resolutionDate": "Resolution Date", + "resolvedBy": "Resolved By", + "firstRespondedOn": "First Responded On", + "resolutionDetails": "Resolution Details", + "selectIssue": "Select Issue", + "allPriorities": "All Priorities", + "allCompanies": "All Companies", + "viewDetails": "View Details", + "editIssue": "Edit Issue", + "deleteIssue": "Delete Issue", + "enterSubject": "Enter issue subject", + "selectPriority": "Select priority", + "selectIssueType": "Select issue type", + "describeIssue": "Describe the issue in detail...", + "contactInformation": "Contact Information", + "createNewIssue": "Create a new support issue", + "resolution": "Resolution", + "describeResolution": "Describe how the issue was resolved...", + "selectCompany": "Select company", + "statusInformation": "Status Information", + "currentStatus": "Current Status", + "timeline": "Timeline" + }, + "maintenance": { + "title": "Asset Maintenance", + "maintenanceLogs": "Maintenance Logs", + "maintenanceDetails": "Maintenance Details", + "addMaintenance": "Add New Maintenance", + "maintenanceTeam": "Maintenance Team", + "newMaintenanceTeam": "New Maintenance Team", + "teamId": "Team ID", + "teamName": "Team Name", + "managerEmail": "Manager Email", + "managerName": "Manager Name", + "expertise": "Expertise", + "selectTeam": "Select Team", + "viewDetails": "View Details", + "editTeam": "Edit Team", + "duplicateTeam": "Duplicate Team", + "deleteTeam": "Delete Team", + "selectHospital":"Select Hospital", + "selectExpertise":"Select Expertise", + "selectManager":"Select Manager", + "enterTeamName":"Enter Team Name", + "teamInformation":"Team Information", + "selectUser": "Select User", + "selectRole":"Select Role", + "totalMembers": "Total Members", + "teamSummary" : "Team Summary", + "addFirstMember":"Add First Member", + "manager":"Maintenance Manager" + + }, + "users": { + "title": "Users", + "userDetails": "User Details", + "newUser": "New User", + "addUser": "Add New User" + }, + "events": { + "title": "Events", + "eventDetails": "Event Details", + "newEvent": "New Event", + "addEvent": "Add New Event" + }, + "listPages": { + "addNew": "Add New", + "searchPlaceholder": "Search...", + "noResults": "No results found", + "showing": "Showing", + "of": "of", + "results": "results", + "selectAll": "Select All", + "deselectAll": "Deselect All", + "selected": "selected", + "actions": "Actions", + "view": "View", + "edit": "Edit", + "delete": "Delete", + "duplicate": "Duplicate", + "export": "Export", + "print": "Print", + "filters": "Filters", + "clearFilters": "Clear Filters", + "applyFilters": "Apply Filters", + "columns": "Columns", + "exportSelected": "Export Selected", + "exportAllOnPage": "Export All on Page", + "exportAllWithFilters": "Export All with Filters", + "exportFormat": "Export Format", + "csv": "CSV", + "excel": "Excel", + "exporting": "Exporting...", + "exportComplete": "Export Complete", + "close": "Close", + "loading": "Loading...", + "refresh": "Refresh", + "deselectAllTitle": "Deselect all", + "selectAllTitle": "Select all", + "typeToSearch": "Type to search...", + "enterFilterName": "Enter filter name", + "enterFilterNameExample": "Enter filter name (e.g., 'Open High Priority')", + "allStatuses": "All Statuses", + "noIssuesFound": "No issues found", + "clearFilters": "Clear filters", + "createFirstIssue": "Create your first issue", + "saveFilterPreset": "Save Filter Preset", + "saveFilter": "Save Filter", + "filtering": "Filtering...", + "noMaintenanceTeamsFound": "No maintenance teams found", + "createFirstTeam": "Create your first team", + "all": "All", + "tryAdjustingFilters": "Try adjusting your search or filters", + "getStartedCreateFirst": "Get started by creating your first PPM Planner", + "noMaintenanceLogsFound": "No maintenance logs found", + "createFirstMaintenanceLog": "Create your first maintenance log", + "total": "Total", + "noPPMSchedulesFound": "No PPM schedules found", + "createFirstPPMSchedule": "Create your first PPM schedule" + }, + "filters": { + "assetId": "Asset ID", + "hospital": "Hospital", + "name": "Name", + "serial": "Serial", + "status": "Status", + "location": "Location", + "dept": "Dept", + "modality": "Modality", + "mfr": "Mfr", + "supplier": "Supplier", + "workOrderId": "WO ID", + "asset": "Asset", + "type": "Type", + "priority": "Priority", + "allHospitals": "All Hospitals", + "allModalities": "All Modalities", + "filterByCompany": "Filter by Company", + "allManufacturers": "All Manufacturers" + }, + "users": { + "title": "Users", + "userDetails": "User Details", + "newUser": "New User", + "addUser": "Add New User", + "searchUsers": "Search users...", + "manageUsers": "Manage user accounts and permissions", + "noUsersFound": "No users found", + "tryAdjustingSearch": "Try adjusting your search terms.", + "noUsersAvailable": "No users available.", + "backToDashboard": "Back to Dashboard" + }, + "events": { + "title": "Events", + "eventDetails": "Event Details", + "newEvent": "New Event", + "addEvent": "Add New Event", + "upcomingEvents": "Upcoming Events", + "eventsFromFrappe": "Events from your Frappe backend", + "noEventsFound": "No events found", + "noEventsScheduled": "No events are currently scheduled.", + "refreshEvents": "Refresh Events" } } diff --git a/asm_app/src/pages/ActiveMap.tsx b/asm_app/src/pages/ActiveMap.tsx index bd72c5a..e32b26e 100644 --- a/asm_app/src/pages/ActiveMap.tsx +++ b/asm_app/src/pages/ActiveMap.tsx @@ -1,12 +1,14 @@ /** * Active Map Page * - * Displays hospitals/locations on an interactive map with markers showing: + * Displays hospitals and PHCC locations on an interactive map with markers showing: * - Asset counts * - Work Order counts (Normal/Urgent, by status) * - Maintenance Log counts (Planned/Completed/Overdue) * - * Replicates the Frappe "active-map" page functionality + * Supports both Hospital and PHCC location types with different field mappings: + * - Hospital: company field for assets/work orders, custom_hospital_name for maintenance + * - PHCC: custom_site for assets, site_name for work orders, asset-based for maintenance */ import React, { useState, useEffect, useRef } from 'react'; @@ -37,6 +39,7 @@ interface LocationData { name: string; latitude: number; longitude: number; + location_type: 'hospital' | 'phcc'; assets: number; normal_work_orders: number; urgent_work_orders: number; @@ -47,6 +50,7 @@ interface LocationData { wo_progress: number; wo_review: number; wo_completed: number; + phcc_asset_names?: string[]; } // Component to handle map bounds fitting @@ -73,147 +77,224 @@ const MapBounds: React.FC<{ locations: LocationData[] }> = ({ locations }) => { const ActiveMap: React.FC = () => { const navigate = useNavigate(); const [selectedHospital, setSelectedHospital] = useState(''); + const [selectedPHCC, setSelectedPHCC] = useState(''); const [locations, setLocations] = useState([]); const [loading, setLoading] = useState(true); const markersRef = useRef>({}); + // Fetch location counts based on location type + const fetchLocationCounts = async (location: any, locationType: 'hospital' | 'phcc'): Promise => { + const isPhcc = locationType === 'phcc'; + const assetFilterField = isPhcc ? 'custom_site' : 'company'; + const woFilterField = isPhcc ? 'site_name' : 'company'; + + const counts: Partial = { + assets: 0, + normal_work_orders: 0, + urgent_work_orders: 0, + planned_maintenance: 0, + completed_maintenance: 0, + overdue_maintenance: 0, + wo_open: 0, + wo_progress: 0, + wo_review: 0, + wo_completed: 0, + phcc_asset_names: [] + }; + + try { + // Fetch Asset count + const assetsResponse = await apiService.apiCall( + `/api/resource/Asset?filters=${encodeURIComponent(JSON.stringify({ [assetFilterField]: location.name }))}&fields=["name"]&limit_page_length=0` + ); + const assetList = assetsResponse?.data || []; + counts.assets = assetList.length; + + // Store asset names for PHCC (needed for maintenance log queries) + if (isPhcc) { + counts.phcc_asset_names = assetList.map((a: any) => a.name); + } + + // Fetch Normal Work Orders + const normalWOResponse = await apiService.apiCall( + `/api/resource/Work_Order?filters=${encodeURIComponent(JSON.stringify({ + [woFilterField]: location.name, + custom_priority_: 'Normal', + repair_status: ['in', ['Open', 'Work In Progress']] + }))}&fields=["name"]` + ); + counts.normal_work_orders = normalWOResponse?.data?.length || 0; + + // Fetch Urgent Work Orders + const urgentWOResponse = await apiService.apiCall( + `/api/resource/Work_Order?filters=${encodeURIComponent(JSON.stringify({ + [woFilterField]: location.name, + custom_priority_: 'Urgent', + repair_status: ['in', ['Open', 'Work In Progress']] + }))}&fields=["name"]` + ); + counts.urgent_work_orders = urgentWOResponse?.data?.length || 0; + + // Fetch WO Status counts + const [woOpen, woProgress, woReview, woCompleted] = await Promise.all([ + // Open + apiService.apiCall( + `/api/resource/Work_Order?filters=${encodeURIComponent(JSON.stringify({ + [woFilterField]: location.name, + repair_status: 'Open' + }))}&fields=["name"]` + ), + // Work In Progress + apiService.apiCall( + `/api/resource/Work_Order?filters=${encodeURIComponent(JSON.stringify({ + [woFilterField]: location.name, + repair_status: 'Work In Progress' + }))}&fields=["name"]` + ), + // Pending Review + apiService.apiCall( + `/api/resource/Work_Order?filters=${encodeURIComponent(JSON.stringify({ + [woFilterField]: location.name, + repair_status: 'Pending Review' + }))}&fields=["name"]` + ), + // Completed + apiService.apiCall( + `/api/resource/Work_Order?filters=${encodeURIComponent(JSON.stringify({ + [woFilterField]: location.name, + repair_status: 'Completed' + }))}&fields=["name"]` + ) + ]); + + counts.wo_open = woOpen?.data?.length || 0; + counts.wo_progress = woProgress?.data?.length || 0; + counts.wo_review = woReview?.data?.length || 0; + counts.wo_completed = woCompleted?.data?.length || 0; + + // Fetch Maintenance counts - different logic for PHCC vs Hospital + if (isPhcc && counts.phcc_asset_names && counts.phcc_asset_names.length > 0) { + // For PHCC, filter by asset_name + const [plannedPM, completedPM, overduePM] = await Promise.all([ + apiService.apiCall( + `/api/resource/Asset Maintenance Log?filters=${encodeURIComponent(JSON.stringify({ + asset_name: ['in', counts.phcc_asset_names], + maintenance_status: 'Planned' + }))}&fields=["name"]` + ), + apiService.apiCall( + `/api/resource/Asset Maintenance Log?filters=${encodeURIComponent(JSON.stringify({ + asset_name: ['in', counts.phcc_asset_names], + maintenance_status: 'Completed' + }))}&fields=["name"]` + ), + apiService.apiCall( + `/api/resource/Asset Maintenance Log?filters=${encodeURIComponent(JSON.stringify({ + asset_name: ['in', counts.phcc_asset_names], + maintenance_status: 'Overdue' + }))}&fields=["name"]` + ) + ]); + counts.planned_maintenance = plannedPM?.data?.length || 0; + counts.completed_maintenance = completedPM?.data?.length || 0; + counts.overdue_maintenance = overduePM?.data?.length || 0; + } else if (!isPhcc) { + // For Hospital, filter by custom_hospital_name + const [plannedPM, completedPM, overduePM] = await Promise.all([ + apiService.apiCall( + `/api/resource/Asset Maintenance Log?filters=${encodeURIComponent(JSON.stringify({ + custom_hospital_name: location.name, + maintenance_status: 'Planned' + }))}&fields=["name"]` + ), + apiService.apiCall( + `/api/resource/Asset Maintenance Log?filters=${encodeURIComponent(JSON.stringify({ + custom_hospital_name: location.name, + maintenance_status: 'Completed' + }))}&fields=["name"]` + ), + apiService.apiCall( + `/api/resource/Asset Maintenance Log?filters=${encodeURIComponent(JSON.stringify({ + custom_hospital_name: location.name, + maintenance_status: 'Overdue' + }))}&fields=["name"]` + ) + ]); + counts.planned_maintenance = plannedPM?.data?.length || 0; + counts.completed_maintenance = completedPM?.data?.length || 0; + counts.overdue_maintenance = overduePM?.data?.length || 0; + } + } catch (err) { + console.error(`Error fetching counts for ${location.name}:`, err); + } + + return { + name: location.name, + latitude: parseFloat(location.latitude), + longitude: parseFloat(location.longitude), + location_type: locationType, + ...counts + } as LocationData; + }; + // Fetch locations and their counts const fetchAndRenderData = async () => { setLoading(true); try { - // Build filters - const filters: Record = { - latitude: ['!=', ''], - longitude: ['!=', ''] - }; + let allLocations: LocationData[] = []; + const fetchPromises: Promise[] = []; - if (selectedHospital) { - filters.name = selectedHospital; - } - - // Fetch locations - const locationsResponse = await apiService.apiCall( - `/api/resource/Location?filters=${encodeURIComponent(JSON.stringify(filters))}&fields=["name","latitude","longitude"]` - ); - - const locationList = locationsResponse?.data || []; - - // For each location, fetch counts - const locationPromises = locationList.map(async (location: any) => { - const counts: Partial = { - assets: 0, - normal_work_orders: 0, - urgent_work_orders: 0, - planned_maintenance: 0, - completed_maintenance: 0, - overdue_maintenance: 0, - wo_open: 0, - wo_progress: 0, - wo_review: 0, - wo_completed: 0 + // Fetch Hospital locations (if no PHCC is specifically selected, or if hospital is selected) + if (!selectedPHCC || selectedHospital) { + const hospitalFilters: Record = { + latitude: ['!=', ''], + longitude: ['!=', ''], + custom_is_hospital: 1 }; - try { - // Fetch Asset count - use fields=["name"] to minimize data transfer - const assetsResponse = await apiService.apiCall( - `/api/resource/Asset?filters=${encodeURIComponent(JSON.stringify({ company: location.name }))}&fields=["name"]` - ); - counts.assets = assetsResponse?.data?.length || 0; - - // Fetch Normal Work Orders - const normalWOResponse = await apiService.apiCall( - `/api/resource/Work_Order?filters=${encodeURIComponent(JSON.stringify({ - company: location.name, - custom_priority_: 'Normal', - repair_status: ['in', ['Open', 'Work In Progress']] - }))}&fields=["name"]` - ); - counts.normal_work_orders = normalWOResponse?.data?.length || 0; - - // Fetch Urgent Work Orders - const urgentWOResponse = await apiService.apiCall( - `/api/resource/Work_Order?filters=${encodeURIComponent(JSON.stringify({ - company: location.name, - custom_priority_: 'Urgent', - repair_status: ['in', ['Open', 'Work In Progress']] - }))}&fields=["name"]` - ); - counts.urgent_work_orders = urgentWOResponse?.data?.length || 0; - - // Fetch WO Status counts - const [woOpen, woProgress, woReview, woCompleted, plannedPM, completedPM, overduePM] = await Promise.all([ - // Open - apiService.apiCall( - `/api/resource/Work_Order?filters=${encodeURIComponent(JSON.stringify({ - company: location.name, - repair_status: 'Open' - }))}&fields=["name"]` - ), - // Work In Progress - apiService.apiCall( - `/api/resource/Work_Order?filters=${encodeURIComponent(JSON.stringify({ - company: location.name, - repair_status: 'Work In Progress' - }))}&fields=["name"]` - ), - // Pending Review - apiService.apiCall( - `/api/resource/Work_Order?filters=${encodeURIComponent(JSON.stringify({ - company: location.name, - repair_status: 'Pending Review' - }))}&fields=["name"]` - ), - // Completed - apiService.apiCall( - `/api/resource/Work_Order?filters=${encodeURIComponent(JSON.stringify({ - company: location.name, - repair_status: 'Completed' - }))}&fields=["name"]` - ), - // Planned Maintenance - apiService.apiCall( - `/api/resource/Asset Maintenance Log?filters=${encodeURIComponent(JSON.stringify({ - custom_hospital_name: location.name, - maintenance_status: 'Planned' - }))}&fields=["name"]` - ), - // Completed Maintenance - apiService.apiCall( - `/api/resource/Asset Maintenance Log?filters=${encodeURIComponent(JSON.stringify({ - custom_hospital_name: location.name, - maintenance_status: 'Completed' - }))}&fields=["name"]` - ), - // Overdue Maintenance - apiService.apiCall( - `/api/resource/Asset Maintenance Log?filters=${encodeURIComponent(JSON.stringify({ - custom_hospital_name: location.name, - maintenance_status: 'Overdue' - }))}&fields=["name"]` - ) - ]); - - counts.wo_open = woOpen?.data?.length || 0; - counts.wo_progress = woProgress?.data?.length || 0; - counts.wo_review = woReview?.data?.length || 0; - counts.wo_completed = woCompleted?.data?.length || 0; - counts.planned_maintenance = plannedPM?.data?.length || 0; - counts.completed_maintenance = completedPM?.data?.length || 0; - counts.overdue_maintenance = overduePM?.data?.length || 0; - } catch (err) { - console.error(`Error fetching counts for ${location.name}:`, err); + if (selectedHospital) { + hospitalFilters.name = selectedHospital; } - return { - name: location.name, - latitude: parseFloat(location.latitude), - longitude: parseFloat(location.longitude), - ...counts - } as LocationData; - }); + fetchPromises.push( + (async () => { + const locationsResponse = await apiService.apiCall( + `/api/resource/Location?filters=${encodeURIComponent(JSON.stringify(hospitalFilters))}&fields=["name","latitude","longitude"]&limit_page_length=0` + ); + const locationList = locationsResponse?.data || []; + const locationPromises = locationList.map((loc: any) => fetchLocationCounts(loc, 'hospital')); + return Promise.all(locationPromises); + })() + ); + } - const results = await Promise.all(locationPromises); - setLocations(results.filter(l => !isNaN(l.latitude) && !isNaN(l.longitude))); + // Fetch PHCC locations (if no hospital is specifically selected, or if PHCC is selected) + if (!selectedHospital || selectedPHCC) { + const phccFilters: Record = { + latitude: ['!=', ''], + longitude: ['!=', ''], + custom_is_phcc: 1 + }; + + if (selectedPHCC) { + phccFilters.name = selectedPHCC; + } + + fetchPromises.push( + (async () => { + const locationsResponse = await apiService.apiCall( + `/api/resource/Location?filters=${encodeURIComponent(JSON.stringify(phccFilters))}&fields=["name","latitude","longitude"]&limit_page_length=0` + ); + const locationList = locationsResponse?.data || []; + const locationPromises = locationList.map((loc: any) => fetchLocationCounts(loc, 'phcc')); + return Promise.all(locationPromises); + })() + ); + } + + const results = await Promise.all(fetchPromises); + allLocations = results.flat().filter(l => !isNaN(l.latitude) && !isNaN(l.longitude)); + setLocations(allLocations); } catch (error) { console.error('Error fetching map data:', error); } finally { @@ -223,38 +304,57 @@ const ActiveMap: React.FC = () => { useEffect(() => { fetchAndRenderData(); - }, [selectedHospital]); + }, [selectedHospital, selectedPHCC]); // Navigate to list view with filters - const navigateToWorkOrders = (hospital: string, priority?: string, status?: string) => { + const navigateToWorkOrders = (location: LocationData, priority?: string, status?: string) => { const params = new URLSearchParams(); - if (hospital) params.set('company', hospital); + const filterField = location.location_type === 'phcc' ? 'site_name' : 'company'; + params.set(filterField, location.name); if (priority) params.set('priority', priority); if (status) params.set('status', status); navigate(`/work-orders?${params.toString()}`); }; - const navigateToAssets = (hospital: string) => { + const navigateToAssets = (location: LocationData) => { const params = new URLSearchParams(); - if (hospital) params.set('company', hospital); + const filterField = location.location_type === 'phcc' ? 'custom_site' : 'company'; + params.set(filterField, location.name); navigate(`/assets?${params.toString()}`); }; - const navigateToMaintenanceCalendar = (hospital: string, status?: string) => { + const navigateToMaintenanceCalendar = (location: LocationData, status?: string) => { const params = new URLSearchParams(); - if (hospital) params.set('hospital', hospital); + if (location.location_type === 'phcc') { + // For PHCC, we need to pass asset names or use a different approach + params.set('phcc', location.name); + } else { + params.set('hospital', location.name); + } if (status) params.set('status', status); navigate(`/maintenance-calendar?${params.toString()}`); }; // Create popup content with modern UI matching the application const createPopupContent = (location: LocationData) => { + const isPhcc = location.location_type === 'phcc'; + const typeBadge = isPhcc ? ( + + PHCC + + ) : ( + + Hospital + + ); + return (
- {/* Hospital Name Header */} + {/* Location Name Header */}
-

+

{location.name} + {typeBadge}

Total Assets: {location.assets} @@ -268,13 +368,13 @@ const ActiveMap: React.FC = () => {

@@ -225,7 +225,7 @@ const AssetMaintenanceList: React.FC = () => { setSearchTerm(e.target.value)} className="flex-1 outline-none text-gray-700 dark:text-gray-200 bg-transparent" @@ -242,7 +242,7 @@ const AssetMaintenanceList: React.FC = () => { }} className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500" > - + @@ -258,22 +258,22 @@ const AssetMaintenanceList: React.FC = () => { - Log ID + {t('maintenance.logId')} - Asset + {t('commonFields.assetShort')} - Type + {t('commonFields.typeShort')} - Due Date + {t('ppm.dueDate')} - Status + {t('commonFields.status')} - Actions + {t('listPages.actions')} @@ -283,12 +283,12 @@ const AssetMaintenanceList: React.FC = () => {
-

No maintenance logs found

+

{t('listPages.noMaintenanceLogsFound')}

diff --git a/asm_app/src/pages/IssueDetail.tsx b/asm_app/src/pages/IssueDetail.tsx new file mode 100644 index 0000000..ba41621 --- /dev/null +++ b/asm_app/src/pages/IssueDetail.tsx @@ -0,0 +1,686 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { useIssueDetails, useIssueMutations } from '../hooks/useIssue'; +import { + FaArrowLeft, + FaSave, + FaEdit, + FaTrash, + FaCheckCircle, + FaTimesCircle, + FaExclamationTriangle, + FaClock, + FaUser, + FaBuilding, + FaEnvelope, + FaCalendarAlt, + FaTag, + FaComment +} from 'react-icons/fa'; +import { toast, ToastContainer, Bounce } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; +import LinkField from '../components/LinkField'; +import type { CreateIssueData } from '../services/issueService'; + +// Helper to get today's date in YYYY-MM-DD format +const getTodayDate = (): string => { + return new Date().toISOString().split('T')[0]; +}; + +// Helper to get current time in HH:MM:SS format +const getCurrentTime = (): string => { + return new Date().toTimeString().split(' ')[0]; +}; + +// Status badge styles +const getStatusStyle = (status: string) => { + switch (status?.toLowerCase()) { + case 'open': + return { bg: 'bg-blue-100 dark:bg-blue-900/30', text: 'text-blue-800 dark:text-blue-300', border: 'border-blue-200 dark:border-blue-800' }; + case 'replied': + return { bg: 'bg-purple-100 dark:bg-purple-900/30', text: 'text-purple-800 dark:text-purple-300', border: 'border-purple-200 dark:border-purple-800' }; + case 'on hold': + return { bg: 'bg-yellow-100 dark:bg-yellow-900/30', text: 'text-yellow-800 dark:text-yellow-300', border: 'border-yellow-200 dark:border-yellow-800' }; + case 'resolved': + return { bg: 'bg-green-100 dark:bg-green-900/30', text: 'text-green-800 dark:text-green-300', border: 'border-green-200 dark:border-green-800' }; + case 'closed': + return { bg: 'bg-gray-100 dark:bg-gray-700', text: 'text-gray-800 dark:text-gray-300', border: 'border-gray-200 dark:border-gray-600' }; + default: + return { bg: 'bg-gray-100 dark:bg-gray-700', text: 'text-gray-800 dark:text-gray-300', border: 'border-gray-200 dark:border-gray-600' }; + } +}; + +const IssueDetail: React.FC = () => { + const { t } = useTranslation(); + const { issueName } = useParams<{ issueName: string }>(); + const navigate = useNavigate(); + + const isNewIssue = issueName === 'new'; + + // Form data state + const [formData, setFormData] = useState({ + subject: '', + raised_by: '', + status: 'Open', + priority: '', + issue_type: '', + description: '', + contact: '', + company: '', + customer: '', + project: '', + resolution_details: '', + opening_date: isNewIssue ? getTodayDate() : '', + opening_time: isNewIssue ? getCurrentTime() : '', + first_responded_on: '', + resolution_date: '', + resolution_by: '', + }); + + const { issue, loading, error, refetch } = useIssueDetails(isNewIssue ? null : issueName || null); + const { createIssue, updateIssue, deleteIssue, loading: saving } = useIssueMutations(); + + const [isEditing, setIsEditing] = useState(isNewIssue); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + + // Load issue data when fetched + useEffect(() => { + if (issue && !isNewIssue) { + setFormData({ + subject: issue.subject || '', + raised_by: issue.raised_by || '', + status: issue.status || 'Open', + priority: issue.priority || '', + issue_type: issue.issue_type || '', + description: issue.description || '', + contact: issue.contact || '', + company: issue.company || '', + customer: issue.customer || '', + project: issue.project || '', + resolution_details: issue.resolution_details || '', + opening_date: issue.opening_date || '', + opening_time: issue.opening_time || '', + first_responded_on: issue.first_responded_on ? issue.first_responded_on.split(' ')[0] : '', + resolution_date: issue.resolution_date ? issue.resolution_date.split(' ')[0] : '', + resolution_by: issue.resolution_by || '', + }); + setIsEditing(false); + } + }, [issue, isNewIssue]); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + }; + + const handleSave = async () => { + if (!formData.subject) { + toast.error('Please enter a subject', { + position: "top-right", + autoClose: 4000, + icon: + }); + return; + } + + try { + if (isNewIssue) { + const newIssue = await createIssue(formData); + toast.success('Issue created successfully!', { + position: "top-right", + autoClose: 3000, + icon: + }); + navigate(`/support/${newIssue.name}`); + } else { + await updateIssue(issueName!, formData); + toast.success('Issue updated successfully!', { + position: "top-right", + autoClose: 3000, + icon: + }); + setIsEditing(false); + refetch(); + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error'; + toast.error(`Failed to save: ${errorMessage}`, { + position: "top-right", + autoClose: 6000, + icon: + }); + } + }; + + const handleDelete = async () => { + try { + await deleteIssue(issueName!); + toast.success('Issue deleted successfully!', { + position: "top-right", + autoClose: 3000, + icon: + }); + navigate('/support'); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error'; + toast.error(`Failed to delete: ${errorMessage}`, { + position: "top-right", + autoClose: 6000, + icon: + }); + } + }; + + const isFieldDisabled = useCallback((fieldname: string): boolean => { + if (!isEditing) return true; + // Some fields are always read-only + if (['opening_date', 'opening_time'].includes(fieldname) && !isNewIssue) { + return true; + } + return false; + }, [isEditing, isNewIssue]); + + // Format datetime + const formatDateTime = (dateStr: string) => { + if (!dateStr) return '-'; + return new Date(dateStr).toLocaleString(); + }; + + if (loading) { + return ( +
+
+
+

Loading issue details...

+
+
+ ); + } + + if (error && !isNewIssue) { + return ( +
+
+

Error Loading Issue

+

{error}

+ +
+
+ ); + } + + const currentStatus = issue?.status || formData.status || 'Open'; + const statusStyle = getStatusStyle(currentStatus); + + return ( +
+ {/* Toast Container */} + + + {/* Header */} +
+
+ +
+

+ {isNewIssue ? t('issues.newIssue') : issue?.name || t('issues.issueDetails')} + {!isNewIssue && ( + + {currentStatus} + + )} +

+

+ {isNewIssue ? t('issues.createNewIssue') : formData.subject} +

+
+
+ +
+ {!isNewIssue && !isEditing && ( + <> + + + + )} + {isEditing && ( + <> + + + + )} +
+
+ + {/* Delete Confirmation Modal */} + {showDeleteConfirm && ( +
+
+
+ +
+

Delete Issue

+

+ Are you sure you want to delete this issue? This action cannot be undone. +

+
+
+
+ + +
+
+
+ )} + + {/* Form */} +
+ {/* Main Content - Left Column */} +
+ {/* Issue Details */} +
+

+ + {t('issues.issueDetails')} +

+ +
+
+ + +
+ +
+
+ + +
+ +
+ setFormData({ ...formData, priority: val })} + disabled={isFieldDisabled('priority')} + placeholder={t('issues.selectPriority')} + /> +
+
+ +
+ setFormData({ ...formData, issue_type: val })} + disabled={isFieldDisabled('issue_type')} + placeholder={t('issues.selectIssueType')} + /> +
+ +
+ +