import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import ppmPlannerService, { type BulkScheduleData } from '../services/ppmPlannerService'; import { FaFilter, FaCalendar, FaCheckCircle, FaSearch, FaArrowLeft, FaSpinner } from 'react-icons/fa'; import LinkField from '../components/LinkField'; // Updated Asset interface to match backend API response interface Asset { name: string; asset_name: string; custom_modality?: string; company?: string; custom_manufacturer?: string; custom_device_status?: string; custom_model?: string; } // Updated filters to match backend API parameters interface AssetFilters { company?: string; custom_modality?: string; custom_manufacturer?: string; custom_device_status?: string; custom_model?: string; department?: string; } const PPMPlanner: React.FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); // Updated filters state to match backend API const [filters, setFilters] = useState({}); const [selectedAssets, setSelectedAssets] = useState([]); const [scheduleData, setScheduleData] = useState({ start_date: '', end_date: '', maintenance_team: '', assign_to: '', pm_for: '', maintenance_manager: '', periodicity: 'Monthly', maintenance_type: 'Preventive', no_of_pms: '', department: '' }); const [loading, setLoading] = useState(false); const [fetchingAssets, setFetchingAssets] = useState(false); const [assets, setAssets] = useState([]); const [filterOptions, setFilterOptions] = useState({ modalities: [] as string[], assetTypes: [] as string[], departments: [] as string[], locations: [] as string[], manufacturers: [] as string[], models: [] as string[], company: [] as string[] }); const [maintenanceTeams, setMaintenanceTeams] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const [successResult, setSuccessResult] = useState<{ show: boolean; document?: string; count: number; type: 'pm_schedule' | 'maintenance_logs'; } | null>(null); useEffect(() => { loadFilterOptions(); loadMaintenanceTeams(); }, []); // Helper function to calculate end_date based on start_date, periodicity, and no_of_pms const calculateEndDate = (startDate: string, periodicity: string, noOfPms: string): string | null => { if (!startDate || !periodicity || !noOfPms) { return null; } const noOfPmsNum = parseInt(noOfPms, 10); if (isNaN(noOfPmsNum) || noOfPmsNum < 1) { return null; } // Start date is PM #1, so we need to add (no_of_pms - 1) periods const occurrences = noOfPmsNum - 1; if (occurrences < 0) { return null; } const start = new Date(startDate); const end = new Date(start); switch (periodicity) { case 'Daily': end.setDate(end.getDate() + occurrences); break; case 'Weekly': end.setDate(end.getDate() + (occurrences * 7)); break; case 'Monthly': end.setMonth(end.getMonth() + occurrences); break; case 'Quarterly': end.setMonth(end.getMonth() + (occurrences * 3)); break; case 'Half-yearly': end.setMonth(end.getMonth() + (occurrences * 6)); break; case 'Yearly': end.setFullYear(end.getFullYear() + occurrences); break; case '2 Yearly': end.setFullYear(end.getFullYear() + (occurrences * 2)); break; case '3 Yearly': end.setFullYear(end.getFullYear() + (occurrences * 3)); break; default: return null; } // Format as YYYY-MM-DD const year = end.getFullYear(); const month = String(end.getMonth() + 1).padStart(2, '0'); const day = String(end.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; // Auto-populate maintenance_manager and assign_to when maintenance_team is selected useEffect(() => { const fetchTeamDetails = async () => { if (scheduleData.maintenance_team) { const teamDetails = await ppmPlannerService.getMaintenanceTeamDetails(scheduleData.maintenance_team); if (teamDetails) { setScheduleData(prev => ({ ...prev, maintenance_manager: teamDetails.maintenance_manager || '', assign_to: (teamDetails.team_members && teamDetails.team_members.length === 1) ? teamDetails.team_members[0] : prev.assign_to })); } } else { setScheduleData(prev => ({ ...prev, maintenance_manager: '', assign_to: '' })); } }; fetchTeamDetails(); }, [scheduleData.maintenance_team]); // Auto-calculate end_date when start_date, periodicity, or no_of_pms changes useEffect(() => { if (scheduleData.start_date && scheduleData.periodicity && scheduleData.no_of_pms) { const calculatedEndDate = calculateEndDate( scheduleData.start_date, scheduleData.periodicity, scheduleData.no_of_pms ); if (calculatedEndDate) { setScheduleData(prev => ({ ...prev, end_date: calculatedEndDate })); } } }, [scheduleData.start_date, scheduleData.periodicity, scheduleData.no_of_pms]); const loadFilterOptions = async () => { const options = await ppmPlannerService.getFilterOptions(); setFilterOptions(options); }; const loadMaintenanceTeams = async () => { const teams = await ppmPlannerService.getMaintenanceTeams(); setMaintenanceTeams(teams); }; // Updated fetchAssets to call the Frappe Server Script API const fetchAssets = async () => { setFetchingAssets(true); try { // Build query parameters matching backend API const params = new URLSearchParams(); if (filters.company) { params.append('company', filters.company); } if (filters.custom_modality) { params.append('custom_modality', filters.custom_modality); } if (filters.custom_manufacturer) { params.append('custom_manufacturer', filters.custom_manufacturer); } if (filters.custom_device_status) { params.append('custom_device_status', filters.custom_device_status); } if (filters.custom_model) { params.append('custom_model', filters.custom_model); } if (filters.department) { params.append('department', filters.department); } // Call the Frappe Server Script API const response = await fetch(`/api/method/get_assets?${params.toString()}`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, credentials: 'include', // Important for Frappe session authentication }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); const filteredAssets: Asset[] = data.message || []; setAssets(filteredAssets); setSelectedAssets([]); } catch (error) { console.error('Error fetching assets:', error); alert('Failed to fetch assets: ' + (error instanceof Error ? error.message : 'Unknown error')); } finally { setFetchingAssets(false); } }; const handleFilterChange = (key: keyof AssetFilters, value: string) => { setFilters(prev => ({ ...prev, [key]: value || undefined })); }; const toggleAssetSelection = (assetName: string) => { setSelectedAssets(prev => prev.includes(assetName) ? prev.filter(name => name !== assetName) : [...prev, assetName] ); }; const handleSelectAll = () => { const filteredAssets = getFilteredAssets(); if (selectedAssets.length === filteredAssets.length && filteredAssets.length > 0) { setSelectedAssets([]); } else { setSelectedAssets(filteredAssets.map(a => a.name)); } }; const getFilteredAssets = () => { if (!searchTerm) return assets; const term = searchTerm.toLowerCase(); return assets.filter(asset => asset.asset_name?.toLowerCase().includes(term) || asset.custom_modality?.toLowerCase().includes(term) || asset.company?.toLowerCase().includes(term) || asset.custom_manufacturer?.toLowerCase().includes(term) || asset.custom_model?.toLowerCase().includes(term) || asset.custom_device_status?.toLowerCase().includes(term) ); }; const handleGenerateSchedule = async () => { if (selectedAssets.length === 0) { alert('Please select at least one asset'); return; } if (!filters.company) { alert('Please select a Hospital/Company in the filters first'); return; } if (!scheduleData.pm_for) { alert('Please enter a PM Name'); return; } if (!scheduleData.start_date || !scheduleData.end_date) { alert('Please select start and end dates'); return; } if (new Date(scheduleData.start_date) > new Date(scheduleData.end_date)) { alert('Start date must be before end date'); return; } // Require assign_to to avoid validation error when Asset Maintenance is auto-created if (!scheduleData.assign_to) { alert('Please assign the task to a team member. This is required for Asset Maintenance creation.'); return; } const confirmed = window.confirm( `Are you sure you want to create maintenance schedules for ${selectedAssets.length} asset(s)?` ); if (!confirmed) return; setLoading(true); try { // Get full asset details for selected assets (including manufacturer and model) const selectedAssetDetails = assets .filter(asset => selectedAssets.includes(asset.name)) .map(asset => ({ name: asset.name, custom_manufacturer: asset.custom_manufacturer, custom_model: asset.custom_model, })); const bulkData: BulkScheduleData = { assets: selectedAssetDetails, // Pass full asset details start_date: scheduleData.start_date, end_date: scheduleData.end_date, maintenance_team: scheduleData.maintenance_team || undefined, assign_to: scheduleData.assign_to || undefined, maintenance_manager: scheduleData.maintenance_manager || undefined, periodicity: scheduleData.periodicity, maintenance_type: scheduleData.maintenance_type, no_of_pms: scheduleData.no_of_pms || undefined, pm_for: scheduleData.pm_for || undefined, hospital: filters.company!, // Form-level fields from filters modality: filters.custom_modality, manufacturer: filters.custom_manufacturer, model: filters.custom_model, department: scheduleData.department || filters.department || undefined, }; // Debug logs console.log('=== DEBUG: Selected Asset Details ===', selectedAssetDetails); console.log('=== DEBUG: bulkData ===', bulkData); const result = await ppmPlannerService.createBulkMaintenanceSchedules(bulkData); setSuccessResult({ show: true, document: result.document, count: result.created || selectedAssets.length, type: 'pm_schedule' }); setSelectedAssets([]); setScheduleData({ start_date: '', end_date: '', maintenance_team: '', assign_to: '', pm_for: '', maintenance_manager: '', periodicity: 'Monthly', maintenance_type: 'Preventive', no_of_pms: '', department: '' }); } catch (error) { console.error('Error creating schedules:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; alert(`Failed to create maintenance schedules:\n\n${errorMessage}`); } finally { setLoading(false); } }; const filteredAssets = getFilteredAssets(); const hasActiveFilters = Object.values(filters).some(v => v); return (
{/* Header */}

