2296 lines
102 KiB
TypeScript
2296 lines
102 KiB
TypeScript
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: <FaExclamationTriangle />
|
|
});
|
|
}
|
|
};
|
|
|
|
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<CreateWorkOrderData & {
|
|
stock_consumption?: number;
|
|
stock_items?: StockItem[];
|
|
site_name?: string;
|
|
need_procurement?: number;
|
|
custom_assign_to_contractor?: string;
|
|
docstatus?: number;
|
|
// New fields
|
|
custom_assigned_supervisor?: string;
|
|
custom_moh_supervisor?: string;
|
|
total_hours_spent?: number;
|
|
custom_pending_reason?: string;
|
|
total_repair_cost?: number;
|
|
// Service Agreement fields
|
|
custom_service_agreement?: string;
|
|
custom_service_coverage?: string;
|
|
custom_start_date?: string;
|
|
custom_end_date?: string;
|
|
custom_total_amount?: number;
|
|
}>({
|
|
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<string, any>);
|
|
|
|
// 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<Record<number, string>>({});
|
|
|
|
// Function to fetch available stock from Bin doctype
|
|
const fetchAvailableStock = async (itemCode: string, warehouse: string): Promise<number> => {
|
|
if (!itemCode || !warehouse) return 0;
|
|
try {
|
|
const response = await apiService.apiCall<any>(
|
|
`/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<number> => {
|
|
if (!itemCode) return 0;
|
|
try {
|
|
const response = await apiService.apiCall<any>(
|
|
`/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: <FaExclamationTriangle />,
|
|
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<Record<string, any>>({});
|
|
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<string, any> = {};
|
|
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<any>(`/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<any>(
|
|
`/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: <FaExclamationTriangle />
|
|
});
|
|
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: <FaCheckCircle />
|
|
});
|
|
} else {
|
|
toast.error('No asset found with this serial number', {
|
|
position: "top-right",
|
|
autoClose: 4000,
|
|
icon: <FaTimesCircle />
|
|
});
|
|
}
|
|
};
|
|
|
|
// 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<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
|
const { name, value } = e.target;
|
|
setFormData(prev => ({ ...prev, [name]: value }));
|
|
};
|
|
|
|
// Handler for priority change - also recalculates deadline
|
|
const handlePriorityChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
const newPriority = e.target.value;
|
|
setFormData(prev => ({ ...prev, custom_priority_: newPriority }));
|
|
};
|
|
|
|
// Handler for failure date change - also recalculates deadline
|
|
const handleFailureDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
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<HTMLInputElement>) => {
|
|
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: <FaTimesCircle />
|
|
});
|
|
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: <FaCheckCircle />
|
|
});
|
|
navigate(`/work-orders/${newWorkOrder.name}`);
|
|
} else if (workOrderName) {
|
|
await updateWorkOrder(workOrderName, savePayload);
|
|
toast.success('Work order updated successfully!', {
|
|
position: "top-right",
|
|
autoClose: 3000,
|
|
icon: <FaCheckCircle />
|
|
});
|
|
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: <FaExclamationTriangle />
|
|
});
|
|
// Refresh the document and retry
|
|
await refetch();
|
|
toast.info('Please review the latest changes and try saving again.', {
|
|
position: "top-right",
|
|
autoClose: 5000,
|
|
icon: <FaInfoCircle />
|
|
});
|
|
} else {
|
|
toast.error(`Failed to save work order: ${errorMessage}`, {
|
|
position: "top-right",
|
|
autoClose: 6000,
|
|
icon: <FaTimesCircle />
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
// 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<any>(
|
|
// `/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: <FaCheckCircle />
|
|
});
|
|
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: <FaExclamationTriangle />
|
|
});
|
|
await refetch();
|
|
toast.info('Please try the action again after reviewing the latest changes.', {
|
|
position: "top-right",
|
|
autoClose: 5000,
|
|
icon: <FaInfoCircle />
|
|
});
|
|
} else {
|
|
toast.error(`Failed to apply action "${action}". ${errorMsg}`, {
|
|
position: "top-right",
|
|
autoClose: 6000,
|
|
icon: <FaTimesCircle />
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
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 work order details...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error && !isNewWorkOrder && !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('/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
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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 (
|
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-4 sm:p-6 min-w-0 overflow-x-hidden">
|
|
{/* Toast Container for notifications */}
|
|
<ToastContainer
|
|
position="top-right"
|
|
autoClose={4000}
|
|
hideProgressBar={false}
|
|
newestOnTop
|
|
closeOnClick
|
|
rtl={false}
|
|
pauseOnFocusLoss
|
|
draggable
|
|
pauseOnHover
|
|
theme="colored"
|
|
transition={Bounce}
|
|
/>
|
|
|
|
{/* 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('/work-orders')}
|
|
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">{getPageTitle()}</span>
|
|
</button>
|
|
|
|
{/* Workflow State Badge */}
|
|
{!isNewWorkOrder && (
|
|
<span className={`px-3 py-1 rounded-full text-xs font-medium ${stateStyle.bg} ${stateStyle.text} ${stateStyle.border} border`}>
|
|
{currentWorkflowState}
|
|
</span>
|
|
)}
|
|
|
|
{isCreatingFromAsset && (
|
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 rounded-full text-xs font-medium">
|
|
<FaLink size={10} />
|
|
Linked from Asset: {assetIdFromParams}
|
|
</span>
|
|
)}
|
|
|
|
{isLoadingAsset && (
|
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded-full text-xs font-medium">
|
|
<FaSpinner className="animate-spin" size={12} />
|
|
Loading asset details...
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
{/* Service Report Print Button */}
|
|
{!isNewWorkOrder && (
|
|
<button
|
|
onClick={handlePrintServiceReport}
|
|
className="bg-purple-600 hover:bg-purple-700 text-white px-6 py-2 rounded-lg flex items-center gap-2"
|
|
title="Print Service Report"
|
|
>
|
|
<FaPrint />
|
|
Service Report
|
|
</button>
|
|
)}
|
|
|
|
{!isNewWorkOrder && !isEditing && canEditBasedOnWorkflow && (
|
|
<button
|
|
onClick={() => {
|
|
setIsEditing(true);
|
|
toast.info('Edit mode enabled', {
|
|
position: "top-right",
|
|
autoClose: 2000,
|
|
icon: <FaEdit />
|
|
});
|
|
}}
|
|
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 (isNewWorkOrder) {
|
|
navigate('/work-orders');
|
|
} else {
|
|
setIsEditing(false);
|
|
toast.info('Edit cancelled - changes discarded', {
|
|
position: "top-right",
|
|
autoClose: 2000,
|
|
icon: <FaTimesCircle />
|
|
});
|
|
}
|
|
}}
|
|
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 || isLoadingAsset}
|
|
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>
|
|
|
|
{/* Asset Link Info Banner */}
|
|
{isCreatingFromAsset && isNewWorkOrder && (
|
|
<div className="mb-6 bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-4">
|
|
<div className="flex items-start gap-3">
|
|
<FaLink className="text-orange-500 mt-0.5" />
|
|
<div>
|
|
<h3 className="text-sm font-semibold text-orange-800 dark:text-orange-300">
|
|
Creating Work Order from Asset
|
|
</h3>
|
|
<p className="text-xs text-orange-700 dark:text-orange-400 mt-1">
|
|
Asset information has been pre-filled from <strong>{formData.asset_name || assetIdFromParams}</strong>.
|
|
Please select a Work Order Type and add any additional details.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6" style={{ overflow: 'visible' }}>
|
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6" style={{ overflow: 'visible' }}>
|
|
{/* Left Column - Main Info */}
|
|
<div className="lg:col-span-3 space-y-6" style={{ overflow: 'visible' }}>
|
|
|
|
{/* Asset Information Section */}
|
|
<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 items-center justify-between 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">
|
|
Asset Information
|
|
</h2>
|
|
{isCreatingFromAsset && (
|
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400 rounded text-[10px] font-medium">
|
|
<FaLink size={8} />
|
|
From Asset
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{/* First Row: Hospital, Asset Type, Site Name (Mobile/Dental hospitals) */}
|
|
<div>
|
|
<LinkField
|
|
label="Hospital"
|
|
doctype="Company"
|
|
value={formData.company || ''}
|
|
onChange={(val) => {
|
|
setFormData({
|
|
...formData,
|
|
company: val,
|
|
department: '',
|
|
site_name: '',
|
|
custom_assign_to_contractor: '',
|
|
custom_moh_supervisor: '',
|
|
custom_assigned_supervisor: '',
|
|
});
|
|
}}
|
|
disabled={!isEditing}
|
|
filters={{ domain: 'Healthcare' }}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<LinkField
|
|
label="Asset Type"
|
|
doctype="Asset Type"
|
|
value={formData.asset_type || ''}
|
|
onChange={handleAssetTypeChange}
|
|
disabled={!isEditing}
|
|
/>
|
|
</div>
|
|
|
|
{shouldShowSiteNameField && (
|
|
<div>
|
|
<LinkField
|
|
key={formData.company || 'site-name'}
|
|
label="Site Name"
|
|
doctype="Mobile Team Site"
|
|
value={formData.site_name || ''}
|
|
onChange={(val) => setFormData({ ...formData, site_name: val })}
|
|
disabled={!isEditing || !formData.company || siteLocked}
|
|
filters={mobileTeamSiteFilters}
|
|
placeholder="Select Site"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Other Asset fields - Hidden for Non Biomedical */}
|
|
{!isNonBiomedical && (
|
|
<>
|
|
<div className="relative z-[50]">
|
|
<LinkField
|
|
label="Asset ID"
|
|
doctype="Asset"
|
|
value={formData.asset || ''}
|
|
onChange={handleAssetIdChange}
|
|
disabled={!isEditing || isLoadingAsset}
|
|
filters={formData.company ? { company: formData.company } : {}}
|
|
/>
|
|
{formData.asset && (
|
|
<p className="mt-1 text-xs text-green-600 dark:text-green-400">
|
|
✓ Asset details auto-populated
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Serial Number
|
|
</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
name="serial_number"
|
|
value={formData.serial_number}
|
|
onChange={handleChange}
|
|
onBlur={handleSerialNumberBlur}
|
|
disabled={!isEditing || isLoadingAsset}
|
|
placeholder="Enter serial number"
|
|
className="flex-1 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"
|
|
/>
|
|
{isEditing && (
|
|
<button
|
|
type="button"
|
|
onClick={handleSerialNumberSearch}
|
|
disabled={isLoadingAsset || !formData.serial_number}
|
|
className="px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
|
title="Search asset by serial number"
|
|
>
|
|
<FaSearch size={14} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Local ID
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="custom_local_id"
|
|
value={formData.custom_local_id || ''}
|
|
onChange={handleChange}
|
|
disabled={!isEditing || isLoadingAsset}
|
|
placeholder="Local 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">
|
|
MOH ID
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="custom_moh_id"
|
|
value={formData.custom_moh_id || ''}
|
|
onChange={handleChange}
|
|
disabled={!isEditing || isLoadingAsset}
|
|
placeholder="MOH 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 Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="asset_name"
|
|
value={formData.asset_name}
|
|
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>
|
|
<LinkField
|
|
label="Manufacturer"
|
|
doctype="Manufacturer"
|
|
value={formData.manufacturer || ''}
|
|
onChange={(val) => setFormData({ ...formData, manufacturer: val })}
|
|
disabled={!isEditing}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<LinkField
|
|
label="Supplier"
|
|
doctype="Supplier"
|
|
value={formData.supplier || ''}
|
|
onChange={(val) => setFormData({ ...formData, supplier: val })}
|
|
disabled={!isEditing}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Model
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="model"
|
|
value={formData.model}
|
|
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>
|
|
</div>
|
|
|
|
{/* Work Order 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">Work Order Information</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Work Order ID
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={isNewWorkOrder || isDuplicating ? 'Auto-generated' : workOrder?.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-500 dark:text-gray-400"
|
|
/>
|
|
</div>
|
|
|
|
<div className="relative z-[50]">
|
|
<LinkField
|
|
label="Work Order Type"
|
|
doctype="Issue Type"
|
|
value={formData.work_order_type || ''}
|
|
onChange={(val) => setFormData({ ...formData, work_order_type: val })}
|
|
disabled={!isEditing}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Priority
|
|
</label>
|
|
<select
|
|
name="custom_priority_"
|
|
value={formData.custom_priority_}
|
|
onChange={handlePriorityChange}
|
|
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="Normal">Normal</option>
|
|
<option value="Urgent">Urgent</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Status
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.repair_status}
|
|
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-500 dark:text-gray-400"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Pending Reason
|
|
</label>
|
|
<select
|
|
name="custom_pending_reason"
|
|
value={formData.custom_pending_reason || ''}
|
|
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 Pending Reason</option>
|
|
<option value="Need Part">Need Part</option>
|
|
<option value="Waiting For Quotation">Waiting For Quotation</option>
|
|
<option value="Waiting For PO">Waiting For PO</option>
|
|
<option value="Waiting For Part Delivery">Waiting For Part Delivery</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Need Procurement Checkbox */}
|
|
<div className="flex items-center">
|
|
<label className="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData.need_procurement === 1}
|
|
onChange={handleNeedProcurementChange}
|
|
disabled={!isEditing}
|
|
className="w-4 h-4 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"
|
|
/>
|
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
Need Procurement
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Service Agreement Section - Hidden for Non Biomedical */}
|
|
{!isNonBiomedical && (
|
|
<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">
|
|
Service Agreement Details
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Site Contractor
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="custom_site_contractor"
|
|
value={formData.custom_site_contractor}
|
|
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">
|
|
Subcontractor
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="custom_subcontractor"
|
|
value={formData.custom_subcontractor}
|
|
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">
|
|
Service Agreement
|
|
</label>
|
|
<select
|
|
name="custom_service_agreement"
|
|
value={formData.custom_service_agreement || ''}
|
|
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 Service Agreement</option>
|
|
<option value="Warranty">Warranty</option>
|
|
<option value="Contract">Contract</option>
|
|
<option value="Frame Work">Frame Work</option>
|
|
<option value="Main Contractor">Main Contractor</option>
|
|
<option value="Out of warranty">Out of warranty</option>
|
|
<option value="Under Dismantle">Under Dismantle</option>
|
|
<option value="Under Installation">Under Installation</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Service Coverage
|
|
</label>
|
|
<select
|
|
name="custom_service_coverage"
|
|
value={formData.custom_service_coverage || ''}
|
|
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 Service Coverage</option>
|
|
<option value="PM Only">PM Only</option>
|
|
<option value="Labour">Labour</option>
|
|
<option value="Labour & Parts">Labour & Parts</option>
|
|
<option value="Comprehensive">Comprehensive</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Start Date
|
|
</label>
|
|
<input
|
|
type="date"
|
|
name="custom_start_date"
|
|
value={formData.custom_start_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">
|
|
End Date
|
|
</label>
|
|
<input
|
|
type="date"
|
|
name="custom_end_date"
|
|
value={formData.custom_end_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">
|
|
Total Amount
|
|
</label>
|
|
<input
|
|
type="number"
|
|
name="custom_total_amount"
|
|
min="0"
|
|
step="0.01"
|
|
value={formData.custom_total_amount || 0}
|
|
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>
|
|
</div>
|
|
)}
|
|
|
|
{/* Description 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">
|
|
Description
|
|
</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">
|
|
Nature of Complaint
|
|
</label>
|
|
<textarea
|
|
name="description"
|
|
value={formData.description}
|
|
onChange={handleChange}
|
|
disabled={!isEditing}
|
|
placeholder="Describe the nature of complaint..."
|
|
rows={4}
|
|
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 resize-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Work Performed
|
|
</label>
|
|
<textarea
|
|
name="actions_performed"
|
|
value={formData.actions_performed}
|
|
onChange={handleChange}
|
|
disabled={!isEditing}
|
|
placeholder="Describe the work performed..."
|
|
rows={4}
|
|
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 resize-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Location & Assignment */}
|
|
<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">Location & Assignment</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
|
|
<div>
|
|
<LinkField
|
|
key={`moh-supervisor-${formData.company || 'none'}`}
|
|
label="MOH Supervisor"
|
|
doctype="User"
|
|
query="frappe.core.doctype.user.user.user_query"
|
|
value={formData.custom_moh_supervisor || ''}
|
|
onChange={(val) => setFormData({ ...formData, custom_moh_supervisor: val })}
|
|
disabled={!isEditing || !formData.company}
|
|
filters={mohSupervisorFilters}
|
|
placeholder={formData.company ? 'Select MOH Supervisor' : 'Select Hospital first'}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<LinkField
|
|
key={`team-leader-${formData.company || 'none'}`}
|
|
label="Team Leader"
|
|
doctype="User"
|
|
query="frappe.core.doctype.user.user.user_query"
|
|
value={formData.custom_assigned_supervisor || ''}
|
|
onChange={(val) => setFormData({ ...formData, custom_assigned_supervisor: val })}
|
|
disabled={!isEditing || !formData.company}
|
|
filters={teamLeaderFilters}
|
|
placeholder={formData.company ? 'Select Team Leader' : 'Select Hospital first'}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<LinkField
|
|
key={`assigned-technician-${formData.company || 'none'}`}
|
|
label="Assigned Technician"
|
|
doctype="User"
|
|
query="frappe.core.doctype.user.user.user_query"
|
|
value={formData.custom_assign_to_contractor || ''}
|
|
onChange={(val) => setFormData({ ...formData, custom_assign_to_contractor: val })}
|
|
disabled={!isEditing || !formData.company}
|
|
filters={assignedTechnicianFilters}
|
|
placeholder={formData.company ? 'Select Technician' : 'Select Hospital first'}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<LinkField
|
|
label="Department"
|
|
doctype="Department"
|
|
value={formData.department || ''}
|
|
onChange={(val) => setFormData({ ...formData, department: val })}
|
|
disabled={!isEditing}
|
|
filters={departmentFilters}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Failure Date
|
|
</label>
|
|
<input
|
|
type="date"
|
|
name="failure_date"
|
|
value={formData.failure_date}
|
|
onChange={handleFailureDateChange}
|
|
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">
|
|
Deadline Date
|
|
<span className="ml-1 text-xs text-gray-400">(Auto-calculated)</span>
|
|
</label>
|
|
<input
|
|
type="date"
|
|
name="custom_deadline_date"
|
|
value={formData.custom_deadline_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"
|
|
/>
|
|
{isEditing && formData.failure_date && (
|
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
{formData.custom_priority_ === 'Urgent' ? 'Urgent' : 'Normal'} priority
|
|
{formData.need_procurement === 1 ? ' + Procurement' : ''}:
|
|
{' '}+{formData.need_procurement === 1 ? 30 : (formData.custom_priority_ === 'Urgent' ? 1 : 5)} days
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Total Hours Spent
|
|
<span className="ml-1 text-xs text-gray-400">(Calculated from response to completion)</span>
|
|
</label>
|
|
<input
|
|
type="number"
|
|
name="total_hours_spent"
|
|
min="0"
|
|
step="0.01"
|
|
value={formData.total_hours_spent || 0}
|
|
readOnly
|
|
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">
|
|
First Responded On
|
|
<span className="ml-1 text-xs text-gray-400">(Auto-set on repair start)</span>
|
|
</label>
|
|
<input
|
|
type="datetime-local"
|
|
name="first_responded_on"
|
|
value={formData.first_responded_on}
|
|
readOnly
|
|
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">
|
|
Completion Date & Time
|
|
<span className="ml-1 text-xs text-gray-400">(Auto-set on Complete)</span>
|
|
</label>
|
|
<input
|
|
type="datetime-local"
|
|
name="completion_date"
|
|
value={formData.completion_date}
|
|
readOnly
|
|
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>
|
|
</div>
|
|
|
|
{/* Stock Consumption Details Section */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700 relative z-20" style={{ overflow: 'visible' }}>
|
|
<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">
|
|
Stock Consumption Details
|
|
</h2>
|
|
|
|
{/* Stock Consumed Checkbox */}
|
|
<div className="mb-4">
|
|
<label className="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData.stock_consumption === 1}
|
|
onChange={(e) => {
|
|
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"
|
|
/>
|
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
Parts Consumed
|
|
</span>
|
|
</label>
|
|
<p className="mt-1 ml-8 text-xs text-gray-500 dark:text-gray-400">
|
|
Check this if spare parts or items were used during the repair
|
|
</p>
|
|
</div>
|
|
|
|
{/* Stock Items Table - Only shown when checkbox is checked */}
|
|
{formData.stock_consumption === 1 && (
|
|
<div className="mt-4">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
Stock Items
|
|
</h3>
|
|
{isEditing && (
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
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: <FaPlus />
|
|
});
|
|
}}
|
|
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"
|
|
>
|
|
<FaPlus size={10} />
|
|
Add Item
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Table */}
|
|
<div className="stock-items-table-wrapper">
|
|
<div className="stock-items-scroll-container">
|
|
<table className="w-full text-sm border border-gray-200 dark:border-gray-700 rounded-lg">
|
|
<thead className="bg-gray-50 dark:bg-gray-700">
|
|
<tr>
|
|
<th className="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-10">
|
|
#
|
|
</th>
|
|
<th className="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider min-w-[180px]">
|
|
Item <span className="text-red-500">*</span>
|
|
</th>
|
|
<th className="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-28">
|
|
Valuation Rate
|
|
</th>
|
|
<th className="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider min-w-[180px]">
|
|
Warehouse <span className="text-red-500">*</span>
|
|
</th>
|
|
<th className="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-24">
|
|
Consumed Qty
|
|
</th>
|
|
<th className="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-28">
|
|
Available Stock
|
|
</th>
|
|
<th className="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-28">
|
|
Total Value
|
|
</th>
|
|
{isEditing && (
|
|
<th className="px-3 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-16">
|
|
Action
|
|
</th>
|
|
)}
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
{(formData.stock_items || []).length === 0 ? (
|
|
<tr>
|
|
<td colSpan={isEditing ? 8 : 7} className="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
|
<p>No items added yet</p>
|
|
{isEditing && (
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
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
|
|
</button>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
(formData.stock_items || []).map((item, index) => (
|
|
<React.Fragment key={index}>
|
|
<tr className={`hover:bg-gray-50 dark:hover:bg-gray-700/50 ${stockWarnings[index] ? 'bg-red-50 dark:bg-red-900/20' : ''}`}>
|
|
<td className="px-3 py-3 text-gray-500 dark:text-gray-400">
|
|
{index + 1}
|
|
</td>
|
|
<td className="px-3 py-3 relative" style={{ zIndex: 50 - index }}>
|
|
<div className="relative">
|
|
<LinkField
|
|
label=""
|
|
doctype="Item"
|
|
value={item.item_code}
|
|
onChange={(val) => handleItemCodeChange(index, val)}
|
|
disabled={!isEditing}
|
|
placeholder="Select Item"
|
|
compact={true}
|
|
usePortal={true}
|
|
filters={{
|
|
is_stock_item: 1,
|
|
...(formData.company ? { custom_hospital_name: formData.company } : {})
|
|
}}
|
|
/>
|
|
</div>
|
|
</td>
|
|
<td className="px-3 py-3">
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
step="0.01"
|
|
value={item.valuation_rate || 0}
|
|
onChange={(e) => {
|
|
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"
|
|
/>
|
|
</td>
|
|
<td className="px-3 py-3 relative" style={{ zIndex: 50 - index }}>
|
|
<div className="relative">
|
|
<LinkField
|
|
label=""
|
|
doctype="Warehouse"
|
|
value={item.warehouse}
|
|
onChange={(val) => handleWarehouseChange(index, val)}
|
|
disabled={!isEditing}
|
|
placeholder="Select Warehouse"
|
|
compact={true}
|
|
usePortal={true}
|
|
/>
|
|
</div>
|
|
</td>
|
|
<td className="px-3 py-3">
|
|
<input
|
|
type="number"
|
|
min="1"
|
|
value={item.consumed_quantity || 1}
|
|
onChange={(e) => 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'}`}
|
|
/>
|
|
</td>
|
|
<td className="px-3 py-3">
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={item.custom_available_stock || 0}
|
|
disabled
|
|
className="w-full px-2 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300"
|
|
title="Auto-populated from stock"
|
|
/>
|
|
</td>
|
|
<td className="px-3 py-3">
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
step="0.01"
|
|
value={item.total_value || 0}
|
|
disabled
|
|
className="w-full px-2 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300"
|
|
/>
|
|
</td>
|
|
{isEditing && (
|
|
<td className="px-3 py-3 text-center">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
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: <FaTrash />
|
|
});
|
|
}}
|
|
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"
|
|
>
|
|
<FaTrash size={14} />
|
|
</button>
|
|
</td>
|
|
)}
|
|
</tr>
|
|
{/* Stock Warning Row */}
|
|
{stockWarnings[index] && (
|
|
<tr className="bg-red-50 dark:bg-red-900/30">
|
|
<td colSpan={isEditing ? 8 : 7} className="px-4 py-2">
|
|
<div className="flex items-center gap-2 text-red-600 dark:text-red-400 text-xs">
|
|
<FaExclamationTriangle />
|
|
<span>{stockWarnings[index]}</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</React.Fragment>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Summary */}
|
|
{(formData.stock_items || []).length > 0 && (
|
|
<div className="mt-3 flex flex-wrap justify-between items-center gap-4 text-sm bg-gray-50 dark:bg-gray-700/50 p-3 rounded-lg">
|
|
<span className="text-gray-500 dark:text-gray-400">
|
|
Total Items: <span className="font-medium text-gray-700 dark:text-gray-300">{(formData.stock_items || []).length}</span>
|
|
</span>
|
|
<span className="text-gray-500 dark:text-gray-400">
|
|
Total Qty: <span className="font-medium text-gray-700 dark:text-gray-300">
|
|
{(formData.stock_items || []).reduce((sum, item) => sum + (item.consumed_quantity || 0), 0)}
|
|
</span>
|
|
</span>
|
|
<span className="text-gray-500 dark:text-gray-400">
|
|
Total Value: <span className="font-semibold text-green-600 dark:text-green-400">
|
|
{(formData.stock_items || []).reduce((sum, item) => sum + (item.total_value || 0), 0).toFixed(2)}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Total Repair Cost field - below stock items table */}
|
|
<div className="mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Total Repair Cost
|
|
</label>
|
|
<input
|
|
type="number"
|
|
name="total_repair_cost"
|
|
min="0"
|
|
step="0.01"
|
|
value={formData.total_repair_cost || 0}
|
|
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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
{/* Right Column - Status & Workflow */}
|
|
<div className="space-y-6">
|
|
{/* Workflow Actions Card */}
|
|
{!isNewWorkOrder && (
|
|
<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 items-center justify-between 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">
|
|
Workflow Actions
|
|
</h2>
|
|
</div>
|
|
|
|
{/* Current State */}
|
|
<div className={`p-4 rounded-lg border mb-4 ${stateStyle.bg} ${stateStyle.border}`}>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Current State</p>
|
|
<p className={`text-lg font-semibold ${stateStyle.text}`}>
|
|
{currentWorkflowState}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Workflow Loading */}
|
|
{workflowLoading && (
|
|
<div className="flex items-center gap-2 text-gray-500 dark:text-gray-400 mb-4">
|
|
<FaSpinner className="animate-spin" />
|
|
<span className="text-sm">Loading actions...</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Workflow Error */}
|
|
{workflowError && (
|
|
<div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg mb-4">
|
|
<div className="flex items-start gap-2">
|
|
<FaExclamationTriangle className="text-red-500 mt-0.5" />
|
|
<p className="text-sm text-red-600 dark:text-red-400">{workflowError}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Confirmation Dialog */}
|
|
{confirmAction && (
|
|
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg mb-4">
|
|
<div className="flex items-start gap-2 mb-3">
|
|
<FaExclamationTriangle className="text-yellow-500 mt-0.5" />
|
|
<div>
|
|
<p className="text-sm font-medium text-yellow-800 dark:text-yellow-200">
|
|
Confirm Action
|
|
</p>
|
|
<p className="text-xs text-yellow-600 dark:text-yellow-400 mt-1">
|
|
Are you sure you want to <strong>{confirmAction.action}</strong> this work order?
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => 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 ? (
|
|
<span className="flex items-center gap-1">
|
|
<FaSpinner className="animate-spin" size={12} />
|
|
Processing...
|
|
</span>
|
|
) : (
|
|
`Yes, ${confirmAction.action}`
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={() => 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
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Available Actions */}
|
|
{!workflowLoading && transitions.length > 0 && !confirmAction && (
|
|
<div className="space-y-3">
|
|
{isSystemManager && (
|
|
<div className="p-2 bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg mb-2">
|
|
<p className="text-xs text-purple-700 dark:text-purple-300">
|
|
<strong>System Manager:</strong> Showing all valid actions for this state (conditions evaluated).
|
|
</p>
|
|
</div>
|
|
)}
|
|
<p className="text-xs font-medium text-gray-500 dark:text-gray-400 flex items-center gap-1">
|
|
<FaInfoCircle size={12} />
|
|
Available Actions ({transitions.length})
|
|
</p>
|
|
<div className="flex flex-col gap-2">
|
|
{transitions.map((transition: WorkflowTransition, index: number) => (
|
|
<button
|
|
key={`${transition.action}-${transition.next_state}-${index}`}
|
|
onClick={() => 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 ? (
|
|
<FaSpinner className="animate-spin" size={14} />
|
|
) : (
|
|
<span>{getIcon(transition.action)}</span>
|
|
)}
|
|
{transition.action}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Show next states */}
|
|
<div className="mt-3 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2">Action Results:</p>
|
|
{transitions.map((t: WorkflowTransition, i: number) => (
|
|
<p key={i} className="text-xs text-gray-600 dark:text-gray-300">
|
|
{t.action} → <span className="font-medium">{t.next_state}</span>
|
|
</p>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* No Actions Available */}
|
|
{!workflowLoading && transitions.length === 0 && (
|
|
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
<p className="text-sm text-gray-500 dark:text-gray-400 text-center">
|
|
No workflow actions available
|
|
</p>
|
|
<p className="text-xs text-gray-400 dark:text-gray-500 text-center mt-1">
|
|
(Conditions may not be met for available transitions)
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* WO Feedback — read-only summary when submitted (submit via public link only) */}
|
|
{!isNewWorkOrder &&
|
|
['Approved', 'Closed'].includes(currentWorkflowState) &&
|
|
workOrderName && <WoFeedbackSummary workOrder={workOrderName} />}
|
|
|
|
{/* Status Summary 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>
|
|
|
|
{!isNewWorkOrder && workOrder && (
|
|
<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">Repair Status</p>
|
|
<p className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{workOrder.repair_status || 'Open'}
|
|
</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">Priority</p>
|
|
<p className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{workOrder.custom_priority_ || 'Normal'}
|
|
</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">Created</p>
|
|
<p className="text-sm text-gray-900 dark:text-white">
|
|
{workOrder.creation ? new Date(workOrder.creation).toLocaleString() : '-'}
|
|
</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">Last Modified</p>
|
|
<p className="text-sm text-gray-900 dark:text-white">
|
|
{workOrder.modified ? new Date(workOrder.modified).toLocaleString() : '-'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{isNewWorkOrder && (
|
|
<div className="text-center py-8">
|
|
<FaInfoCircle 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>
|
|
|
|
{!isNewWorkOrder && !isDuplicating && (
|
|
<ActivityLog
|
|
doctype="Work_Order"
|
|
docname={workOrderName || null}
|
|
creationDate={workOrder?.creation}
|
|
createdBy={workOrder?.owner}
|
|
initialVisible={5}
|
|
collapsible={false}
|
|
startCollapsed={false}
|
|
compact={true}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
{!isNewWorkOrder && !isDuplicating && (
|
|
<div className="mt-6 space-y-6 max-w-5xl">
|
|
<CommentSection
|
|
referenceDoctype="Work_Order"
|
|
referenceName={workOrderName || null}
|
|
pollInterval={30000}
|
|
initialLimit={5}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Styles for table scrolling */}
|
|
<style>{`
|
|
/* Wrapper for table - allow natural flow */
|
|
.stock-items-table-wrapper {
|
|
width: 100%;
|
|
position: relative;
|
|
}
|
|
|
|
/* Scroll container handles horizontal scrolling only */
|
|
.stock-items-scroll-container {
|
|
width: 100%;
|
|
overflow-x: auto;
|
|
overflow-y: visible;
|
|
-webkit-overflow-scrolling: touch;
|
|
}
|
|
|
|
/* Table styling */
|
|
.stock-items-scroll-container table {
|
|
border-collapse: separate;
|
|
border-spacing: 0;
|
|
min-width: 100%;
|
|
}
|
|
|
|
.stock-items-scroll-container tbody tr td {
|
|
position: relative;
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default WorkOrderDetail; |