1034 lines
47 KiB
TypeScript
1034 lines
47 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
||
import { useTranslation } from 'react-i18next';
|
||
import { useMaintenanceLogDetails, useMaintenanceMutations } from '../hooks/useAssetMaintenance';
|
||
import { FaArrowLeft, FaSave, FaEdit, FaClock, FaList, FaPlus, FaTrash, FaCheck, FaTimes, FaExclamationTriangle } from 'react-icons/fa';
|
||
import apiService from '../services/apiService';
|
||
import WorkflowActions from '../components/WorkflowActions';
|
||
import useDefaultHospital from '../hooks/useDefaultHospital';
|
||
|
||
// Cast apiService to any to bypass strict typing
|
||
const api = apiService as any;
|
||
|
||
// PPM Table Row Interface
|
||
interface PPMTableRow {
|
||
name?: string;
|
||
idx?: number;
|
||
maintenance_name: string;
|
||
working: number | boolean;
|
||
defect_found: number | boolean;
|
||
not_working: number | boolean;
|
||
}
|
||
|
||
// Updated Interface with all fields from JSON
|
||
interface MaintenanceLogData {
|
||
name?: string;
|
||
owner?: string;
|
||
creation?: string;
|
||
modified?: string;
|
||
modified_by?: string;
|
||
docstatus?: number;
|
||
idx?: number;
|
||
workflow_state?: string;
|
||
asset_maintenance?: string;
|
||
naming_series?: string;
|
||
asset_name: string;
|
||
custom_asset_type?: string;
|
||
item_code?: string;
|
||
item_name?: string;
|
||
custom_asset_names?: string;
|
||
custom_hospital_name?: string;
|
||
task?: string;
|
||
task_name?: string;
|
||
maintenance_type: string;
|
||
periodicity?: string;
|
||
has_certificate?: number | boolean;
|
||
custom_early_completion?: string;
|
||
maintenance_status: string;
|
||
custom_accepted_by_moh?: number | boolean;
|
||
assign_to_name?: string;
|
||
due_date?: string;
|
||
completion_date?: string;
|
||
custom_early_completion_reason?: string;
|
||
custom_accepted_by_moh_?: number | boolean;
|
||
custom_template?: string;
|
||
custom_table?: PPMTableRow[];
|
||
description?: string;
|
||
}
|
||
|
||
// Toast notification component
|
||
const Toast: React.FC<{ message: string; type: 'warning' | 'success' | 'error'; onClose: () => void }> = ({ message, type, onClose }) => {
|
||
useEffect(() => {
|
||
const timer = setTimeout(onClose, 10000);
|
||
return () => clearTimeout(timer);
|
||
}, [onClose]);
|
||
|
||
const bgColor = type === 'warning' ? 'bg-yellow-500' : type === 'success' ? 'bg-green-500' : 'bg-red-500';
|
||
|
||
return (
|
||
<div className={`fixed top-4 right-4 ${bgColor} text-white px-6 py-4 rounded-lg shadow-lg z-50 max-w-md`}>
|
||
<div className="flex items-start gap-3">
|
||
<FaExclamationTriangle className="mt-0.5 flex-shrink-0" />
|
||
<p className="text-sm">{message}</p>
|
||
<button onClick={onClose} className="ml-2 text-white hover:text-gray-200">
|
||
<FaTimes />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const AssetMaintenanceDetail: React.FC = () => {
|
||
const { t } = useTranslation();
|
||
const { logName } = useParams<{ logName: string }>();
|
||
const navigate = useNavigate();
|
||
const [searchParams] = useSearchParams();
|
||
const duplicateFromLog = searchParams.get('duplicate');
|
||
|
||
const isNewLog = logName === 'new';
|
||
const isDuplicating = isNewLog && !!duplicateFromLog;
|
||
|
||
const { log, loading, error } = useMaintenanceLogDetails(
|
||
isDuplicating ? duplicateFromLog : (isNewLog ? null : logName || null)
|
||
);
|
||
const { createLog, updateLog, loading: saving } = useMaintenanceMutations();
|
||
|
||
// Check if document is approved (not editable)
|
||
const isApproved = log?.workflow_state === 'Approved';
|
||
|
||
const [isEditing, setIsEditing] = useState(isNewLog);
|
||
const [ppmTableRows, setPpmTableRows] = useState<PPMTableRow[]>([]);
|
||
const [toast, setToast] = useState<{ message: string; type: 'warning' | 'success' | 'error' } | null>(null);
|
||
const [templateLoading, setTemplateLoading] = useState(false);
|
||
|
||
const [formData, setFormData] = useState<MaintenanceLogData>({
|
||
asset_name: '',
|
||
custom_asset_type: '',
|
||
item_code: '',
|
||
item_name: '',
|
||
custom_asset_names: '',
|
||
custom_hospital_name: '',
|
||
task: '',
|
||
task_name: '',
|
||
maintenance_type: 'Preventive Maintenance',
|
||
periodicity: '',
|
||
has_certificate: 0,
|
||
custom_early_completion: '',
|
||
maintenance_status: 'Planned',
|
||
custom_accepted_by_moh: 0,
|
||
assign_to_name: '',
|
||
due_date: '',
|
||
completion_date: '',
|
||
custom_early_completion_reason: '',
|
||
custom_accepted_by_moh_: 0,
|
||
custom_template: '',
|
||
custom_table: [],
|
||
description: '',
|
||
});
|
||
|
||
useDefaultHospital(setFormData, {
|
||
enabled: isNewLog && !isDuplicating,
|
||
fields: ['custom_hospital_name'],
|
||
});
|
||
|
||
// Check early completion logic
|
||
const checkEarlyCompletion = useCallback((dueDate: string, completionDate: string, maintenanceStatus: string) => {
|
||
if (maintenanceStatus !== 'Completed') {
|
||
setFormData(prev => ({ ...prev, custom_early_completion: '' }));
|
||
return;
|
||
}
|
||
|
||
if (!dueDate || !completionDate) {
|
||
return;
|
||
}
|
||
|
||
const dueDateObj = new Date(dueDate);
|
||
const completionDateObj = new Date(completionDate);
|
||
const dueMonthStart = new Date(dueDateObj.getFullYear(), dueDateObj.getMonth(), 1);
|
||
|
||
if (completionDateObj < dueMonthStart) {
|
||
setFormData(prev => ({ ...prev, custom_early_completion: 'Yes' }));
|
||
|
||
const formattedCompletionDate = completionDateObj.toLocaleDateString();
|
||
const formattedDueDate = dueDateObj.toLocaleDateString();
|
||
setToast({
|
||
message: `⚠️ Warning: Completion Date (${formattedCompletionDate}) is before the Due Date Month (${formattedDueDate}). Please verify.`,
|
||
type: 'warning'
|
||
});
|
||
}
|
||
}, []);
|
||
|
||
// Load PPM Template
|
||
const loadPPMTemplate = useCallback(async (templateName: string) => {
|
||
if (!templateName) return;
|
||
|
||
setTemplateLoading(true);
|
||
try {
|
||
const response = await api.apiCall(
|
||
`/api/resource/PPM Templates/${encodeURIComponent(templateName)}`,
|
||
'GET'
|
||
);
|
||
|
||
if (response?.data?.ppm_template_table && Array.isArray(response.data.ppm_template_table)) {
|
||
const templateRows: PPMTableRow[] = response.data.ppm_template_table.map((row: any, index: number) => ({
|
||
idx: index + 1,
|
||
maintenance_name: row.maintenance_name || '',
|
||
working: 0,
|
||
defect_found: 0,
|
||
not_working: 0,
|
||
}));
|
||
|
||
setPpmTableRows(templateRows);
|
||
}
|
||
} catch (err) {
|
||
console.error('Error loading PPM template:', err);
|
||
setToast({
|
||
message: 'Failed to load PPM template. Please add items manually.',
|
||
type: 'error'
|
||
});
|
||
} finally {
|
||
setTemplateLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
// Load data when log is loaded
|
||
useEffect(() => {
|
||
if (log) {
|
||
// If document is approved, ensure editing is disabled
|
||
if (log.workflow_state === 'Approved') {
|
||
setIsEditing(false);
|
||
}
|
||
|
||
setFormData({
|
||
asset_name: log.asset_name || '',
|
||
custom_asset_type: log.custom_asset_type || '',
|
||
item_code: log.item_code || '',
|
||
item_name: log.item_name || '',
|
||
custom_asset_names: log.custom_asset_names || '',
|
||
custom_hospital_name: log.custom_hospital_name || '',
|
||
task: log.task || '',
|
||
task_name: log.task_name || '',
|
||
maintenance_type: log.maintenance_type || 'Preventive Maintenance',
|
||
periodicity: log.periodicity || '',
|
||
has_certificate: log.has_certificate || 0,
|
||
custom_early_completion: isDuplicating ? '' : (log.custom_early_completion || ''),
|
||
maintenance_status: isDuplicating ? 'Planned' : (log.maintenance_status || 'Planned'),
|
||
custom_accepted_by_moh: log.custom_accepted_by_moh || 0,
|
||
assign_to_name: log.assign_to_name || '',
|
||
due_date: log.due_date || '',
|
||
completion_date: isDuplicating ? '' : (log.completion_date || ''),
|
||
custom_early_completion_reason: isDuplicating ? '' : (log.custom_early_completion_reason || ''),
|
||
custom_accepted_by_moh_: log.custom_accepted_by_moh_ || 0,
|
||
custom_template: log.custom_template || '',
|
||
custom_table: log.custom_table || [],
|
||
description: log.description || '',
|
||
});
|
||
|
||
if (log.custom_table && Array.isArray(log.custom_table) && log.custom_table.length > 0) {
|
||
setPpmTableRows(log.custom_table.map((row: any, index: number) => ({
|
||
name: row.name,
|
||
idx: row.idx || index + 1,
|
||
maintenance_name: row.maintenance_name || '',
|
||
working: row.working || 0,
|
||
defect_found: row.defect_found || 0,
|
||
not_working: row.not_working || 0,
|
||
})));
|
||
} else if (log.custom_template && (!log.custom_table || log.custom_table.length === 0)) {
|
||
loadPPMTemplate(log.custom_template);
|
||
}
|
||
}
|
||
}, [log, isDuplicating, loadPPMTemplate]);
|
||
|
||
// Handle completion_date change - trigger early completion check
|
||
useEffect(() => {
|
||
if (formData.completion_date && formData.due_date) {
|
||
checkEarlyCompletion(formData.due_date, formData.completion_date, formData.maintenance_status);
|
||
}
|
||
}, [formData.completion_date, formData.due_date, formData.maintenance_status, checkEarlyCompletion]);
|
||
|
||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||
const { name, value, type } = e.target;
|
||
const checked = (e.target as HTMLInputElement).checked;
|
||
|
||
setFormData(prev => ({
|
||
...prev,
|
||
[name]: type === 'checkbox' ? (checked ? 1 : 0) : value
|
||
}));
|
||
};
|
||
|
||
const handleStatusChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||
const newStatus = e.target.value;
|
||
setFormData(prev => ({
|
||
...prev,
|
||
maintenance_status: newStatus
|
||
}));
|
||
|
||
if (newStatus === 'Completed') {
|
||
checkEarlyCompletion(formData.due_date || '', formData.completion_date || '', newStatus);
|
||
} else {
|
||
setFormData(prev => ({ ...prev, custom_early_completion: '' }));
|
||
}
|
||
};
|
||
|
||
const handleCompletionDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const newDate = e.target.value;
|
||
setFormData(prev => ({
|
||
...prev,
|
||
completion_date: newDate
|
||
}));
|
||
|
||
if (formData.maintenance_status === 'Completed') {
|
||
checkEarlyCompletion(formData.due_date || '', newDate, formData.maintenance_status);
|
||
}
|
||
};
|
||
|
||
const handleTemplateChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const newTemplate = e.target.value;
|
||
setFormData(prev => ({ ...prev, custom_template: newTemplate }));
|
||
|
||
if (newTemplate && ppmTableRows.length === 0) {
|
||
loadPPMTemplate(newTemplate);
|
||
} else if (newTemplate && ppmTableRows.length > 0) {
|
||
if (window.confirm('Loading a template will replace existing checklist items. Continue?')) {
|
||
loadPPMTemplate(newTemplate);
|
||
}
|
||
}
|
||
};
|
||
|
||
// PPM Table handlers
|
||
const addPpmRow = () => {
|
||
const newRow: PPMTableRow = {
|
||
idx: ppmTableRows.length + 1,
|
||
maintenance_name: '',
|
||
working: 0,
|
||
defect_found: 0,
|
||
not_working: 0,
|
||
};
|
||
setPpmTableRows([...ppmTableRows, newRow]);
|
||
};
|
||
|
||
const removePpmRow = (index: number) => {
|
||
const updated = ppmTableRows.filter((_, i) => i !== index).map((row, i) => ({
|
||
...row,
|
||
idx: i + 1
|
||
}));
|
||
setPpmTableRows(updated);
|
||
};
|
||
|
||
const updatePpmRow = (index: number, field: keyof PPMTableRow, value: any) => {
|
||
const updated = [...ppmTableRows];
|
||
updated[index] = { ...updated[index], [field]: value };
|
||
setPpmTableRows(updated);
|
||
};
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
|
||
if (!formData.asset_name) {
|
||
alert('Please enter Asset Name');
|
||
return;
|
||
}
|
||
|
||
if (!formData.maintenance_type) {
|
||
alert('Please select Maintenance Type');
|
||
return;
|
||
}
|
||
|
||
// Clean PPM table rows - ensure name is either a valid string or removed
|
||
const cleanedPpmRows = ppmTableRows.map((row, index) => {
|
||
const cleanedRow: any = {
|
||
idx: row.idx || index + 1,
|
||
maintenance_name: row.maintenance_name || '',
|
||
working: row.working ? 1 : 0,
|
||
defect_found: row.defect_found ? 1 : 0,
|
||
not_working: row.not_working ? 1 : 0,
|
||
};
|
||
|
||
// Only include name if it's a valid non-empty string (existing row from DB)
|
||
if (row.name && typeof row.name === 'string' && row.name.trim() !== '') {
|
||
cleanedRow.name = row.name;
|
||
}
|
||
|
||
return cleanedRow;
|
||
});
|
||
|
||
const submitData = {
|
||
...formData,
|
||
custom_table: cleanedPpmRows,
|
||
};
|
||
|
||
console.log('Submitting maintenance log data:', submitData);
|
||
|
||
try {
|
||
if (isNewLog || isDuplicating) {
|
||
const newLog = await createLog(submitData as any);
|
||
const successMessage = isDuplicating
|
||
? 'Maintenance log duplicated successfully!'
|
||
: 'Maintenance log created successfully!';
|
||
alert(successMessage);
|
||
navigate(`/maintenance/${newLog.name}`);
|
||
} else if (logName) {
|
||
await updateLog(logName, submitData as any);
|
||
alert('Maintenance log updated successfully!');
|
||
setIsEditing(false);
|
||
}
|
||
} catch (err) {
|
||
console.error('Maintenance log save error:', err);
|
||
alert('Failed to save: ' + (err instanceof Error ? err.message : 'Unknown error'));
|
||
}
|
||
};
|
||
|
||
const getWorkflowStateBadge = (state: string) => {
|
||
const stateColors: Record<string, string> = {
|
||
'Draft': 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
|
||
'Applied': 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
||
'Pending': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
||
'Approved': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||
'Rejected': 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||
'Completed': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||
};
|
||
return stateColors[state] || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300';
|
||
};
|
||
|
||
const getStatusBadge = (status: string) => {
|
||
const statusColors: Record<string, string> = {
|
||
'Planned': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
||
'Completed': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||
'Overdue': 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||
'Cancelled': 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
|
||
};
|
||
return statusColors[status] || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300';
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-gray-900">
|
||
<div className="text-center">
|
||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
|
||
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading maintenance log...</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error && !isNewLog && !isDuplicating) {
|
||
return (
|
||
<div className="p-4 sm:p-6 bg-gray-50 dark:bg-gray-900 min-h-screen min-w-0 overflow-x-hidden">
|
||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
||
<p className="text-red-600 dark:text-red-400">Error: {error}</p>
|
||
<button
|
||
onClick={() => navigate('/maintenance')}
|
||
className="mt-2 text-red-700 dark:text-red-400 underline hover:text-red-800 dark:hover:text-red-300"
|
||
>
|
||
Back to maintenance logs
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-4 sm:p-6 min-w-0 overflow-x-hidden">
|
||
{/* Toast Notification */}
|
||
{toast && (
|
||
<Toast
|
||
message={toast.message}
|
||
type={toast.type}
|
||
onClose={() => setToast(null)}
|
||
/>
|
||
)}
|
||
|
||
{/* Header */}
|
||
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between lg:items-center flex-wrap">
|
||
<div className="flex items-center gap-4">
|
||
<button
|
||
onClick={() => navigate('/maintenance')}
|
||
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 flex items-center gap-2"
|
||
>
|
||
<FaArrowLeft />
|
||
<span className="text-gray-900 dark:text-white font-semibold">
|
||
{isDuplicating ? 'Duplicate Maintenance Log' : (isNewLog ? 'New Maintenance Log' : 'Maintenance Log Details')}
|
||
</span>
|
||
</button>
|
||
{!isNewLog && log?.workflow_state && (
|
||
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getWorkflowStateBadge(log.workflow_state)}`}>
|
||
{log.workflow_state}
|
||
</span>
|
||
)}
|
||
{/* {isApproved && (
|
||
<span className="px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 flex items-center gap-1">
|
||
🔒 Locked
|
||
</span>
|
||
)} */}
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
{!isNewLog && !isEditing && !isApproved && (
|
||
<button
|
||
onClick={() => setIsEditing(true)}
|
||
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg flex items-center gap-2"
|
||
>
|
||
<FaEdit />
|
||
Edit
|
||
</button>
|
||
)}
|
||
{isEditing && (
|
||
<>
|
||
<button
|
||
onClick={() => {
|
||
if (isNewLog) {
|
||
navigate('/maintenance');
|
||
} else {
|
||
setIsEditing(false);
|
||
}
|
||
}}
|
||
className="bg-gray-300 hover:bg-gray-400 text-gray-700 px-6 py-2 rounded-lg"
|
||
disabled={saving}
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
onClick={handleSubmit}
|
||
disabled={saving}
|
||
className="bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded-lg flex items-center gap-2 disabled:opacity-50"
|
||
>
|
||
<FaSave />
|
||
{saving ? 'Saving...' : 'Save Changes'}
|
||
</button>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Standard View Form */}
|
||
<form onSubmit={handleSubmit} className="space-y-6">
|
||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||
{/* Left Column - Main Content */}
|
||
<div className="lg:col-span-2 space-y-6">
|
||
{/* Basic Information */}
|
||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700">
|
||
<h2 className="text-base font-semibold text-gray-800 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
|
||
Basic Information
|
||
</h2>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Log ID</label>
|
||
<input
|
||
type="text"
|
||
value={isNewLog || isDuplicating ? 'Auto-generated' : log?.name || ''}
|
||
disabled
|
||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white"
|
||
/>
|
||
{isDuplicating && (
|
||
<p className="mt-1 text-xs text-blue-600 dark:text-blue-400">
|
||
💡 Duplicating from: {duplicateFromLog}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* <div>
|
||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Workflow State</label>
|
||
<input
|
||
type="text"
|
||
value={log?.workflow_state || 'Draft'}
|
||
disabled
|
||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white"
|
||
/>
|
||
</div> */}
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Hospital</label>
|
||
<input
|
||
type="text"
|
||
name="custom_hospital_name"
|
||
value={formData.custom_hospital_name || ''}
|
||
onChange={handleChange}
|
||
disabled={!isEditing}
|
||
placeholder="Hospital name"
|
||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
Asset Name <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
name="asset_name"
|
||
value={formData.asset_name}
|
||
onChange={handleChange}
|
||
required
|
||
disabled={!isEditing}
|
||
placeholder="Asset ID"
|
||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Asset Display Name</label>
|
||
<input
|
||
type="text"
|
||
name="custom_asset_names"
|
||
value={formData.custom_asset_names || ''}
|
||
onChange={handleChange}
|
||
disabled={!isEditing}
|
||
placeholder="Display name"
|
||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Asset Type</label>
|
||
<input
|
||
type="text"
|
||
name="custom_asset_type"
|
||
value={formData.custom_asset_type || ''}
|
||
onChange={handleChange}
|
||
disabled={!isEditing}
|
||
placeholder="e.g., Bio Medical"
|
||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||
/>
|
||
</div>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
{/* Maintenance Details */}
|
||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700">
|
||
<h2 className="text-base font-semibold text-gray-800 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
|
||
Maintenance Details
|
||
</h2>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
Maintenance Type <span className="text-red-500">*</span>
|
||
</label>
|
||
<select
|
||
name="maintenance_type"
|
||
value={formData.maintenance_type}
|
||
onChange={handleChange}
|
||
required
|
||
disabled={!isEditing}
|
||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||
>
|
||
<option value="Preventive Maintenance">Preventive Maintenance</option>
|
||
<option value="Corrective Maintenance">Corrective Maintenance</option>
|
||
<option value="Calibration">Calibration</option>
|
||
<option value="Inspection">Inspection</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Periodicity</label>
|
||
<select
|
||
name="periodicity"
|
||
value={formData.periodicity || ''}
|
||
onChange={handleChange}
|
||
disabled={!isEditing}
|
||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||
>
|
||
<option value="">Select periodicity</option>
|
||
<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-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
Template
|
||
{templateLoading && <span className="ml-2 text-blue-500">(Loading...)</span>}
|
||
</label>
|
||
<input
|
||
type="text"
|
||
name="custom_template"
|
||
value={formData.custom_template || ''}
|
||
onChange={handleTemplateChange}
|
||
disabled={!isEditing}
|
||
placeholder="PPM Template name"
|
||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||
/>
|
||
<p className="mt-1 text-xs text-gray-500">Enter template name to auto-load checklist items</p>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Status</label>
|
||
<select
|
||
name="maintenance_status"
|
||
value={formData.maintenance_status}
|
||
onChange={handleStatusChange}
|
||
disabled={!isEditing}
|
||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||
>
|
||
<option value="Planned">Planned</option>
|
||
<option value="Completed">Completed</option>
|
||
<option value="Overdue">Overdue</option>
|
||
<option value="Cancelled">Cancelled</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Assigned To</label>
|
||
<input
|
||
type="text"
|
||
name="assign_to_name"
|
||
value={formData.assign_to_name || ''}
|
||
onChange={handleChange}
|
||
disabled={!isEditing}
|
||
placeholder="Technician name"
|
||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Due Date</label>
|
||
<input
|
||
type="date"
|
||
name="due_date"
|
||
value={formData.due_date || ''}
|
||
onChange={handleChange}
|
||
disabled={!isEditing}
|
||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Completion Date</label>
|
||
<input
|
||
type="date"
|
||
name="completion_date"
|
||
value={formData.completion_date || ''}
|
||
onChange={handleCompletionDateChange}
|
||
disabled={!isEditing}
|
||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Early Completion & MOH Section */}
|
||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700">
|
||
<h2 className="text-base font-semibold text-gray-800 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
|
||
Completion & Approval
|
||
</h2>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
Early Completion
|
||
{formData.custom_early_completion === 'Yes' && (
|
||
<span className="ml-2 text-yellow-500">⚠️ Auto-detected</span>
|
||
)}
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.custom_early_completion || 'No'}
|
||
disabled
|
||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white"
|
||
/>
|
||
<p className="mt-1 text-xs text-gray-500">Auto-set based on completion date vs due date</p>
|
||
</div>
|
||
|
||
{formData.custom_early_completion === 'Yes' && (
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
Early Completion Reason <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
name="custom_early_completion_reason"
|
||
value={formData.custom_early_completion_reason || ''}
|
||
onChange={handleChange}
|
||
disabled={!isEditing}
|
||
required
|
||
placeholder="Required when early completion"
|
||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
<div className="flex items-center gap-2">
|
||
<input
|
||
type="checkbox"
|
||
name="custom_accepted_by_moh"
|
||
checked={!!formData.custom_accepted_by_moh}
|
||
onChange={handleChange}
|
||
disabled={!isEditing}
|
||
className="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
<label className="text-xs font-medium text-gray-700 dark:text-gray-300">Accepted by MOH</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* PPM Table (Checklist) */}
|
||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700">
|
||
<div className="flex justify-between items-center mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
|
||
<h2 className="text-base font-semibold text-gray-800 dark:text-white flex items-center gap-2">
|
||
<FaList />
|
||
PPM Checklist Table
|
||
{templateLoading && <span className="text-sm text-blue-500 font-normal">(Loading template...)</span>}
|
||
</h2>
|
||
{isEditing && (
|
||
<button
|
||
type="button"
|
||
onClick={addPpmRow}
|
||
className="flex items-center gap-1 px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-md"
|
||
>
|
||
<FaPlus className="text-xs" />
|
||
Add Row
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full">
|
||
<thead>
|
||
<tr className="bg-gray-50 dark:bg-gray-700">
|
||
<th className="text-left py-3 px-3 text-xs font-medium text-gray-700 dark:text-gray-300 w-12">#</th>
|
||
<th className="text-left py-3 px-3 text-xs font-medium text-gray-700 dark:text-gray-300">Maintenance Task</th>
|
||
<th className="text-center py-3 px-3 text-xs font-medium text-green-600 dark:text-green-400 w-24">
|
||
<div className="flex items-center justify-center gap-1">
|
||
<FaCheck /> Working
|
||
</div>
|
||
</th>
|
||
<th className="text-center py-3 px-3 text-xs font-medium text-yellow-600 dark:text-yellow-400 w-28">
|
||
<div className="flex items-center justify-center gap-1">
|
||
<FaExclamationTriangle /> Defect
|
||
</div>
|
||
</th>
|
||
<th className="text-center py-3 px-3 text-xs font-medium text-red-600 dark:text-red-400 w-28">
|
||
<div className="flex items-center justify-center gap-1">
|
||
<FaTimes /> Not Working
|
||
</div>
|
||
</th>
|
||
{isEditing && <th className="text-center py-3 px-3 text-xs font-medium text-gray-700 dark:text-gray-300 w-16">Action</th>}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{ppmTableRows.length === 0 ? (
|
||
<tr>
|
||
<td colSpan={isEditing ? 6 : 5} className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||
No checklist items. {isEditing && 'Enter a template name above or click "Add Row" to add items.'}
|
||
</td>
|
||
</tr>
|
||
) : (
|
||
ppmTableRows.map((row, index) => (
|
||
<tr key={row.name || index} className="border-b border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
||
<td className="py-3 px-3 text-sm text-gray-600 dark:text-gray-400">{row.idx || index + 1}</td>
|
||
<td className="py-3 px-3">
|
||
{isEditing ? (
|
||
<input
|
||
type="text"
|
||
value={row.maintenance_name}
|
||
onChange={(e) => updatePpmRow(index, 'maintenance_name', e.target.value)}
|
||
placeholder="Enter maintenance task"
|
||
className="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:outline-none focus:ring-1 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||
/>
|
||
) : (
|
||
<span className="text-sm text-gray-900 dark:text-white">{row.maintenance_name}</span>
|
||
)}
|
||
</td>
|
||
<td className="py-3 px-3 text-center">
|
||
<input
|
||
type="checkbox"
|
||
checked={!!row.working}
|
||
onChange={(e) => updatePpmRow(index, 'working', e.target.checked ? 1 : 0)}
|
||
disabled={!isEditing}
|
||
className="w-5 h-5 text-green-600 rounded focus:ring-2 focus:ring-green-500 cursor-pointer disabled:cursor-default"
|
||
/>
|
||
</td>
|
||
<td className="py-3 px-3 text-center">
|
||
<input
|
||
type="checkbox"
|
||
checked={!!row.defect_found}
|
||
onChange={(e) => updatePpmRow(index, 'defect_found', e.target.checked ? 1 : 0)}
|
||
disabled={!isEditing}
|
||
className="w-5 h-5 text-yellow-600 rounded focus:ring-2 focus:ring-yellow-500 cursor-pointer disabled:cursor-default"
|
||
/>
|
||
</td>
|
||
<td className="py-3 px-3 text-center">
|
||
<input
|
||
type="checkbox"
|
||
checked={!!row.not_working}
|
||
onChange={(e) => updatePpmRow(index, 'not_working', e.target.checked ? 1 : 0)}
|
||
disabled={!isEditing}
|
||
className="w-5 h-5 text-red-600 rounded focus:ring-2 focus:ring-red-500 cursor-pointer disabled:cursor-default"
|
||
/>
|
||
</td>
|
||
{isEditing && (
|
||
<td className="py-3 px-3 text-center">
|
||
<button
|
||
type="button"
|
||
onClick={() => removePpmRow(index)}
|
||
className="text-red-500 hover:text-red-700 p-1"
|
||
title="Remove row"
|
||
>
|
||
<FaTrash />
|
||
</button>
|
||
</td>
|
||
)}
|
||
</tr>
|
||
))
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right Column - Status Summary */}
|
||
<div className="space-y-6">
|
||
{/* Workflow Actions */}
|
||
{!isNewLog && log && (
|
||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700">
|
||
<h2 className="text-base font-semibold text-gray-800 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
|
||
Workflow Actions
|
||
</h2>
|
||
<WorkflowActions
|
||
doctype="Asset Maintenance Log"
|
||
docname={log.name || null}
|
||
workflowState={log.workflow_state}
|
||
showStateInfo={false}
|
||
onActionComplete={(action, success) => {
|
||
if (success) {
|
||
console.log(`Action "${action}" completed successfully`);
|
||
}
|
||
}}
|
||
onStateChange={() => {
|
||
// Reload page to reflect new state
|
||
window.location.reload();
|
||
}}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* Status Card */}
|
||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700">
|
||
<h2 className="text-base font-semibold text-gray-800 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
|
||
Status Summary
|
||
</h2>
|
||
|
||
{!isNewLog && log ? (
|
||
<div className="space-y-4">
|
||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Current Status</p>
|
||
<span className={`inline-block px-3 py-1 rounded-full text-sm font-medium ${getStatusBadge(formData.maintenance_status)}`}>
|
||
{formData.maintenance_status || 'Planned'}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Workflow State</p>
|
||
<span className={`inline-block px-3 py-1 rounded-full text-sm font-medium ${getWorkflowStateBadge(log.workflow_state || 'Draft')}`}>
|
||
{log.workflow_state || 'Draft'}
|
||
</span>
|
||
</div>
|
||
|
||
{formData.custom_early_completion === 'Yes' && (
|
||
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
|
||
<p className="text-xs text-yellow-600 dark:text-yellow-400 mb-1">⚠️ Early Completion</p>
|
||
<p className="text-sm text-yellow-700 dark:text-yellow-300 font-medium">
|
||
Completion date is before the due month
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Due Date</p>
|
||
<p className="text-sm text-gray-900 dark:text-white">
|
||
{formData.due_date ? new Date(formData.due_date).toLocaleDateString() : 'Not set'}
|
||
</p>
|
||
{formData.due_date && new Date(formData.due_date) < new Date() && formData.maintenance_status !== 'Completed' && (
|
||
<p className="text-xs text-red-600 dark:text-red-400 font-semibold mt-1">
|
||
⚠️ Overdue
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Completion Date</p>
|
||
<p className="text-sm text-gray-900 dark:text-white">
|
||
{formData.completion_date ? new Date(formData.completion_date).toLocaleDateString() : 'Not completed'}
|
||
</p>
|
||
</div>
|
||
|
||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Assigned To</p>
|
||
<p className="text-sm text-gray-900 dark:text-white">
|
||
{formData.assign_to_name || 'Unassigned'}
|
||
</p>
|
||
</div>
|
||
|
||
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">PPM Checklist</p>
|
||
<p className="text-sm text-gray-900 dark:text-white">
|
||
{ppmTableRows.length} item(s)
|
||
</p>
|
||
{ppmTableRows.length > 0 && (
|
||
<div className="mt-2 text-xs space-y-1">
|
||
<p className="text-green-600 dark:text-green-400">
|
||
✓ Working: {ppmTableRows.filter(r => r.working).length}
|
||
</p>
|
||
<p className="text-yellow-600 dark:text-yellow-400">
|
||
⚠ Defects: {ppmTableRows.filter(r => r.defect_found).length}
|
||
</p>
|
||
<p className="text-red-600 dark:text-red-400">
|
||
✗ Not Working: {ppmTableRows.filter(r => r.not_working).length}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="text-center py-8">
|
||
<FaClock className="text-4xl text-gray-400 dark:text-gray-500 mx-auto mb-2" />
|
||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||
Status information will appear after creation
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Audit Information */}
|
||
{!isNewLog && log && (
|
||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700">
|
||
<h2 className="text-base font-semibold text-gray-800 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
|
||
Audit Information
|
||
</h2>
|
||
<div className="space-y-3 text-sm">
|
||
<div>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400">Created By</p>
|
||
<p className="text-gray-900 dark:text-white">{log.owner || '-'}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400">Created On</p>
|
||
<p className="text-gray-900 dark:text-white">
|
||
{log.creation ? new Date(log.creation).toLocaleString() : '-'}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400">Modified By</p>
|
||
<p className="text-gray-900 dark:text-white">{log.modified_by || '-'}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400">Modified On</p>
|
||
<p className="text-gray-900 dark:text-white">
|
||
{log.modified ? new Date(log.modified).toLocaleString() : '-'}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default AssetMaintenanceDetail; |