PPM Planner - Bulk Schedule Generator

{/* Filter Section - Updated to match backend API */}

Filter Assets

{/* Company/Hospital - Required */}
handleFilterChange('company', val)} placeholder="Select a hospital/company" />
{/* Modality */}
handleFilterChange('custom_modality', val)} placeholder="Leave empty for all modalities" />
{/* Manufacturer */}
handleFilterChange('custom_manufacturer', val)} placeholder="Leave empty for all manufacturers" />
{/* Device Status */}
{/* Model */}
{/* Department */}
handleFilterChange('department', val)} placeholder="Select department (optional)" />
{hasActiveFilters && ( )}
{/* Asset Selection Section - Updated table columns */} {assets.length > 0 && (

Select Assets ({selectedAssets.length} of {assets.length} selected)

setSearchTerm(e.target.value)} className="pl-10 pr-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" />
{filteredAssets.length === 0 ? ( ) : ( filteredAssets.map(asset => ( )) )}
0} onChange={handleSelectAll} className="rounded" /> Asset Name Modality Manufacturer Model Status
No assets match your search criteria
toggleAssetSelection(asset.name)} className="rounded" /> {asset.asset_name} {asset.custom_modality || '-'} {asset.custom_manufacturer || '-'} {asset.custom_model || '-'} {asset.custom_device_status || '-'}
)} {/* Schedule Configuration */} {selectedAssets.length > 0 && (

Schedule Configuration

setScheduleData(prev => ({ ...prev, pm_for: e.target.value }))} className="w-full px-3 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" placeholder="Enter PM Name" required />
setScheduleData(prev => ({ ...prev, start_date: e.target.value }))} className="w-full px-3 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" required />
setScheduleData(prev => ({ ...prev, no_of_pms: e.target.value }))} min="1" className="w-full px-3 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" placeholder="Enter number of PMs" />

