Iman_AMS/asm_app/src/pages/PPMPlanner.tsx
2026-01-13 14:53:14 +00:00

825 lines
33 KiB
TypeScript

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<AssetFilters>({});
const [selectedAssets, setSelectedAssets] = useState<string[]>([]);
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<Asset[]>([]);
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<any[]>([]);
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 (
<div className="p-6 bg-gray-50 dark:bg-gray-900 min-h-screen">
{/* Header */}
<div className="mb-6 flex items-center gap-4">
<button
onClick={() => navigate('/ppm-planner')}
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 flex items-center gap-2"
>
<FaArrowLeft />
<span>Back to PPM Planner</span>
</button>
<h1 className="text-3xl font-bold text-gray-800 dark:text-white">
PPM Planner - Bulk Schedule Generator
</h1>
</div>
{/* Filter Section - Updated to match backend API */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2 text-gray-800 dark:text-white">
<FaFilter /> Filter Assets
</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{/* Company/Hospital - Required */}
<div>
<LinkField
label="Hospital/Company *"
doctype="Company"
value={filters.company || ''}
onChange={(val) => handleFilterChange('company', val)}
placeholder="Select a hospital/company"
/>
</div>
{/* Modality */}
<div>
<LinkField
label="Modality"
doctype="Modality"
value={filters.custom_modality || ''}
onChange={(val) => handleFilterChange('custom_modality', val)}
placeholder="Leave empty for all modalities"
/>
</div>
{/* Manufacturer */}
<div>
<LinkField
label="Manufacturer"
doctype="Manufacturer"
value={filters.custom_manufacturer || ''}
onChange={(val) => handleFilterChange('custom_manufacturer', val)}
placeholder="Leave empty for all manufacturers"
/>
</div>
{/* Device Status */}
<div>
<label className="block text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">
Device Status
</label>
<select
value={filters.custom_device_status || ''}
onChange={(e) => handleFilterChange('custom_device_status', 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"
>
<option value="">All Statuses</option>
<option value="Active">Active</option>
<option value="Inactive">Inactive</option>
<option value="Under Maintenance">Under Maintenance</option>
<option value="Decommissioned">Decommissioned</option>
</select>
</div>
{/* Model */}
<div>
<label className="block text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">
Model
</label>
<select
value={filters.custom_model || ''}
onChange={(e) => handleFilterChange('custom_model', 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"
>
<option value="">Select Model (optional)</option>
{filterOptions.models.map(mod => (
<option key={mod} value={mod}>{mod}</option>
))}
</select>
</div>
{/* Department */}
<div>
<LinkField
label="Department"
doctype="Department"
value={filters.department || ''}
onChange={(val) => handleFilterChange('department', val)}
placeholder="Select department (optional)"
/>
</div>
</div>
<div className="mt-4 flex gap-3">
<button
onClick={fetchAssets}
disabled={fetchingAssets}
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
{fetchingAssets ? (
<>
<FaSpinner className="animate-spin" />
Loading...
</>
) : (
<>
<FaSearch />
Fetch Assets
</>
)}
</button>
{hasActiveFilters && (
<button
onClick={() => {
setFilters({});
setAssets([]);
setSelectedAssets([]);
}}
className="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg"
>
Clear Filters
</button>
)}
</div>
</div>
{/* Asset Selection Section - Updated table columns */}
{assets.length > 0 && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold text-gray-800 dark:text-white">
Select Assets ({selectedAssets.length} of {assets.length} selected)
</h2>
<div className="flex gap-3 items-center">
<div className="relative">
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder="Search assets..."
value={searchTerm}
onChange={(e) => 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"
/>
</div>
<button
onClick={handleSelectAll}
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 px-4 py-2 border border-blue-600 dark:border-blue-400 rounded-lg"
>
{selectedAssets.length === filteredAssets.length && filteredAssets.length > 0 ? 'Deselect All' : 'Select All'}
</button>
</div>
</div>
<div className="max-h-96 overflow-y-auto border border-gray-200 dark:border-gray-700 rounded-lg">
<table className="w-full">
<thead className="bg-gray-100 dark:bg-gray-700 sticky top-0">
<tr>
<th className="text-left p-3 text-sm font-medium text-gray-700 dark:text-gray-300">
<input
type="checkbox"
checked={selectedAssets.length === filteredAssets.length && filteredAssets.length > 0}
onChange={handleSelectAll}
className="rounded"
/>
</th>
<th className="text-left p-3 text-sm font-medium text-gray-700 dark:text-gray-300">Asset Name</th>
<th className="text-left p-3 text-sm font-medium text-gray-700 dark:text-gray-300">Modality</th>
<th className="text-left p-3 text-sm font-medium text-gray-700 dark:text-gray-300">Manufacturer</th>
<th className="text-left p-3 text-sm font-medium text-gray-700 dark:text-gray-300">Model</th>
<th className="text-left p-3 text-sm font-medium text-gray-700 dark:text-gray-300">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{filteredAssets.length === 0 ? (
<tr>
<td colSpan={6} className="p-6 text-center text-gray-500 dark:text-gray-400">
No assets match your search criteria
</td>
</tr>
) : (
filteredAssets.map(asset => (
<tr
key={asset.name}
className={`hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors ${
selectedAssets.includes(asset.name) ? 'bg-blue-50 dark:bg-blue-900/20' : ''
}`}
>
<td className="p-3">
<input
type="checkbox"
checked={selectedAssets.includes(asset.name)}
onChange={() => toggleAssetSelection(asset.name)}
className="rounded"
/>
</td>
<td className="p-3 text-sm text-gray-900 dark:text-white font-medium">{asset.asset_name}</td>
<td className="p-3 text-sm text-gray-700 dark:text-gray-300">{asset.custom_modality || '-'}</td>
<td className="p-3 text-sm text-gray-700 dark:text-gray-300">{asset.custom_manufacturer || '-'}</td>
<td className="p-3 text-sm text-gray-700 dark:text-gray-300">{asset.custom_model || '-'}</td>
<td className="p-3 text-sm">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
asset.custom_device_status === 'Active'
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400'
: asset.custom_device_status === 'Inactive'
? 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400'
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'
}`}>
{asset.custom_device_status || '-'}
</span>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
)}
{/* Schedule Configuration */}
{selectedAssets.length > 0 && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2 text-gray-800 dark:text-white">
<FaCalendar /> Schedule Configuration
</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">PPM Name *</label>
<input
type="text"
value={scheduleData.pm_for}
onChange={(e) => 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
/>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">First PPM Date *</label>
<input
type="date"
value={scheduleData.start_date}
onChange={(e) => 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
/>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">Periodicity *</label>
<select
value={scheduleData.periodicity}
onChange={(e) => setScheduleData(prev => ({ ...prev, periodicity: 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"
>
<option value="Daily">Daily</option>
<option value="Weekly">Weekly</option>
<option value="Monthly">Monthly</option>
<option value="Quarterly">Quarterly</option>
<option value="Half-yearly">Half-yearly</option>
<option value="Yearly">Yearly</option>
<option value="2 Yearly">2 Yearly</option>
<option value="3 Yearly">3 Yearly</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">Maintenance Type</label>
<select
value={scheduleData.maintenance_type}
onChange={(e) => setScheduleData(prev => ({ ...prev, maintenance_type: 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"
>
<option value="Preventive">Preventive</option>
<option value="Corrective">Corrective</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">No. of PMs</label>
<input
type="number"
value={scheduleData.no_of_pms}
onChange={(e) => 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"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
End date will be auto-calculated
</p>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">Last PPM Date *</label>
<input
type="date"
value={scheduleData.end_date}
onChange={(e) => 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
/>
</div>
<div>
<LinkField
label="Maintenance Team"
doctype="Asset Maintenance Team"
value={scheduleData.maintenance_team}
onChange={(val) => setScheduleData(prev => ({ ...prev, maintenance_team: val }))}
/>
{scheduleData.maintenance_manager && (
<div className="mt-2 p-2 bg-blue-50 dark:bg-blue-900/20 rounded text-xs text-gray-600 dark:text-gray-400">
<span className="font-medium">Maintenance Manager:</span> {scheduleData.maintenance_manager}
</div>
)}
</div>
<div>
<LinkField
label="Assign To *"
doctype="User"
value={scheduleData.assign_to}
onChange={(val) => setScheduleData(prev => ({ ...prev, assign_to: val }))}
placeholder={scheduleData.maintenance_team ? "Select user (auto-selected if only one team member)" : "Select user to assign tasks"}
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Required for Asset Maintenance creation
</p>
{scheduleData.assign_to && (
<div className="mt-1 p-2 bg-green-50 dark:bg-green-900/20 rounded text-xs text-gray-600 dark:text-gray-400">
<span className="font-medium">Assigned To:</span> {scheduleData.assign_to}
</div>
)}
</div>
<div>
<LinkField
label="Department"
doctype="Department"
value={scheduleData.department}
onChange={(val) => setScheduleData(prev => ({ ...prev, department: val }))}
placeholder="Select department (optional)"
/>
</div>
</div>
<button
onClick={handleGenerateSchedule}
disabled={loading || !scheduleData.start_date || !scheduleData.end_date || !scheduleData.pm_for || !scheduleData.assign_to}
className="mt-6 bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded-lg flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
>
{loading ? (
<>
<FaSpinner className="animate-spin" />
Creating Schedules...
</>
) : (
<>
<FaCheckCircle />
Generate Maintenance Schedules ({selectedAssets.length} asset{selectedAssets.length !== 1 ? 's' : ''})
</>
)}
</button>
</div>
)}
{/* Empty State */}
{assets.length === 0 && !fetchingAssets && !successResult?.show && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-12 text-center">
<FaFilter className="mx-auto text-4xl text-gray-400 dark:text-gray-600 mb-4" />
<h3 className="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-2">
No Assets Loaded
</h3>
<p className="text-gray-500 dark:text-gray-400 mb-4">
Use the filters above to search for assets, then click "Fetch Assets" to load them.
</p>
<p className="text-sm text-blue-600 dark:text-blue-400">
Note: Only submitted assets without existing maintenance schedules will be shown.
</p>
</div>
)}
{/* Success Result Modal */}
{successResult?.show && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-2xl max-w-lg w-full p-6">
<div className="text-center mb-6">
<div className="w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
<FaCheckCircle className="text-green-600 dark:text-green-400 text-3xl" />
</div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
Schedules Created Successfully!
</h2>
<p className="text-gray-600 dark:text-gray-400">
{successResult.count} maintenance schedule{successResult.count !== 1 ? 's' : ''} have been created.
</p>
</div>
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 mb-6">
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
What was created:
</h3>
<div className="space-y-2">
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600 dark:text-gray-400">Document:</span>
<span className="text-sm font-medium text-blue-600 dark:text-blue-400">
{successResult.document}
</span>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
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.
</p>
</div>
</div>
<div className="flex flex-col gap-3">
{successResult.document && (
<button
onClick={() => {
navigate(`/ppm-planner/${successResult.document}`);
setSuccessResult(null);
}}
className="w-full bg-blue-600 hover:bg-blue-700 text-white px-4 py-3 rounded-lg font-medium text-center flex items-center justify-center gap-2"
>
<FaCalendar />
View PPM Planner
</button>
)}
<button
onClick={() => navigate('/maintenance-calendar')}
className="w-full bg-purple-600 hover:bg-purple-700 text-white px-4 py-3 rounded-lg font-medium flex items-center justify-center gap-2"
>
<FaCalendar />
View Calendar
</button>
<button
onClick={() => setSuccessResult(null)}
className="w-full bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 px-4 py-3 rounded-lg font-medium"
>
Create More Schedules
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default PPMPlanner;