Updated WF
This commit is contained in:
parent
67adb8ac4d
commit
683c4cd20a
@ -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>
|
||||||
|
|||||||
@ -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}`;
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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>();
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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()}`);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 & 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 */}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
96
asm_app/src/utils/workOrderWorkflowUtils.ts
Normal file
96
asm_app/src/utils/workOrderWorkflowUtils.ts
Normal 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;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user