End date will be auto-calculated

setScheduleData(prev => ({ ...prev, end_date: e.target.value }))} min={scheduleData.start_date} className="w-full px-3 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" required />
setScheduleData(prev => ({ ...prev, maintenance_team: val }))} /> {scheduleData.maintenance_manager && (
Maintenance Manager: {scheduleData.maintenance_manager}
)}
setScheduleData(prev => ({ ...prev, assign_to: val }))} placeholder={scheduleData.maintenance_team ? "Select user (auto-selected if only one team member)" : "Select user to assign tasks"} />

Required for Asset Maintenance creation

{scheduleData.assign_to && (
Assigned To: {scheduleData.assign_to}
)}
setScheduleData(prev => ({ ...prev, department: val }))} placeholder="Select department (optional)" />
)} {/* Empty State */} {assets.length === 0 && !fetchingAssets && !successResult?.show && (

No Assets Loaded

Use the filters above to search for assets, then click "Fetch Assets" to load them.

Note: Only submitted assets without existing maintenance schedules will be shown.

)} {/* Success Result Modal */} {successResult?.show && (

Schedules Created Successfully!

{successResult.count} maintenance schedule{successResult.count !== 1 ? 's' : ''} have been created.

What was created:

Document: {successResult.document}

A PM Schedule Generator document has been created with {successResult.count} asset(s). Frappe will automatically create Asset Maintenance Logs when the document is submitted. You can view and manage it in the PPM Planner section.

{successResult.document && ( )}
)}
); }; export default PPMPlanner;