Updated WF

This commit is contained in:
Duradundi 2026-09-11 14:08:18 +03:00
parent 67adb8ac4d
commit 683c4cd20a
10 changed files with 288 additions and 62 deletions

View File

@ -2,8 +2,8 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/seera-logo.png?v=1785757497" /> <link rel="icon" type="image/png" href="/seera-logo.png?v=1789058360" />
<link rel="apple-touch-icon" href="/seera-logo.png?v=1785757497" /> <link rel="apple-touch-icon" href="/seera-logo.png?v=1789058360" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Seera Arabia Asset Management System" /> <meta name="description" content="Seera Arabia Asset Management System" />
<title>Seera Arabia - Asset Management System</title> <title>Seera Arabia - Asset Management System</title>

View File

@ -70,10 +70,10 @@ const Sidebar: React.FC<SidebarProps> = ({ userEmail }) => {
// Version is automatically updated by build script based on file modification time // Version is automatically updated by build script based on file modification time
const imageVersion = import.meta.env.DEV const imageVersion = import.meta.env.DEV
? `?v=${Date.now()}` ? `?v=${Date.now()}`
: `?v=1785757497`; // Auto-updated by build script : `?v=1789058360`; // Auto-updated by build script
const logoVersion = import.meta.env.DEV const logoVersion = import.meta.env.DEV
? `?v=${Date.now()}` ? `?v=${Date.now()}`
: `?v=1785757497`; // Auto-updated by build script : `?v=1789058360`; // Auto-updated by build script
const backgroundImageUrl = baseUrl.endsWith('/') const backgroundImageUrl = baseUrl.endsWith('/')
? `${baseUrl}sidebar-background.jpg${imageVersion}` ? `${baseUrl}sidebar-background.jpg${imageVersion}`
: `${baseUrl}/sidebar-background.jpg${imageVersion}`; : `${baseUrl}/sidebar-background.jpg${imageVersion}`;

View File

@ -7,7 +7,7 @@ export const DEFAULT_HOSPITAL = 'Assir Central Hospital';
export const DEFAULT_WAREHOUSE = 'Stores - AGH'; export const DEFAULT_WAREHOUSE = 'Stores - AGH';
// Fallback Frappe Backend Base URL // Fallback Frappe Backend Base URL
export const DEFAULT_FRAPPE_BASE_URL = 'https://asir-ch.seeraarabia.com'; export const DEFAULT_FRAPPE_BASE_URL = 'http://34.166.33.197';
// Default logos // Default logos
export const DEFAULT_HOSPITAL_LOGO = '/files/hospital.png'; export const DEFAULT_HOSPITAL_LOGO = '/files/hospital.png';

View File

@ -25,14 +25,18 @@ export const useDocTypeMeta = (doctype: string) => {
try { try {
setLoading(true); setLoading(true);
// getdoctype returns the merged meta, so Custom Fields (custom_*) and
// Property Setters are included. /api/resource/DocType only returns the
// standard fields, which makes every custom field look like
// allow_on_submit = 0.
const response = await apiService.apiCall<any>( const response = await apiService.apiCall<any>(
`/api/resource/DocType/${doctype}` `/api/method/frappe.desk.form.load.getdoctype?doctype=${encodeURIComponent(doctype)}`
); );
// Handle different response structures from Frappe API // Handle different response structures from Frappe API
// Response can be: { data: {...} } or directly {...} // Response can be: { docs: [{...}] }, { data: {...} } or directly {...}
const docTypeData = response.data || response; const docTypeData = response?.docs?.[0] || response?.data || response;
const fieldsList: DocTypeField[] = docTypeData.fields || []; const fieldsList: DocTypeField[] = docTypeData?.fields || [];
// Extract fields that allow editing on submit // Extract fields that allow editing on submit
const allowOnSubmitSet = new Set<string>(); const allowOnSubmitSet = new Set<string>();

View File

@ -44,6 +44,8 @@ const AssetDetail: React.FC = () => {
asset_category: '', asset_category: '',
custom_moh_id: '', custom_moh_id: '',
custom_floor: '', custom_floor: '',
custom_building: '',
custom_room_number: '',
custom_moh_engg: '', custom_moh_engg: '',
custom_smeh_engg: '', custom_smeh_engg: '',
custom_manufacturing_year: '', custom_manufacturing_year: '',
@ -501,6 +503,8 @@ const AssetDetail: React.FC = () => {
asset_category: asset.asset_category || '', asset_category: asset.asset_category || '',
custom_moh_id: (asset as any).custom_moh_id || '', custom_moh_id: (asset as any).custom_moh_id || '',
custom_floor: (asset as any).custom_floor || '', custom_floor: (asset as any).custom_floor || '',
custom_building: (asset as any).custom_building || '',
custom_room_number: (asset as any).custom_room_number || '',
custom_moh_engg: (asset as any).custom_moh_engg || '', custom_moh_engg: (asset as any).custom_moh_engg || '',
custom_smeh_engg: (asset as any).custom_smeh_engg || '', custom_smeh_engg: (asset as any).custom_smeh_engg || '',
custom_manufacturing_year: asset.custom_manufacturing_year || '', custom_manufacturing_year: asset.custom_manufacturing_year || '',
@ -2908,32 +2912,23 @@ const handlePPMPlan = async () => {
/> />
</div> </div>
<div> <LinkField
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1"> label="Floor"
Floor doctype="Floor"
</label>
<input
type="text"
name="custom_floor"
value={formData.custom_floor || ''} value={formData.custom_floor || ''}
onChange={handleChange} onChange={(val) => setFormData({ ...formData, custom_floor: val })}
placeholder="Floor"
disabled={isFieldDisabled('custom_floor')} disabled={isFieldDisabled('custom_floor')}
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" placeholder="Select Floor"
/> />
</div>
<div> <LinkField
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1"> label="Building"
Building doctype="Building"
</label> value={formData.custom_building || ''}
<input onChange={(val) => setFormData({ ...formData, custom_building: val })}
type="text" disabled={isFieldDisabled('custom_building')}
placeholder="Building name" placeholder="Select Building"
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>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1"> <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
@ -2947,17 +2942,14 @@ const handlePPMPlan = async () => {
/> />
</div> */} </div> */}
<div> <LinkField
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1"> label="Room Number"
Room Number doctype="Room"
</label> value={formData.custom_room_number || ''}
<input onChange={(val) => setFormData({ ...formData, custom_room_number: val })}
type="text" disabled={isFieldDisabled('custom_room_number')}
placeholder="e.g. Room 001-002" placeholder="Select Room"
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>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1"> <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">

View File

@ -861,6 +861,9 @@ const AssetList: React.FC = () => {
if (asset.custom_service_coverage) params.append('custom_service_coverage', asset.custom_service_coverage); if (asset.custom_service_coverage) params.append('custom_service_coverage', asset.custom_service_coverage);
if (asset.custom_site) params.append('site_name', asset.custom_site); if (asset.custom_site) params.append('site_name', asset.custom_site);
if (asset.custom_smeh_engg) params.append('smeh_engg', asset.custom_smeh_engg); if (asset.custom_smeh_engg) params.append('smeh_engg', asset.custom_smeh_engg);
if (asset.custom_building) params.append('building', asset.custom_building);
if (asset.custom_room_number) params.append('room_no', asset.custom_room_number);
if (asset.location) params.append('location', asset.location);
navigate(`/work-orders/new?${params.toString()}`); navigate(`/work-orders/new?${params.toString()}`);
}; };

View File

@ -27,6 +27,10 @@ import {
formatDatetimeForApi, formatDatetimeForApi,
formatDatetimeForInput, formatDatetimeForInput,
} from '../utils/workOrderTimingUtils'; } from '../utils/workOrderTimingUtils';
import {
getWorkOrderWorkflowHints,
WORK_ORDER_CONFIRM_ACTIONS,
} from '../utils/workOrderWorkflowUtils';
// Print Format Configuration // Print Format Configuration
const PRINT_FORMAT_NAME = 'Service Report'; // Change this if your print format has a different name const PRINT_FORMAT_NAME = 'Service Report'; // Change this if your print format has a different name
@ -120,6 +124,10 @@ const WorkOrderDetail: React.FC = () => {
custom_start_date?: string; custom_start_date?: string;
custom_end_date?: string; custom_end_date?: string;
custom_total_amount?: number; custom_total_amount?: number;
// Asset location fields, fetched from the selected asset
custom_building?: string;
custom_room_no?: string;
custom_location?: string;
}>({ }>({
company: '', company: '',
work_order_type: isNewWorkOrder && !isDuplicating ? 'Repair (CM)' : '', work_order_type: isNewWorkOrder && !isDuplicating ? 'Repair (CM)' : '',
@ -163,6 +171,10 @@ const WorkOrderDetail: React.FC = () => {
custom_start_date: '', custom_start_date: '',
custom_end_date: '', custom_end_date: '',
custom_total_amount: 0, custom_total_amount: 0,
// Asset location fields
custom_building: '',
custom_room_no: '',
custom_location: '',
// For Frappe field behavior evaluation // For Frappe field behavior evaluation
__islocal: false, __islocal: false,
}); });
@ -288,7 +300,11 @@ const WorkOrderDetail: React.FC = () => {
return isFieldReadOnlyFromFrappe(fieldname); return isFieldReadOnlyFromFrappe(fieldname);
}, [isEditing, isFieldReadOnlyFromFrappe]); }, [isEditing, isFieldReadOnlyFromFrappe]);
// Document data for workflow condition evaluation // Current workflow state (backend field: workflow_state)
const currentWorkflowState =
workOrder?.workflow_state || formData.workflow_state || 'Draft';
// Document data for workflow condition evaluation (matches backend transition conditions)
const workOrderDocData = useMemo(() => { const workOrderDocData = useMemo(() => {
if (isNewWorkOrder) return undefined; if (isNewWorkOrder) return undefined;
@ -297,6 +313,7 @@ const WorkOrderDetail: React.FC = () => {
: (workOrder || formData); : (workOrder || formData);
return { return {
workflow_state: docSource.workflow_state || currentWorkflowState,
asset_type: docSource.asset_type || '', asset_type: docSource.asset_type || '',
site_name: docSource.site_name || '', site_name: docSource.site_name || '',
need_procurement: docSource.need_procurement ?? 0, need_procurement: docSource.need_procurement ?? 0,
@ -305,11 +322,13 @@ const WorkOrderDetail: React.FC = () => {
company: docSource.company || '', company: docSource.company || '',
department: docSource.department || '', department: docSource.department || '',
repair_status: docSource.repair_status || '', repair_status: docSource.repair_status || '',
actions_performed: docSource.actions_performed || '',
}; };
}, [ }, [
isNewWorkOrder, isNewWorkOrder,
isEditing, isEditing,
workOrder, workOrder,
currentWorkflowState,
formData.asset_type, formData.asset_type,
formData.site_name, formData.site_name,
formData.need_procurement, formData.need_procurement,
@ -318,9 +337,18 @@ const WorkOrderDetail: React.FC = () => {
formData.company, formData.company,
formData.department, formData.department,
formData.repair_status, formData.repair_status,
formData.actions_performed,
formData.workflow_state,
]); ]);
// Workflow hook - now with docData for condition evaluation const workflowHints = useMemo(() => {
if (isNewWorkOrder || !workOrderDocData) return [];
return getWorkOrderWorkflowHints(currentWorkflowState, workOrderDocData, {
isEditing,
});
}, [isNewWorkOrder, currentWorkflowState, workOrderDocData, isEditing]);
// Workflow hook — loads transitions/states from backend Workflow "Work Order"
const { const {
transitions, transitions,
loading: workflowLoading, loading: workflowLoading,
@ -332,12 +360,13 @@ const WorkOrderDetail: React.FC = () => {
getStateStyle, getStateStyle,
getButtonStyle, getButtonStyle,
getIcon, getIcon,
refreshTransitions,
} = useWorkflow({ } = useWorkflow({
doctype: 'Work_Order', doctype: 'Work_Order',
docname: isNewWorkOrder ? null : workOrderName || null, docname: isNewWorkOrder ? null : workOrderName || null,
workflowState: workOrder?.workflow_state, workflowState: isNewWorkOrder ? undefined : currentWorkflowState,
enabled: !isNewWorkOrder, enabled: !isNewWorkOrder,
docData: workOrderDocData, // Pass document data for condition evaluation docData: workOrderDocData,
}); });
// Stock warnings state // Stock warnings state
@ -558,6 +587,9 @@ const WorkOrderDetail: React.FC = () => {
'custom_team_leader', 'custom_team_leader',
'custom_moh_supervisor', 'custom_moh_supervisor',
'custom_smeh_engg', 'custom_smeh_engg',
'custom_building',
'custom_room_number',
'location',
] as const; ] as const;
type AssetLookupField = 'custom_serial_number' | 'custom_local_id' | 'custom_moh_id'; type AssetLookupField = 'custom_serial_number' | 'custom_local_id' | 'custom_moh_id';
@ -669,6 +701,10 @@ const WorkOrderDetail: React.FC = () => {
custom_assigned_supervisor: assetData.custom_team_leader || '', custom_assigned_supervisor: assetData.custom_team_leader || '',
custom_moh_supervisor: assetData.custom_moh_supervisor || '', custom_moh_supervisor: assetData.custom_moh_supervisor || '',
custom_assign_to_contractor: assetData.custom_smeh_engg || '', custom_assign_to_contractor: assetData.custom_smeh_engg || '',
// Asset location: Room No maps to the asset's Room Number field
custom_building: assetData.custom_building || '',
custom_room_no: assetData.custom_room_number || '',
custom_location: assetData.location || '',
})); }));
}; };
@ -704,6 +740,9 @@ const WorkOrderDetail: React.FC = () => {
custom_assigned_supervisor: '', custom_assigned_supervisor: '',
custom_moh_supervisor: '', custom_moh_supervisor: '',
custom_assign_to_contractor: '', custom_assign_to_contractor: '',
custom_building: '',
custom_room_no: '',
custom_location: '',
})); }));
} }
}; };
@ -777,6 +816,9 @@ const WorkOrderDetail: React.FC = () => {
custom_assigned_supervisor: searchParams.get('team_leader') || '', custom_assigned_supervisor: searchParams.get('team_leader') || '',
custom_moh_supervisor: searchParams.get('moh_supervisor') || '', custom_moh_supervisor: searchParams.get('moh_supervisor') || '',
custom_assign_to_contractor: searchParams.get('smeh_engg') || '', custom_assign_to_contractor: searchParams.get('smeh_engg') || '',
custom_building: searchParams.get('building') || '',
custom_room_no: searchParams.get('room_no') || '',
custom_location: searchParams.get('location') || '',
}; };
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
@ -834,6 +876,10 @@ const WorkOrderDetail: React.FC = () => {
custom_start_date: formatDateForInput(workOrder.custom_start_date) || '', custom_start_date: formatDateForInput(workOrder.custom_start_date) || '',
custom_end_date: formatDateForInput(workOrder.custom_end_date) || '', custom_end_date: formatDateForInput(workOrder.custom_end_date) || '',
custom_total_amount: workOrder.custom_total_amount || 0, custom_total_amount: workOrder.custom_total_amount || 0,
// Asset location fields
custom_building: workOrder.custom_building || '',
custom_room_no: workOrder.custom_room_no || '',
custom_location: workOrder.custom_location || '',
}); });
} }
}, [workOrder, isDuplicating]); }, [workOrder, isDuplicating]);
@ -920,7 +966,8 @@ const WorkOrderDetail: React.FC = () => {
icon: <FaCheckCircle /> icon: <FaCheckCircle />
}); });
setIsEditing(false); setIsEditing(false);
refetch(); await refetch();
await refreshTransitions();
} }
} catch (err) { } catch (err) {
console.error('Work order save error:', err); console.error('Work order save error:', err);
@ -990,9 +1037,10 @@ const WorkOrderDetail: React.FC = () => {
// Workflow action handler // Workflow action handler
const handleWorkflowAction = async (action: string, nextState?: string) => { const handleWorkflowAction = async (action: string, nextState?: string) => {
const actionsRequiringConfirmation = ['Reject', 'Cancel', 'Close']; if (
WORK_ORDER_CONFIRM_ACTIONS.includes(action as (typeof WORK_ORDER_CONFIRM_ACTIONS)[number]) &&
if (actionsRequiringConfirmation.includes(action) && confirmAction?.action !== action) { confirmAction?.action !== action
) {
setConfirmAction({ action, nextState: nextState || '' }); setConfirmAction({ action, nextState: nextState || '' });
return; return;
} }
@ -1017,7 +1065,9 @@ const WorkOrderDetail: React.FC = () => {
autoClose: 3000, autoClose: 3000,
icon: <FaCheckCircle /> icon: <FaCheckCircle />
}); });
refetch(); // Refresh work order data to get updated assignments setIsEditing(false);
await refetch();
await refreshTransitions();
} else { } else {
const errorMsg = workflowError || 'Please try again.'; const errorMsg = workflowError || 'Please try again.';
const isTimestampError = errorMsg.includes('TimestampMismatchError') || const isTimestampError = errorMsg.includes('TimestampMismatchError') ||
@ -1080,7 +1130,6 @@ const WorkOrderDetail: React.FC = () => {
return 'Work Order Details'; return 'Work Order Details';
}; };
const currentWorkflowState = workOrder?.workflow_state || formData.workflow_state || 'Draft';
const stateStyle = getStateStyle(currentWorkflowState); const stateStyle = getStateStyle(currentWorkflowState);
// Check if editing is allowed based on workflow // Check if editing is allowed based on workflow
@ -1088,7 +1137,6 @@ const WorkOrderDetail: React.FC = () => {
// Check if editing is allowed based on workflow and roles // Check if editing is allowed based on workflow and roles
const currentDocstatus = workOrder?.docstatus ?? formData.docstatus ?? 0; const currentDocstatus = workOrder?.docstatus ?? formData.docstatus ?? 0;
const currentState = workOrder?.workflow_state || formData.workflow_state || 'Draft';
const canEditBasedOnWorkflow = const canEditBasedOnWorkflow =
isNewWorkOrder || isNewWorkOrder ||
@ -1448,6 +1496,39 @@ const canEditBasedOnWorkflow =
</div> </div>
</> </>
)} )}
<div>
<LinkField
label="Building"
doctype="Building"
value={formData.custom_building || ''}
onChange={(val) => setFormData({ ...formData, custom_building: val })}
disabled={!isEditing}
placeholder="Select Building"
/>
</div>
<div>
<LinkField
label="Room No"
doctype="Room"
value={formData.custom_room_no || ''}
onChange={(val) => setFormData({ ...formData, custom_room_no: val })}
disabled={!isEditing}
placeholder="Select Room"
/>
</div>
<div>
<LinkField
label="Location"
doctype="Location"
value={formData.custom_location || ''}
onChange={(val) => setFormData({ ...formData, custom_location: val })}
disabled={!isEditing}
placeholder="Select Location"
/>
</div>
</div> </div>
</div> </div>
@ -1495,7 +1576,19 @@ const canEditBasedOnWorkflow =
<div> <div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1"> <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
Status Workflow State
</label>
<input
type="text"
value={currentWorkflowState}
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">
Repair Status
</label> </label>
<input <input
type="text" type="text"
@ -1503,6 +1596,9 @@ const canEditBasedOnWorkflow =
disabled 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" 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"
/> />
<p className="mt-1 text-[10px] text-gray-400 dark:text-gray-500">
Updated by workflow (e.g. Open → Work In Progress → Completed → Closed)
</p>
</div> </div>
<div> <div>
@ -1538,6 +1634,11 @@ const canEditBasedOnWorkflow =
Need Procurement Need Procurement
</span> </span>
</label> </label>
{currentWorkflowState === 'Repair InProgress' && (
<p className="mt-1 text-[10px] text-amber-600 dark:text-amber-400 ml-7">
Check and save to enable Material Request → Pending Purchase
</p>
)}
</div> </div>
</div> </div>
</div> </div>
@ -1698,6 +1799,11 @@ const canEditBasedOnWorkflow =
rows={4} 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" 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"
/> />
{currentWorkflowState === 'Repair InProgress' && (
<p className="mt-1 text-[10px] text-amber-600 dark:text-amber-400">
Required for Work Done (with Assigned Technician set)
</p>
)}
</div> </div>
</div> </div>
</div> </div>
@ -1747,6 +1853,11 @@ const canEditBasedOnWorkflow =
filters={assignedTechnicianFilters} filters={assignedTechnicianFilters}
placeholder={formData.company ? 'Select Technician' : 'Select Hospital first'} placeholder={formData.company ? 'Select Technician' : 'Select Hospital first'}
/> />
{['Sent to Team Leader', 'Repair InProgress', 'Completed'].includes(currentWorkflowState) && (
<p className="mt-1 text-[10px] text-amber-600 dark:text-amber-400">
Required for Send For Repair, Work Done, and Approve &amp; Close workflow actions
</p>
)}
</div> </div>
<div> <div>
@ -2289,12 +2400,25 @@ const canEditBasedOnWorkflow =
{/* No Actions Available */} {/* No Actions Available */}
{!workflowLoading && transitions.length === 0 && ( {!workflowLoading && transitions.length === 0 && (
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg"> <div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg space-y-2">
<p className="text-sm text-gray-500 dark:text-gray-400 text-center"> <p className="text-sm text-gray-500 dark:text-gray-400 text-center">
No workflow actions available No workflow actions available for your role or current document state
</p> </p>
<p className="text-xs text-gray-400 dark:text-gray-500 text-center mt-1"> {workflowHints.length > 0 && (
(Conditions may not be met for available transitions) <ul className="text-xs text-gray-600 dark:text-gray-300 space-y-1 list-disc list-inside">
{workflowHints.map((hint) => (
<li key={hint}>{hint}</li>
))}
</ul>
)}
</div>
)}
{/* Hints while actions are visible (save reminder when editing) */}
{!workflowLoading && transitions.length > 0 && isEditing && (
<div className="p-2 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<p className="text-xs text-blue-700 dark:text-blue-300">
Save after editing so workflow conditions match the server before applying an action.
</p> </p>
</div> </div>
)} )}
@ -2303,7 +2427,7 @@ const canEditBasedOnWorkflow =
{/* WO Feedback — read-only summary when submitted (submit via public link only) */} {/* WO Feedback — read-only summary when submitted (submit via public link only) */}
{!isNewWorkOrder && {!isNewWorkOrder &&
['Approved', 'Closed'].includes(currentWorkflowState) && ['Completed', 'Closed'].includes(currentWorkflowState) &&
workOrderName && <WoFeedbackSummary workOrder={workOrderName} />} workOrderName && <WoFeedbackSummary workOrder={workOrderName} />}
{/* Status Summary Card */} {/* Status Summary Card */}

