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">
<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>

View File

@ -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}`;

View File

@ -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';

View File

@ -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>();

View File

@ -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"
<LinkField
label="Floor"
doctype="Floor"
value={formData.custom_floor || ''}
onChange={handleChange}
placeholder="Floor"
onChange={(val) => setFormData({ ...formData, custom_floor: val })}
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>
<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"
<LinkField
label="Building"
doctype="Building"
value={formData.custom_building || ''}
onChange={(val) => setFormData({ ...formData, custom_building: val })}
disabled={isFieldDisabled('custom_building')}
placeholder="Select Building"
/>
</div>
{/* <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"
<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>
{/* <div>
<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_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()}`);
};

View File

@ -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 &amp; 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 */}

View File

@ -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;

View File

@ -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 {

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;
}