import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; import { useWorkOrderDetails, useWorkOrderMutations } from '../hooks/useWorkOrder'; import { useWorkflow } from '../hooks/useWorkflow'; import { useFrappeFieldBehavior } from '../hooks/useFrappeFieldBehavior'; import { setCurrentUser } from '../services/workflowService'; import type { WorkflowTransition } from '../services/workflowService'; import { FaArrowLeft, FaSave, FaEdit, FaLink, FaSearch, FaSpinner, FaExclamationTriangle, FaInfoCircle, FaPrint, FaPlus, FaTrash, FaCheckCircle, FaTimesCircle } from 'react-icons/fa'; import type { CreateWorkOrderData } from '../services/workOrderService'; import { toast, ToastContainer, Bounce } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import LinkField from '../components/LinkField'; import WorkflowActions from '../components/WorkflowActions'; import ActivityLog from '../components/ActivityLog'; import CommentSection from '../components/CommentSection'; import WoFeedbackSummary from '../components/WoFeedbackSummary'; import apiService from '../services/apiService'; import API_CONFIG from '../config/api'; import useDefaultHospital from '../hooks/useDefaultHospital'; import { isSiteEnabledHospital, buildMobileTeamSiteFilters } from '../utils/hospitalUtils'; import useUserPhccSiteFilter from '../hooks/useUserPhccSiteFilter'; import { applyWorkOrderStatusTimestamps, calculateTotalHoursSpent, formatDatetimeForApi, formatDatetimeForInput, } from '../utils/workOrderTimingUtils'; // Print Format Configuration const PRINT_FORMAT_NAME = 'Service Report'; // Change this if your print format has a different name // Helper to get today's date in YYYY-MM-DD format const getTodayDate = (): string => { return new Date().toISOString().split('T')[0]; }; // Helper to add days to a date const addDays = (dateStr: string, days: number): string => { if (!dateStr) return ''; const date = new Date(dateStr); date.setDate(date.getDate() + days); return date.toISOString().split('T')[0]; }; const WorkOrderDetail: React.FC = () => { const { workOrderName } = useParams<{ workOrderName: string }>(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const duplicateFromWorkOrder = searchParams.get('duplicate'); // Check if creating from Asset const assetIdFromParams = searchParams.get('asset'); const isCreatingFromAsset = !!assetIdFromParams; const isNewWorkOrder = workOrderName === 'new'; const isDuplicating = isNewWorkOrder && !!duplicateFromWorkOrder; /** * Open Service Report print format in a new window * Uses Frappe's built-in print view with trigger_print to auto-open print dialog */ const handlePrintServiceReport = () => { if (!workOrderName || isNewWorkOrder) return; // Construct the print URL using Frappe's printview const baseUrl = API_CONFIG.BASE_URL || ''; const printUrl = `${baseUrl}/printview?doctype=Work_Order&name=${encodeURIComponent(workOrderName)}&format=${encodeURIComponent(PRINT_FORMAT_NAME)}&trigger_print=1`; // Open in new window/tab const printWindow = window.open(printUrl, '_blank'); // Fallback: If popup is blocked, show message if (!printWindow) { toast.warning('Please allow popups for this site to print the Service Report.', { position: "top-right", autoClose: 5000, icon: }); } }; const { workOrder, loading, error, refetch } = useWorkOrderDetails( isDuplicating ? duplicateFromWorkOrder : (isNewWorkOrder ? null : workOrderName || null) ); const { createWorkOrder, updateWorkOrder, loading: saving } = useWorkOrderMutations(); const [isEditing, setIsEditing] = useState(isNewWorkOrder); const [isLoadingAsset, setIsLoadingAsset] = useState(false); const [confirmAction, setConfirmAction] = useState<{ action: string; nextState: string } | null>(null); // Stock Item interface for child table interface StockItem { item_code: string; item_name?: string; warehouse: string; consumed_quantity: number; valuation_rate: number; custom_available_stock: number; total_value: number; } const [formData, setFormData] = useState({ company: '', work_order_type: isNewWorkOrder && !isDuplicating ? 'Repair (CM)' : '', asset: '', asset_name: '', description: '', repair_status: 'Open', workflow_state: 'Draft', department: '', custom_priority_: 'Normal', asset_type: '', manufacturer: '', supplier: '', serial_number: '', custom_local_id: '', custom_moh_id: '', model: '', custom_site_contractor: '', custom_subcontractor: '', failure_date: isNewWorkOrder ? getTodayDate() : '', custom_deadline_date: '', completion_date: '', first_responded_on: '', actions_performed: '', stock_consumption: 0, stock_items: [], // Fields for workflow conditions site_name: '', need_procurement: 0, custom_assign_to_contractor: '', docstatus: 0, // New fields custom_assigned_supervisor: '', custom_moh_supervisor: '', total_hours_spent: 0, custom_pending_reason: '', total_repair_cost: 0, // Service Agreement fields custom_service_agreement: '', custom_service_coverage: '', custom_start_date: '', custom_end_date: '', custom_total_amount: 0, // For Frappe field behavior evaluation __islocal: false, }); useDefaultHospital(setFormData, { enabled: isNewWorkOrder && !isCreatingFromAsset, fields: ['company'], }); // Check if asset type is Non Biomedical const isNonBiomedical = formData.asset_type === 'Non Biomedical'; // Calculate deadline date based on failure_date, priority, and need_procurement const calculateDeadlineDate = useCallback(( failureDate: string, priority: string, needProcurement: number ): string => { if (!failureDate) return ''; let daysToAdd = 0; const isProcurementNeeded = needProcurement === 1; if (priority === 'Normal') { daysToAdd = isProcurementNeeded ? 30 : 5; } else if (priority === 'Urgent') { daysToAdd = isProcurementNeeded ? 30 : 1; } return addDays(failureDate, daysToAdd); }, []); // Update deadline date when failure_date, priority, or need_procurement changes useEffect(() => { // Only auto-calculate when editing if (!isEditing) return; const newDeadlineDate = calculateDeadlineDate( formData.failure_date, formData.custom_priority_ || 'Normal', formData.need_procurement || 0 ); if (newDeadlineDate && newDeadlineDate !== formData.custom_deadline_date) { setFormData(prev => ({ ...prev, custom_deadline_date: newDeadlineDate })); } }, [formData.failure_date, formData.custom_priority_, formData.need_procurement, isEditing, calculateDeadlineDate]); // Auto-set response/completion timestamps and recalculate hours when status changes useEffect(() => { if (!isEditing) return; setFormData(prev => { const next = applyWorkOrderStatusTimestamps(prev); if ( next.first_responded_on === prev.first_responded_on && next.completion_date === prev.completion_date && next.total_hours_spent === prev.total_hours_spent ) { return prev; } return next; }); }, [formData.repair_status, isEditing]); // Keep total hours in sync when both timestamps are present useEffect(() => { if (!isEditing) return; const hours = calculateTotalHoursSpent(formData.first_responded_on, formData.completion_date); setFormData(prev => { if ((prev.total_hours_spent || 0) === hours) return prev; return { ...prev, total_hours_spent: hours }; }); }, [formData.first_responded_on, formData.completion_date, isEditing]); // Clear asset-related fields when asset type changes to Non Biomedical useEffect(() => { if (isNonBiomedical && isEditing) { setFormData(prev => ({ ...prev, // Clear fields that should be hidden for Non Biomedical asset: '', asset_name: '', serial_number: '', custom_local_id: '', custom_moh_id: '', manufacturer: '', supplier: '', model: '', })); } }, [isNonBiomedical, isEditing]); // Frappe dynamic field behavior - evaluates depends_on, mandatory_depends_on, read_only_depends_on const { shouldShowField: frappeShowField, isMandatory: frappeMandatory, isReadOnly: frappeReadOnly, loading: fieldConfigLoading } = useFrappeFieldBehavior('Work_Order', formData as Record); // Helper function to check if field should be visible based on Frappe's depends_on const shouldShowField = useCallback((fieldname: string): boolean => { return frappeShowField(fieldname); }, [frappeShowField]); // Helper function to check if field is mandatory based on Frappe's mandatory_depends_on const isFieldMandatory = useCallback((fieldname: string): boolean => { return frappeMandatory(fieldname); }, [frappeMandatory]); // Helper function to check if field is read-only based on Frappe's read_only_depends_on const isFieldReadOnlyFromFrappe = useCallback((fieldname: string): boolean => { return frappeReadOnly(fieldname); }, [frappeReadOnly]); // Combined helper to check if field should be disabled (not editing OR Frappe read_only_depends_on) const isFieldDisabled = useCallback((fieldname: string): boolean => { if (!isEditing) return true; return isFieldReadOnlyFromFrappe(fieldname); }, [isEditing, isFieldReadOnlyFromFrappe]); // Document data for workflow condition evaluation const workOrderDocData = useMemo(() => { if (isNewWorkOrder) return undefined; const docSource = isEditing ? { ...(workOrder || {}), ...formData } : (workOrder || formData); return { asset_type: docSource.asset_type || '', site_name: docSource.site_name || '', need_procurement: docSource.need_procurement ?? 0, custom_assign_to_contractor: docSource.custom_assign_to_contractor || '', docstatus: docSource.docstatus ?? 0, company: docSource.company || '', department: docSource.department || '', repair_status: docSource.repair_status || '', }; }, [ isNewWorkOrder, isEditing, workOrder, formData.asset_type, formData.site_name, formData.need_procurement, formData.custom_assign_to_contractor, formData.docstatus, formData.company, formData.department, formData.repair_status, ]); // Workflow hook - now with docData for condition evaluation const { transitions, loading: workflowLoading, actionLoading, error: workflowError, canEdit: workflowCanEdit, isSystemManager, applyAction, getStateStyle, getButtonStyle, getIcon, } = useWorkflow({ doctype: 'Work_Order', docname: isNewWorkOrder ? null : workOrderName || null, workflowState: workOrder?.workflow_state, enabled: !isNewWorkOrder, docData: workOrderDocData, // Pass document data for condition evaluation }); // Default warehouse for new stock items const DEFAULT_WAREHOUSE = 'Al Jouf Central Warehouse - AJH'; // Stock warnings state const [stockWarnings, setStockWarnings] = useState>({}); // Function to fetch available stock from Bin doctype const fetchAvailableStock = async (itemCode: string, warehouse: string): Promise => { if (!itemCode || !warehouse) return 0; try { const response = await apiService.apiCall( `/api/resource/Bin?filters=[["item_code","=","${itemCode}"],["warehouse","=","${warehouse}"]]&fields=["actual_qty"]&limit=1` ); if (response?.data && response.data.length > 0) { return response.data[0].actual_qty || 0; } return 0; } catch (err) { console.error('Error fetching available stock:', err); return 0; } }; // Function to fetch item valuation rate const fetchItemValuationRate = async (itemCode: string): Promise => { if (!itemCode) return 0; try { const response = await apiService.apiCall( `/api/resource/Item/${itemCode}?fields=["valuation_rate"]` ); return response?.data?.valuation_rate || 0; } catch (err) { console.error('Error fetching valuation rate:', err); return 0; } }; // Handle item code change - fetch valuation rate and available stock const handleItemCodeChange = async (index: number, itemCode: string) => { const updatedItems = [...(formData.stock_items || [])]; updatedItems[index] = { ...updatedItems[index], item_code: itemCode }; if (itemCode) { // Fetch valuation rate const valuationRate = await fetchItemValuationRate(itemCode); updatedItems[index].valuation_rate = valuationRate; updatedItems[index].total_value = valuationRate * (updatedItems[index].consumed_quantity || 1); // Fetch available stock if warehouse is set if (updatedItems[index].warehouse) { const availableStock = await fetchAvailableStock(itemCode, updatedItems[index].warehouse); updatedItems[index].custom_available_stock = availableStock; // Check stock and set warning validateStock(index, updatedItems[index].consumed_quantity, availableStock, itemCode, updatedItems[index].warehouse); } } setFormData({ ...formData, stock_items: updatedItems }); }; // Handle warehouse change - fetch available stock const handleWarehouseChange = async (index: number, warehouse: string) => { const updatedItems = [...(formData.stock_items || [])]; updatedItems[index] = { ...updatedItems[index], warehouse }; if (warehouse && updatedItems[index].item_code) { const availableStock = await fetchAvailableStock(updatedItems[index].item_code, warehouse); updatedItems[index].custom_available_stock = availableStock; // Check stock and set warning validateStock(index, updatedItems[index].consumed_quantity, availableStock, updatedItems[index].item_code, warehouse); } setFormData({ ...formData, stock_items: updatedItems }); }; // Validate stock quantity const validateStock = (index: number, consumedQty: number, availableStock: number, itemCode: string, warehouse: string) => { if (consumedQty > availableStock) { setStockWarnings(prev => ({ ...prev, [index]: `Insufficient stock for ${itemCode} in ${warehouse}. Available: ${availableStock}, Required: ${consumedQty}` })); toast.warning(`Insufficient stock for ${itemCode}. Available: ${availableStock}, Required: ${consumedQty}`, { position: "top-right", autoClose: 5000, icon: , toastId: `stock-warning-${index}` // Prevent duplicate toasts for same item }); } else { setStockWarnings(prev => { const newWarnings = { ...prev }; delete newWarnings[index]; return newWarnings; }); } }; // Handle consumed quantity change const handleConsumedQtyChange = (index: number, qty: number) => { const updatedItems = [...(formData.stock_items || [])]; const rate = updatedItems[index].valuation_rate || 0; const availableStock = updatedItems[index].custom_available_stock || 0; updatedItems[index] = { ...updatedItems[index], consumed_quantity: qty, total_value: rate * qty }; // Validate stock if (updatedItems[index].item_code && updatedItems[index].warehouse) { validateStock(index, qty, availableStock, updatedItems[index].item_code, updatedItems[index].warehouse); } setFormData({ ...formData, stock_items: updatedItems }); }; // Department filters based on company const [departmentFilters, setDepartmentFilters] = useState>({}); const { userPhccSiteName, siteLocked } = useUserPhccSiteFilter(); const shouldShowSiteNameField = useMemo( () => isSiteEnabledHospital(formData.company), [formData.company] ); const mobileTeamSiteFilters = useMemo( () => buildMobileTeamSiteFilters(formData.company, userPhccSiteName), [formData.company, userPhccSiteName] ); // Auto-set site name from user profile when available useEffect(() => { if (!userPhccSiteName) return; setFormData((prev) => { if (prev.site_name) return prev; return { ...prev, site_name: userPhccSiteName }; }); }, [userPhccSiteName]); const assignedTechnicianFilters = useMemo(() => { if (!formData.company) { return {}; } return { custom_site_name: formData.company, role_profile_name: 'Technician', }; }, [formData.company]); const mohSupervisorFilters = useMemo(() => { if (!formData.company) { return {}; } return { custom_site_name: formData.company, role_profile_name: 'MOH Supervisor', }; }, [formData.company]); const teamLeaderFilters = useMemo(() => { if (!formData.company) { return {}; } return { custom_site_name: formData.company, role_profile_name: 'Maintenace Manager', }; }, [formData.company]); // Update department filters when company changes useEffect(() => { const filters: Record = {}; if (formData.company) { filters['company'] = formData.company; } setDepartmentFilters(filters); }, [formData.company]); // Function to fetch asset details by Asset ID const fetchAssetDetails = async (assetId: string) => { if (!assetId) return null; try { setIsLoadingAsset(true); const response = await apiService.apiCall(`/api/resource/Asset/${assetId}`); return response?.data || null; } catch (err) { console.error('Error fetching asset details:', err); return null; } finally { setIsLoadingAsset(false); } }; // Function to search asset by Serial Number const fetchAssetBySerialNumber = async (serialNumber: string) => { if (!serialNumber) return null; try { setIsLoadingAsset(true); const response = await apiService.apiCall( `/api/resource/Asset?filters=[["custom_serial_number","=","${serialNumber}"]]&fields=["name","asset_name","company","department","custom_serial_number","custom_local_id","custom_moh_id","custom_asset_type","custom_manufacturer","supplier","custom_site_contractor","custom_subcontractor","custom_model","custom_service_agreement","custom_service_coverage","custom_start_date","custom_end_date","custom_total_amount","custom_site","custom_team_leader","custom_moh_supervisor","custom_smeh_engg"]&limit=1` ); if (response?.data && response.data.length > 0) { return response.data[0]; } return null; } catch (err) { console.error('Error fetching asset by serial number:', err); return null; } finally { setIsLoadingAsset(false); } }; // Helper function to format date for input const formatDateForInput = (dateStr: string | null | undefined): string => { if (!dateStr) return ''; // Extract just "YYYY-MM-DD" from "YYYY-MM-DD HH:MM:SS" return dateStr.split(' ')[0]; }; // Function to populate form with asset data const populateFromAsset = (assetData: any) => { if (!assetData) return; setFormData(prev => ({ ...prev, asset: assetData.name || prev.asset, asset_name: assetData.asset_name || '', company: assetData.company || '', department: assetData.department || '', serial_number: assetData.custom_serial_number || '', custom_local_id: assetData.custom_local_id || '', custom_moh_id: assetData.custom_moh_id || '', asset_type: assetData.custom_asset_type || '', manufacturer: assetData.custom_manufacturer || '', supplier: assetData.supplier || '', custom_site_contractor: assetData.custom_site_contractor || '', custom_subcontractor: assetData.custom_subcontractor || '', model: assetData.custom_model || '', site_name: assetData.custom_site || '', // Service Agreement fields - auto-populate from asset custom_service_agreement: assetData.custom_service_agreement || '', custom_service_coverage: assetData.custom_service_coverage || '', custom_start_date: formatDateForInput(assetData.custom_start_date) || '', custom_end_date: formatDateForInput(assetData.custom_end_date) || '', custom_total_amount: assetData.custom_total_amount || 0, custom_assigned_supervisor: assetData.custom_team_leader || '', custom_moh_supervisor: assetData.custom_moh_supervisor || '', custom_assign_to_contractor: assetData.custom_smeh_engg || '', })); }; // Handler for Asset ID change const handleAssetIdChange = async (assetId: string) => { setFormData(prev => ({ ...prev, asset: assetId })); if (assetId) { const assetData = await fetchAssetDetails(assetId); if (assetData) { populateFromAsset(assetData); } } else { setFormData(prev => ({ ...prev, asset: '', asset_name: '', serial_number: '', custom_local_id: '', custom_moh_id: '', asset_type: '', manufacturer: '', supplier: '', custom_site_contractor: '', custom_subcontractor: '', model: '', site_name: '', // Clear service agreement fields custom_service_agreement: '', custom_service_coverage: '', custom_start_date: '', custom_end_date: '', custom_total_amount: 0, custom_assigned_supervisor: '', custom_moh_supervisor: '', custom_assign_to_contractor: '', })); } }; // Handler for Serial Number search const handleSerialNumberSearch = async () => { if (!formData.serial_number) { toast.warning('Please enter a serial number to search', { position: "top-right", autoClose: 3000, icon: }); return; } const assetData = await fetchAssetBySerialNumber(formData.serial_number); if (assetData) { populateFromAsset(assetData); toast.success(`Asset found: ${assetData.asset_name || assetData.name}`, { position: "top-right", autoClose: 3000, icon: }); } else { toast.error('No asset found with this serial number', { position: "top-right", autoClose: 4000, icon: }); } }; // Handler for Serial Number blur const handleSerialNumberBlur = async () => { if (formData.serial_number && !formData.asset) { const assetData = await fetchAssetBySerialNumber(formData.serial_number); if (assetData) { populateFromAsset(assetData); } } }; // Pre-populate form when creating from Asset useEffect(() => { if (isNewWorkOrder && isCreatingFromAsset && !isDuplicating) { const assetData = { asset: searchParams.get('asset') || '', asset_name: searchParams.get('asset_name') || '', asset_type: searchParams.get('asset_type') || '', manufacturer: searchParams.get('manufacturer') || '', supplier: searchParams.get('supplier') || '', serial_number: searchParams.get('serial_number') || '', custom_local_id: searchParams.get('local_id') || '', custom_moh_id: searchParams.get('moh_id') || '', department: searchParams.get('department') || '', custom_site_contractor: searchParams.get('site_contractor') || '', custom_subcontractor: searchParams.get('subcontractor') || '', company: searchParams.get('company') || '', site_name: searchParams.get('site_name') || '', // Supervisor fields from URL params (mapped from Asset) custom_assigned_supervisor: searchParams.get('team_leader') || '', custom_moh_supervisor: searchParams.get('moh_supervisor') || '', custom_assign_to_contractor: searchParams.get('smeh_engg') || '', }; setFormData(prev => ({ ...prev, ...assetData, repair_status: 'Open', workflow_state: 'Draft', custom_priority_: 'Normal', failure_date: getTodayDate(), })); } }, [isNewWorkOrder, isCreatingFromAsset, isDuplicating, searchParams]); useEffect(() => { if (workOrder) { setFormData({ company: workOrder.company || '', work_order_type: workOrder.work_order_type || '', asset: workOrder.asset || '', asset_name: isDuplicating ? `${workOrder.asset_name} (Copy)` : (workOrder.asset_name || ''), description: workOrder.description || '', repair_status: isDuplicating ? 'Open' : (workOrder.repair_status || 'Open'), workflow_state: isDuplicating ? 'Draft' : (workOrder.workflow_state || 'Draft'), department: workOrder.department || '', custom_priority_: workOrder.custom_priority_ || 'Normal', asset_type: workOrder.asset_type || '', manufacturer: workOrder.manufacturer || '', supplier: workOrder.supplier || '', serial_number: workOrder.serial_number || '', custom_local_id: workOrder.custom_local_id || '', custom_moh_id: workOrder.custom_moh_id || '', model: workOrder.model || '', custom_site_contractor: workOrder.custom_site_contractor || '', custom_subcontractor: workOrder.custom_subcontractor || '', failure_date: formatDateForInput(workOrder.failure_date) || '', custom_deadline_date: formatDateForInput(workOrder.custom_deadline_date) || '', first_responded_on: formatDatetimeForInput(workOrder.first_responded_on) || '', completion_date: formatDatetimeForInput(workOrder.completion_date) || '', actions_performed: workOrder.actions_performed || '', stock_consumption: workOrder.stock_consumption || 0, stock_items: workOrder.stock_items || [], // Fields for workflow conditions site_name: workOrder.site_name || '', need_procurement: workOrder.need_procurement || 0, custom_assign_to_contractor: workOrder.custom_assign_to_contractor || '', docstatus: workOrder.docstatus || 0, // New fields custom_assigned_supervisor: workOrder.custom_assigned_supervisor || '', custom_moh_supervisor: workOrder.custom_moh_supervisor || '', total_hours_spent: workOrder.total_hours_spent || 0, custom_pending_reason: workOrder.custom_pending_reason || '', total_repair_cost: workOrder.total_repair_cost || 0, // Service Agreement fields custom_service_agreement: workOrder.custom_service_agreement || '', custom_service_coverage: workOrder.custom_service_coverage || '', custom_start_date: formatDateForInput(workOrder.custom_start_date) || '', custom_end_date: formatDateForInput(workOrder.custom_end_date) || '', custom_total_amount: workOrder.custom_total_amount || 0, }); } }, [workOrder, isDuplicating]); const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); }; // Handler for priority change - also recalculates deadline const handlePriorityChange = (e: React.ChangeEvent) => { const newPriority = e.target.value; setFormData(prev => ({ ...prev, custom_priority_: newPriority })); }; // Handler for failure date change - also recalculates deadline const handleFailureDateChange = (e: React.ChangeEvent) => { const newFailureDate = e.target.value; setFormData(prev => ({ ...prev, failure_date: newFailureDate })); }; // Handler for need procurement checkbox change - also recalculates deadline const handleNeedProcurementChange = (e: React.ChangeEvent) => { const newValue = e.target.checked ? 1 : 0; setFormData(prev => ({ ...prev, need_procurement: newValue })); }; // Handler for asset type change - clears fields when Non Biomedical is selected const handleAssetTypeChange = (val: string) => { if (val === 'Non Biomedical') { // Clear asset-related fields when Non Biomedical is selected setFormData(prev => ({ ...prev, asset_type: val, asset: '', asset_name: '', serial_number: '', custom_local_id: '', custom_moh_id: '', manufacturer: '', supplier: '', model: '', })); } else { setFormData(prev => ({ ...prev, asset_type: val })); } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!formData.work_order_type) { toast.error('Please select a Work Order Type', { position: "top-right", autoClose: 4000, icon: }); return; } try { const savePayload = applyWorkOrderStatusTimestamps({ ...formData, first_responded_on: formatDatetimeForApi(formData.first_responded_on), completion_date: formatDatetimeForApi(formData.completion_date), }); if (isNewWorkOrder || isDuplicating) { const newWorkOrder = await createWorkOrder(savePayload); const successMessage = isDuplicating ? 'Work order duplicated successfully!' : isCreatingFromAsset ? 'Work order created from asset successfully!' : 'Work order created successfully!'; toast.success(successMessage, { position: "top-right", autoClose: 3000, icon: }); navigate(`/work-orders/${newWorkOrder.name}`); } else if (workOrderName) { await updateWorkOrder(workOrderName, savePayload); toast.success('Work order updated successfully!', { position: "top-right", autoClose: 3000, icon: }); setIsEditing(false); refetch(); } } catch (err) { console.error('Work order save error:', err); const errorMessage = err instanceof Error ? err.message : 'Unknown error'; const errorString = JSON.stringify(err); // Check if it's a timestamp mismatch error const isTimestampError = errorMessage.includes('TimestampMismatchError') || errorMessage.includes('Document has been modified') || errorMessage.includes('Please refresh') || errorString.includes('TimestampMismatchError'); if (isTimestampError) { toast.error('Document was modified by another user. Refreshing...', { position: "top-right", autoClose: 4000, icon: }); // Refresh the document and retry await refetch(); toast.info('Please review the latest changes and try saving again.', { position: "top-right", autoClose: 5000, icon: }); } else { toast.error(`Failed to save work order: ${errorMessage}`, { position: "top-right", autoClose: 6000, icon: }); } } }; // Function to call assign_supervisor_or_technician API before workflow action // const callBeforeWorkflowAction = async (action: string): Promise<{ assigned_to: string | null } | null> => { // if (!workOrderName || isNewWorkOrder) return null; // // Only call for specific actions that need assignment // const actionsNeedingAssignment = ['Apply', 'Send For Repair']; // if (!actionsNeedingAssignment.includes(action)) { // return null; // } // try { // const response = await apiService.apiCall( // `/api/method/assign_supervisor_or_technician`, // { // method: 'POST', // body: JSON.stringify({ // work_order: workOrderName, // action: action, // asset_type: formData.asset_type || workOrder?.asset_type || '' // }) // } // ); // return response?.message || null; // } catch (err) { // console.error('Error in before_workflow_action:', err); // // Don't block workflow action if this fails, just log it // return null; // } // }; // Workflow action handler const handleWorkflowAction = async (action: string, nextState?: string) => { const actionsRequiringConfirmation = ['Reject', 'Cancel', 'Close']; if (actionsRequiringConfirmation.includes(action) && confirmAction?.action !== action) { setConfirmAction({ action, nextState: nextState || '' }); return; } setConfirmAction(null); // Show loading toast const loadingToastId = toast.loading(`Applying action "${action}"...`, { position: "top-right" }); // NOTE: Supervisor/Technician assignment is now handled when Asset is selected const success = await applyAction(action, nextState); // Dismiss loading toast toast.dismiss(loadingToastId); if (success) { toast.success(`Action "${action}" completed successfully!`, { position: "top-right", autoClose: 3000, icon: }); refetch(); // Refresh work order data to get updated assignments } else { const errorMsg = workflowError || 'Please try again.'; const isTimestampError = errorMsg.includes('TimestampMismatchError') || errorMsg.includes('Document has been modified') || errorMsg.includes('Please refresh'); if (isTimestampError) { toast.error('Document was modified. Refreshing...', { position: "top-right", autoClose: 4000, icon: }); await refetch(); toast.info('Please try the action again after reviewing the latest changes.', { position: "top-right", autoClose: 5000, icon: }); } else { toast.error(`Failed to apply action "${action}". ${errorMsg}`, { position: "top-right", autoClose: 6000, icon: }); } } }; if (loading) { return ( Loading work order details... ); } if (error && !isNewWorkOrder && !isDuplicating) { return ( Error: {error} navigate('/work-orders')} className="mt-2 text-red-700 dark:text-red-400 underline hover:text-red-800 dark:hover:text-red-300" > Back to work orders list ); } const getPageTitle = () => { if (isDuplicating) return 'Duplicate Work Order'; if (isCreatingFromAsset) return 'Create Work Order from Asset'; if (isNewWorkOrder) return 'New Work Order'; return 'Work Order Details'; }; const currentWorkflowState = workOrder?.workflow_state || formData.workflow_state || 'Draft'; const stateStyle = getStateStyle(currentWorkflowState); // Check if editing is allowed based on workflow // const canEditBasedOnWorkflow = isNewWorkOrder || (!workflowLoading && transitions.length > 0); // Check if editing is allowed based on workflow and roles const currentDocstatus = workOrder?.docstatus ?? formData.docstatus ?? 0; const currentState = workOrder?.workflow_state || formData.workflow_state || 'Draft'; const canEditBasedOnWorkflow = isNewWorkOrder || (isSystemManager && currentDocstatus === 0) || workflowCanEdit; return ( {/* Toast Container for notifications */} {/* Header */} navigate('/work-orders')} className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 flex items-center gap-2" > {getPageTitle()} {/* Workflow State Badge */} {!isNewWorkOrder && ( {currentWorkflowState} )} {isCreatingFromAsset && ( Linked from Asset: {assetIdFromParams} )} {isLoadingAsset && ( Loading asset details... )} {/* Service Report Print Button */} {!isNewWorkOrder && ( Service Report )} {!isNewWorkOrder && !isEditing && canEditBasedOnWorkflow && ( { setIsEditing(true); toast.info('Edit mode enabled', { position: "top-right", autoClose: 2000, icon: }); }} className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg flex items-center gap-2" > Edit )} {isEditing && ( <> { if (isNewWorkOrder) { navigate('/work-orders'); } else { setIsEditing(false); toast.info('Edit cancelled - changes discarded', { position: "top-right", autoClose: 2000, icon: }); } }} className="bg-gray-300 hover:bg-gray-400 text-gray-700 px-6 py-2 rounded-lg" disabled={saving} > Cancel {saving ? 'Saving...' : 'Save Changes'} > )} {/* Asset Link Info Banner */} {isCreatingFromAsset && isNewWorkOrder && ( Creating Work Order from Asset Asset information has been pre-filled from {formData.asset_name || assetIdFromParams}. Please select a Work Order Type and add any additional details. )} {/* Left Column - Main Info */} {/* Asset Information Section */} Asset Information {isCreatingFromAsset && ( From Asset )} {/* First Row: Hospital, Asset Type, Site Name (Mobile/Dental hospitals) */} { setFormData({ ...formData, company: val, department: '', site_name: '', custom_assign_to_contractor: '', custom_moh_supervisor: '', custom_assigned_supervisor: '', }); }} disabled={!isEditing} filters={{ domain: 'Healthcare' }} /> {shouldShowSiteNameField && ( setFormData({ ...formData, site_name: val })} disabled={!isEditing || !formData.company || siteLocked} filters={mobileTeamSiteFilters} placeholder="Select Site" /> )} {/* Other Asset fields - Hidden for Non Biomedical */} {!isNonBiomedical && ( <> {formData.asset && ( ✓ Asset details auto-populated )} Serial Number {isEditing && ( )} Local ID MOH ID Asset Name setFormData({ ...formData, manufacturer: val })} disabled={!isEditing} /> setFormData({ ...formData, supplier: val })} disabled={!isEditing} /> Model > )} {/* Work Order Information */} Work Order Information Work Order ID setFormData({ ...formData, work_order_type: val })} disabled={!isEditing} /> Priority Normal Urgent Status Pending Reason Select Pending Reason Need Part Waiting For Quotation Waiting For PO Waiting For Part Delivery {/* Need Procurement Checkbox */} Need Procurement {/* Service Agreement Section - Hidden for Non Biomedical */} {!isNonBiomedical && ( Service Agreement Details Site Contractor Subcontractor Service Agreement Select Service Agreement Warranty Contract Frame Work Main Contractor Out of warranty Under Dismantle Under Installation Service Coverage Select Service Coverage PM Only Labour Labour & Parts Comprehensive Start Date End Date Total Amount )} {/* Description Section */} Description Nature of Complaint Work Performed {/* Location & Assignment */} Location & Assignment setFormData({ ...formData, custom_moh_supervisor: val })} disabled={!isEditing || !formData.company} filters={mohSupervisorFilters} placeholder={formData.company ? 'Select MOH Supervisor' : 'Select Hospital first'} /> setFormData({ ...formData, custom_assigned_supervisor: val })} disabled={!isEditing || !formData.company} filters={teamLeaderFilters} placeholder={formData.company ? 'Select Team Leader' : 'Select Hospital first'} /> setFormData({ ...formData, custom_assign_to_contractor: val })} disabled={!isEditing || !formData.company} filters={assignedTechnicianFilters} placeholder={formData.company ? 'Select Technician' : 'Select Hospital first'} /> setFormData({ ...formData, department: val })} disabled={!isEditing} filters={departmentFilters} /> Failure Date Deadline Date (Auto-calculated) {isEditing && formData.failure_date && ( {formData.custom_priority_ === 'Urgent' ? 'Urgent' : 'Normal'} priority {formData.need_procurement === 1 ? ' + Procurement' : ''}: {' '}+{formData.need_procurement === 1 ? 30 : (formData.custom_priority_ === 'Urgent' ? 1 : 5)} days )} Total Hours Spent (Calculated from response to completion) First Responded On (Auto-set on repair start) Completion Date & Time (Auto-set on Complete) {/* Stock Consumption Details Section */} Stock Consumption Details {/* Stock Consumed Checkbox */} { setFormData({ ...formData, stock_consumption: e.target.checked ? 1 : 0, stock_items: e.target.checked ? (formData.stock_items?.length ? formData.stock_items : [{ item_code: '', warehouse: DEFAULT_WAREHOUSE, consumed_quantity: 1, valuation_rate: 0, custom_available_stock: 0, total_value: 0 }]) : [] }); }} disabled={!isEditing} className="w-5 h-5 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600 disabled:opacity-50" /> Parts Consumed Check this if spare parts or items were used during the repair {/* Stock Items Table - Only shown when checkbox is checked */} {formData.stock_consumption === 1 && ( Stock Items {isEditing && ( { setFormData({ ...formData, stock_items: [ ...(formData.stock_items || []), { item_code: '', warehouse: DEFAULT_WAREHOUSE, consumed_quantity: 1, valuation_rate: 0, custom_available_stock: 0, total_value: 0 } ] }); toast.info('New stock item row added', { position: "top-right", autoClose: 2000, icon: }); }} className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-green-600 hover:bg-green-700 rounded-md transition-colors" > Add Item )} {/* Table */} # Item * Valuation Rate Warehouse * Consumed Qty Available Stock Total Value {isEditing && ( Action )} {(formData.stock_items || []).length === 0 ? ( No items added yet {isEditing && ( { setFormData({ ...formData, stock_items: [{ item_code: '', warehouse: DEFAULT_WAREHOUSE, consumed_quantity: 1, valuation_rate: 0, custom_available_stock: 0, total_value: 0 }] }); }} className="mt-2 text-blue-600 dark:text-blue-400 hover:underline text-sm" > + Add first item )} ) : ( (formData.stock_items || []).map((item, index) => ( {index + 1} handleItemCodeChange(index, val)} disabled={!isEditing} placeholder="Select Item" compact={true} usePortal={true} filters={{ is_stock_item: 1, ...(formData.company ? { custom_hospital_name: formData.company } : {}) }} /> { const updatedItems = [...(formData.stock_items || [])]; const rate = parseFloat(e.target.value) || 0; const qty = updatedItems[index].consumed_quantity || 0; updatedItems[index] = { ...updatedItems[index], valuation_rate: rate, total_value: rate * qty }; setFormData({ ...formData, stock_items: updatedItems }); }} disabled={!isEditing} className="w-full px-2 py-1.5 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" /> handleWarehouseChange(index, val)} disabled={!isEditing} placeholder="Select Warehouse" compact={true} usePortal={true} /> handleConsumedQtyChange(index, parseInt(e.target.value) || 1)} disabled={!isEditing} className={`w-full px-2 py-1.5 text-sm border 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 ${stockWarnings[index] ? 'border-red-500 dark:border-red-500' : 'border-gray-300 dark:border-gray-600'}`} /> {isEditing && ( { const deletedItem = formData.stock_items?.[index]; const updatedItems = (formData.stock_items || []).filter((_, i) => i !== index); setFormData({ ...formData, stock_items: updatedItems }); // Clear warning for deleted row setStockWarnings(prev => { const newWarnings = { ...prev }; delete newWarnings[index]; return newWarnings; }); toast.warning(`Stock item ${deletedItem?.item_code || 'row'} removed`, { position: "top-right", autoClose: 2000, icon: }); }} className="p-1.5 text-red-500 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors" title="Remove item" > )} {/* Stock Warning Row */} {stockWarnings[index] && ( {stockWarnings[index]} )} )) )} {/* Summary */} {(formData.stock_items || []).length > 0 && ( Total Items: {(formData.stock_items || []).length} Total Qty: {(formData.stock_items || []).reduce((sum, item) => sum + (item.consumed_quantity || 0), 0)} Total Value: {(formData.stock_items || []).reduce((sum, item) => sum + (item.total_value || 0), 0).toFixed(2)} )} )} {/* Total Repair Cost field - below stock items table */} Total Repair Cost {/* Right Column - Status & Workflow */} {/* Workflow Actions Card */} {!isNewWorkOrder && ( Workflow Actions {/* Current State */} Current State {currentWorkflowState} {/* Workflow Loading */} {workflowLoading && ( Loading actions... )} {/* Workflow Error */} {workflowError && ( {workflowError} )} {/* Confirmation Dialog */} {confirmAction && ( Confirm Action Are you sure you want to {confirmAction.action} this work order? handleWorkflowAction(confirmAction.action, confirmAction.nextState)} disabled={actionLoading} className="px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white text-sm rounded-md disabled:opacity-50" > {actionLoading ? ( Processing... ) : ( `Yes, ${confirmAction.action}` )} setConfirmAction(null)} disabled={actionLoading} className="px-3 py-1.5 bg-gray-300 hover:bg-gray-400 text-gray-700 text-sm rounded-md disabled:opacity-50" > Cancel )} {/* Available Actions */} {!workflowLoading && transitions.length > 0 && !confirmAction && ( {isSystemManager && ( System Manager: Showing all valid actions for this state (conditions evaluated). )} Available Actions ({transitions.length}) {transitions.map((transition: WorkflowTransition, index: number) => ( handleWorkflowAction(transition.action, transition.next_state)} disabled={actionLoading} className={`w-full px-4 py-2.5 rounded-lg text-sm font-medium transition-colors disabled:opacity-50 flex items-center justify-center gap-2 ${getButtonStyle(transition.action)}`} > {actionLoading ? ( ) : ( {getIcon(transition.action)} )} {transition.action} ))} {/* Show next states */} Action Results: {transitions.map((t: WorkflowTransition, i: number) => ( {t.action} → {t.next_state} ))} )} {/* No Actions Available */} {!workflowLoading && transitions.length === 0 && ( No workflow actions available (Conditions may not be met for available transitions) )} )} {/* WO Feedback — read-only summary when submitted (submit via public link only) */} {!isNewWorkOrder && ['Approved', 'Closed'].includes(currentWorkflowState) && workOrderName && } {/* Status Summary Card */} Status Summary {!isNewWorkOrder && workOrder && ( Repair Status {workOrder.repair_status || 'Open'} Priority {workOrder.custom_priority_ || 'Normal'} Created {workOrder.creation ? new Date(workOrder.creation).toLocaleString() : '-'} Last Modified {workOrder.modified ? new Date(workOrder.modified).toLocaleString() : '-'} )} {isNewWorkOrder && ( Status information will appear after creation )} {!isNewWorkOrder && !isDuplicating && ( )} {!isNewWorkOrder && !isDuplicating && ( )} {/* Styles for table scrolling */} ); }; export default WorkOrderDetail;
Loading work order details...
Error: {error}
Asset information has been pre-filled from {formData.asset_name || assetIdFromParams}. Please select a Work Order Type and add any additional details.
✓ Asset details auto-populated
{formData.custom_priority_ === 'Urgent' ? 'Urgent' : 'Normal'} priority {formData.need_procurement === 1 ? ' + Procurement' : ''}: {' '}+{formData.need_procurement === 1 ? 30 : (formData.custom_priority_ === 'Urgent' ? 1 : 5)} days
Check this if spare parts or items were used during the repair
No items added yet
Current State
{currentWorkflowState}
{workflowError}
Confirm Action
Are you sure you want to {confirmAction.action} this work order?
System Manager: Showing all valid actions for this state (conditions evaluated).
Available Actions ({transitions.length})
Action Results:
{t.action} → {t.next_state}
No workflow actions available
(Conditions may not be met for available transitions)
Repair Status
{workOrder.repair_status || 'Open'}
Priority
{workOrder.custom_priority_ || 'Normal'}
Created
{workOrder.creation ? new Date(workOrder.creation).toLocaleString() : '-'}
Last Modified
{workOrder.modified ? new Date(workOrder.modified).toLocaleString() : '-'}
Status information will appear after creation