View File

@ -18,6 +18,8 @@ export interface Asset {
asset_category?: string; asset_category?: string;
custom_moh_id?: string; custom_moh_id?: string;
custom_floor?: string; custom_floor?: string;
custom_building?: string;
custom_room_number?: string;
custom_moh_engg?: string; custom_moh_engg?: string;
custom_smeh_engg?: string; custom_smeh_engg?: string;
custom_manufacturing_year?: string; custom_manufacturing_year?: string;
@ -127,6 +129,8 @@ export interface CreateAssetData {
asset_category?: string; asset_category?: string;
custom_moh_id?: string; custom_moh_id?: string;
custom_floor?: string; custom_floor?: string;
custom_building?: string;
custom_room_number?: string;
custom_moh_engg?: string; custom_moh_engg?: string;
custom_smeh_engg?: string; custom_smeh_engg?: string;
custom_manufacturing_year?: string; custom_manufacturing_year?: string;

View File

@ -68,6 +68,9 @@ export interface WorkOrder {
stock_items?: StockItem[]; stock_items?: StockItem[];
site_name?: string; site_name?: string;
custom_assign_to_contractor?: string; custom_assign_to_contractor?: string;
custom_building?: string;
custom_room_no?: string;
custom_location?: string;
} }
export interface StockItem { export interface StockItem {

View File

@ -0,0 +1,96 @@
/**
* Helpers for the Work Order workflow (backend: Workflow "Work Order").
* Transitions and allow_edit rules are loaded from Frappe; these hints mirror
* the conditions configured on the workflow.
*/
export interface WorkOrderWorkflowDoc {
workflow_state?: string;
asset_type?: string;
site_name?: string;
custom_assign_to_contractor?: string;
actions_performed?: string;
need_procurement?: number;
}
/** Actions that should prompt for confirmation before applying. */
export const WORK_ORDER_CONFIRM_ACTIONS = [
'Approve & Close',
'Re-Open',
'Reject',
'Cancel',
'Close',
] as const;
/** repair_status values set by workflow state update_field (backend). */
export const WORK_ORDER_REPAIR_STATUS_BY_STATE: Record<string, string> = {
Draft: 'Open',
'Sent to Team Leader': 'Open',
'Sent To Site Manager': 'Open',
'Repair InProgress': 'Work In Progress',
'Pending Approval': 'Pending Review',
Completed: 'Completed',
Cancelled: 'Cancelled',
Closed: 'Closed',
};
/**
* Human-readable hints when no workflow actions are shown (conditions / save).
*/
export function getWorkOrderWorkflowHints(
state: string,
doc: WorkOrderWorkflowDoc,
options?: { isEditing?: boolean }
): string[] {
const hints: string[] = [];
const isEditing = options?.isEditing ?? false;
if (isEditing) {
hints.push('Save the work order after changing fields so workflow actions refresh.');
}
switch (state) {
case 'Draft':
if (doc.asset_type === 'Biomedical' && !doc.site_name) {
hints.push('PHCC End User: set Site Name (Biomedical asset) to enable Apply.');
}
break;
case 'Sent to Team Leader':
if (!doc.custom_assign_to_contractor) {
hints.push(
'Maintenance Manager: set Assigned Technician, save, then use Send For Repair.'
);
}
break;
case 'Repair InProgress':
if (!doc.custom_assign_to_contractor) {
hints.push('Set Assigned Technician and save to enable Work Done.');
} else if (!doc.actions_performed?.trim()) {
hints.push('Fill Actions Performed and save to enable Work Done.');
}
if (doc.need_procurement !== 1) {
hints.push('Check Need Procurement and save to enable Material Request.');
}
break;
case 'Pending Purchase':
hints.push('Finance Manager: use Send For Repair when procurement is complete.');
break;
case 'Completed':
if (!doc.custom_assign_to_contractor) {
hints.push(
'Set Assigned Technician and save to enable Approve & Close (End user / Manager roles).'
);
}
hints.push('Maintenance Manager can Re-Open without a contractor assigned.');
break;
default:
break;
}
return hints;
}