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}

); } 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 */}
{/* Workflow State Badge */} {!isNewWorkOrder && ( {currentWorkflowState} )} {isCreatingFromAsset && ( Linked from Asset: {assetIdFromParams} )} {isLoadingAsset && ( Loading asset details... )}
{/* Service Report Print Button */} {!isNewWorkOrder && ( )} {!isNewWorkOrder && !isEditing && canEditBasedOnWorkflow && ( )} {isEditing && ( <> )}
{/* 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

)}
{isEditing && ( )}
setFormData({ ...formData, manufacturer: val })} disabled={!isEditing} />
setFormData({ ...formData, supplier: val })} disabled={!isEditing} />
)}
{/* Work Order Information */}

Work Order Information

setFormData({ ...formData, work_order_type: val })} disabled={!isEditing} />
{/* Need Procurement Checkbox */}
{/* Service Agreement Section - Hidden for Non Biomedical */} {!isNonBiomedical && (

Service Agreement Details

)} {/* Description Section */}

Description