Updated WF
This commit is contained in:
parent
67adb8ac4d
commit
683c4cd20a
@ -2,8 +2,8 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/seera-logo.png?v=1785757497" />
|
||||
<link rel="apple-touch-icon" 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=1789058360" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Seera Arabia Asset Management System" />
|
||||
<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
|
||||
const imageVersion = import.meta.env.DEV
|
||||
? `?v=${Date.now()}`
|
||||
: `?v=1785757497`; // Auto-updated by build script
|
||||
: `?v=1789058360`; // Auto-updated by build script
|
||||
const logoVersion = import.meta.env.DEV
|
||||
? `?v=${Date.now()}`
|
||||
: `?v=1785757497`; // Auto-updated by build script
|
||||
: `?v=1789058360`; // Auto-updated by build script
|
||||
const backgroundImageUrl = baseUrl.endsWith('/')
|
||||
? `${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';
|
||||
|
||||
// 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
|
||||
export const DEFAULT_HOSPITAL_LOGO = '/files/hospital.png';
|
||||
|
||||
@ -25,14 +25,18 @@ export const useDocTypeMeta = (doctype: string) => {
|
||||
|
||||
try {
|
||||
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>(
|
||||
`/api/resource/DocType/${doctype}`
|
||||
`/api/method/frappe.desk.form.load.getdoctype?doctype=${encodeURIComponent(doctype)}`
|
||||
);
|
||||
|
||||
// Handle different response structures from Frappe API
|
||||
// Response can be: { data: {...} } or directly {...}
|
||||
const docTypeData = response.data || response;
|
||||
const fieldsList: DocTypeField[] = docTypeData.fields || [];
|
||||
// Response can be: { docs: [{...}] }, { data: {...} } or directly {...}
|
||||
const docTypeData = response?.docs?.[0] || response?.data || response;
|
||||
const fieldsList: DocTypeField[] = docTypeData?.fields || [];
|
||||
|
||||
// Extract fields that allow editing on submit
|
||||
const allowOnSubmitSet = new Set<string>();
|
||||
|
||||
@ -44,6 +44,8 @@ const AssetDetail: React.FC = () => {
|
||||
asset_category: '',
|
||||
custom_moh_id: '',
|
||||
custom_floor: '',
|
||||
custom_building: '',
|
||||
custom_room_number: '',
|
||||
custom_moh_engg: '',
|
||||
custom_smeh_engg: '',
|
||||
custom_manufacturing_year: '',
|
||||
@ -501,6 +503,8 @@ const AssetDetail: React.FC = () => {
|
||||
asset_category: asset.asset_category || '',
|
||||
custom_moh_id: (asset as any).custom_moh_id || '',
|
||||
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_smeh_engg: (asset as any).custom_smeh_engg || '',
|
||||
custom_manufacturing_year: asset.custom_manufacturing_year || '',
|
||||
@ -2908,32 +2912,23 @@ const handlePPMPlan = async () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Floor
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="custom_floor"
|
||||
value={formData.custom_floor || ''}
|
||||
onChange={handleChange}
|
||||
placeholder="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"
|
||||
/>
|
||||
</div>
|
||||
<LinkField
|
||||
label="Floor"
|
||||
doctype="Floor"
|
||||
value={formData.custom_floor || ''}
|
||||
onChange={(val) => setFormData({ ...formData, custom_floor: val })}
|
||||
disabled={isFieldDisabled('custom_floor')}
|
||||
placeholder="Select Floor"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Building
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Building name"
|
||||
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>
|
||||
<LinkField
|
||||
label="Building"
|
||||
doctype="Building"
|
||||
value={formData.custom_building || ''}
|
||||
onChange={(val) => setFormData({ ...formData, custom_building: val })}
|
||||
disabled={isFieldDisabled('custom_building')}
|
||||
placeholder="Select Building"
|
||||
/>
|
||||
|
||||
{/* <div>
|
||||
<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>
|
||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Room Number
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. Room 001-002"
|
||||
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>
|
||||
<LinkField
|
||||
label="Room Number"
|
||||
doctype="Room"
|
||||
value={formData.custom_room_number || ''}
|
||||
onChange={(val) => setFormData({ ...formData, custom_room_number: val })}
|
||||
disabled={isFieldDisabled('custom_room_number')}
|
||||
placeholder="Select Room"
|
||||
/>
|
||||
|
||||
{/* <div>
|
||||
<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_site) params.append('site_name', asset.custom_site);
|
||||
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()}`);
|
||||
};
|
||||
|
||||
@ -27,6 +27,10 @@ import {
|
||||
formatDatetimeForApi,
|
||||
formatDatetimeForInput,
|
||||
} from '../utils/workOrderTimingUtils';
|
||||
import {
|
||||
getWorkOrderWorkflowHints,
|
||||
WORK_ORDER_CONFIRM_ACTIONS,
|
||||
} from '../utils/workOrderWorkflowUtils';
|
||||
|
||||
// Print Format Configuration
|
||||
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_end_date?: string;
|
||||
custom_total_amount?: number;
|
||||
// Asset location fields, fetched from the selected asset
|
||||
custom_building?: string;
|
||||
custom_room_no?: string;
|
||||
custom_location?: string;
|
||||
}>({
|
||||
company: '',
|
||||
work_order_type: isNewWorkOrder && !isDuplicating ? 'Repair (CM)' : '',
|
||||
@ -163,6 +171,10 @@ const WorkOrderDetail: React.FC = () => {
|
||||
custom_start_date: '',
|
||||
custom_end_date: '',
|
||||
custom_total_amount: 0,
|
||||
// Asset location fields
|
||||
custom_building: '',
|
||||
custom_room_no: '',
|
||||
custom_location: '',
|
||||
// For Frappe field behavior evaluation
|
||||
__islocal: false,
|
||||
});
|
||||
@ -288,7 +300,11 @@ const WorkOrderDetail: React.FC = () => {
|
||||
return isFieldReadOnlyFromFrappe(fieldname);
|
||||
}, [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(() => {
|
||||
if (isNewWorkOrder) return undefined;
|
||||
|
||||
@ -297,6 +313,7 @@ const WorkOrderDetail: React.FC = () => {
|
||||
: (workOrder || formData);
|
||||
|
||||
return {
|
||||
workflow_state: docSource.workflow_state || currentWorkflowState,
|
||||
asset_type: docSource.asset_type || '',
|
||||
site_name: docSource.site_name || '',
|
||||
need_procurement: docSource.need_procurement ?? 0,
|
||||
@ -305,11 +322,13 @@ const WorkOrderDetail: React.FC = () => {
|
||||
company: docSource.company || '',
|
||||
department: docSource.department || '',
|
||||
repair_status: docSource.repair_status || '',
|
||||
actions_performed: docSource.actions_performed || '',
|
||||
};
|
||||
}, [
|
||||
isNewWorkOrder,
|
||||
isEditing,
|
||||
workOrder,
|
||||
currentWorkflowState,
|
||||
formData.asset_type,
|
||||
formData.site_name,
|
||||
formData.need_procurement,
|
||||
@ -318,9 +337,18 @@ const WorkOrderDetail: React.FC = () => {
|
||||
formData.company,
|
||||
formData.department,
|
||||
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 {
|
||||
transitions,
|
||||
loading: workflowLoading,
|
||||
@ -332,12 +360,13 @@ const WorkOrderDetail: React.FC = () => {
|
||||
getStateStyle,
|
||||
getButtonStyle,
|
||||
getIcon,
|
||||
refreshTransitions,
|
||||
} = useWorkflow({
|
||||
doctype: 'Work_Order',
|
||||
docname: isNewWorkOrder ? null : workOrderName || null,
|
||||
workflowState: workOrder?.workflow_state,
|
||||
workflowState: isNewWorkOrder ? undefined : currentWorkflowState,
|
||||
enabled: !isNewWorkOrder,
|
||||
docData: workOrderDocData, // Pass document data for condition evaluation
|
||||
docData: workOrderDocData,
|
||||
});
|
||||
|
||||
// Stock warnings state
|
||||
@ -558,6 +587,9 @@ const WorkOrderDetail: React.FC = () => {
|
||||
'custom_team_leader',
|
||||
'custom_moh_supervisor',
|
||||
'custom_smeh_engg',
|
||||
'custom_building',
|
||||
'custom_room_number',
|
||||
'location',
|
||||
] as const;
|
||||
|
||||
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_moh_supervisor: assetData.custom_moh_supervisor || '',
|
||||
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_moh_supervisor: '',
|
||||
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_moh_supervisor: searchParams.get('moh_supervisor') || '',
|
||||
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 => ({
|
||||
...prev,
|
||||
@ -834,6 +876,10 @@ const WorkOrderDetail: React.FC = () => {
|
||||
custom_start_date: formatDateForInput(workOrder.custom_start_date) || '',
|
||||
custom_end_date: formatDateForInput(workOrder.custom_end_date) || '',
|
||||
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]);
|
||||
@ -920,7 +966,8 @@ const WorkOrderDetail: React.FC = () => {
|
||||
icon: <FaCheckCircle />
|
||||
});
|
||||
setIsEditing(false);
|
||||
refetch();
|
||||
await refetch();
|
||||
await refreshTransitions();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Work order save error:', err);
|
||||
@ -990,9 +1037,10 @@ const WorkOrderDetail: React.FC = () => {
|
||||
|
||||
// Workflow action handler
|
||||
const handleWorkflowAction = async (action: string, nextState?: string) => {
|
||||
const actionsRequiringConfirmation = ['Reject', 'Cancel', 'Close'];
|
||||
|
||||
if (actionsRequiringConfirmation.includes(action) && confirmAction?.action !== action) {
|
||||
if (
|
||||
WORK_ORDER_CONFIRM_ACTIONS.includes(action as (typeof WORK_ORDER_CONFIRM_ACTIONS)[number]) &&
|
||||
confirmAction?.action !== action
|
||||
) {
|
||||
setConfirmAction({ action, nextState: nextState || '' });
|
||||
return;
|
||||
}
|
||||
@ -1017,7 +1065,9 @@ const WorkOrderDetail: React.FC = () => {
|
||||
autoClose: 3000,
|
||||
icon: <FaCheckCircle />
|
||||
});
|
||||
refetch(); // Refresh work order data to get updated assignments
|
||||
setIsEditing(false);
|
||||
await refetch();
|
||||
await refreshTransitions();
|
||||
} else {
|
||||
const errorMsg = workflowError || 'Please try again.';
|
||||
const isTimestampError = errorMsg.includes('TimestampMismatchError') ||
|
||||
@ -1080,7 +1130,6 @@ const WorkOrderDetail: React.FC = () => {
|
||||
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
|
||||
@ -1088,7 +1137,6 @@ const WorkOrderDetail: React.FC = () => {
|
||||
|
||||
// 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 ||
|
||||
@ -1448,6 +1496,39 @@ const canEditBasedOnWorkflow =
|
||||
</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>
|
||||
|
||||
@ -1495,7 +1576,19 @@ const canEditBasedOnWorkflow =
|
||||
|
||||
<div>
|
||||
<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>
|
||||
<input
|
||||
type="text"
|
||||
@ -1503,6 +1596,9 @@ const canEditBasedOnWorkflow =
|
||||
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"
|
||||
/>
|
||||
<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>
|
||||
@ -1538,6 +1634,11 @@ const canEditBasedOnWorkflow =
|
||||
Need Procurement
|
||||
</span>
|
||||
</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>
|
||||
@ -1698,6 +1799,11 @@ const canEditBasedOnWorkflow =
|
||||
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"
|
||||
/>
|
||||
{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>
|
||||
@ -1747,6 +1853,11 @@ const canEditBasedOnWorkflow =
|
||||
filters={assignedTechnicianFilters}
|
||||
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>
|
||||
@ -2289,12 +2400,25 @@ const canEditBasedOnWorkflow =
|
||||
|
||||
{/* No Actions Available */}
|
||||
{!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">
|
||||
No workflow actions available
|
||||
No workflow actions available for your role or current document state
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 dark:text-gray-500 text-center mt-1">
|
||||
(Conditions may not be met for available transitions)
|
||||
{workflowHints.length > 0 && (
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
@ -2303,7 +2427,7 @@ const canEditBasedOnWorkflow =
|
||||
|
||||
{/* WO Feedback — read-only summary when submitted (submit via public link only) */}
|
||||
{!isNewWorkOrder &&
|
||||
['Approved', 'Closed'].includes(currentWorkflowState) &&
|
||||
['Completed', 'Closed'].includes(currentWorkflowState) &&
|
||||
workOrderName && <WoFeedbackSummary workOrder={workOrderName} />}
|
||||
|
||||
{/* Status Summary Card */}
|
||||
|
||||
@ -18,6 +18,8 @@ export interface Asset {
|
||||
asset_category?: string;
|
||||
custom_moh_id?: string;
|
||||
custom_floor?: string;
|
||||
custom_building?: string;
|
||||
custom_room_number?: string;
|
||||
custom_moh_engg?: string;
|
||||
custom_smeh_engg?: string;
|
||||
custom_manufacturing_year?: string;
|
||||
@ -127,6 +129,8 @@ export interface CreateAssetData {
|
||||
asset_category?: string;
|
||||
custom_moh_id?: string;
|
||||
custom_floor?: string;
|
||||
custom_building?: string;
|
||||
custom_room_number?: string;
|
||||
custom_moh_engg?: string;
|
||||
custom_smeh_engg?: string;
|
||||
custom_manufacturing_year?: string;
|
||||
|
||||
@ -68,6 +68,9 @@ export interface WorkOrder {
|
||||
stock_items?: StockItem[];
|
||||
site_name?: string;
|
||||
custom_assign_to_contractor?: string;
|
||||
custom_building?: string;
|
||||
custom_room_no?: string;
|
||||
custom_location?: string;
|
||||
}
|
||||
|
||||
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