- {labels.map((label: string, i: number) => {
- const barWidth = Math.max(20, (groupWidth - barSpacing * (datasets.length + 1)) / datasets.length);
-
- return (
-
-
- {datasets.map((dataset: any, dsIndex: number) => {
- const value = dataset.values?.[i] || 0;
- const height = (value / max) * chartHeight;
- const color = dataset.color || generateColors(datasets.length)[dsIndex];
-
- return (
-
0 ? '4px' : '0px',
- }}
- >
-
- {value.toLocaleString()}
-
-
- );
- })}
-
-
- {label}
-
-
- );
- })}
+
+
+ {!data || !data.datasets || !data.datasets.length ? (
+
+
+
No chart data available
+ ) : (
+
+ )}
+
+ );
+};
- {/* Legend */}
- {datasets.length > 1 && (
-
- {datasets.map((dataset: any, i: number) => (
-
- ))}
+// Chart Card Component (Compact Version)
+const ChartCard: React.FC<{ title: string; data: any; type: 'bar' | 'pie' }> = ({ title, data, type }) => {
+ return (
+
+
{title}
+ {!data || !data.datasets || !data.datasets.length ? (
+
+
+
No chart data available
- )}
-
+
+ ) : type === 'pie' ? (
+
+ ) : (
+
+ )}
);
};
@@ -359,18 +1269,21 @@ const BarChart: React.FC<{ data: any }> = ({ data }) => {
// Helper: Generate colors
function generateColors(count: number): string[] {
const colors = [
- '#4F46E5', // Indigo
- '#10B981', // Green
- '#F59E0B', // Amber
- '#EF4444', // Red
'#8B5CF6', // Purple
+ '#6366F1', // Indigo
+ '#3B82F6', // Blue
'#06B6D4', // Cyan
- '#F97316', // Orange
- '#EC4899', // Pink
'#14B8A6', // Teal
- '#6366F1', // Violet
+ '#EC4899', // Pink
+ '#A855F7', // Purple variant
+ '#0EA5E9', // Sky blue
+ '#10B981', // Emerald
+ '#F472B6', // Pink variant
+ '#7C3AED', // Violet
+ '#2DD4BF', // Teal variant
];
return Array.from({ length: count }, (_, i) => colors[i % colors.length]);
}
export default ModernDashboard;
+
diff --git a/src/pages/PPMDetail.tsx b/src/pages/PPMDetail.tsx
new file mode 100644
index 0000000..2967ae7
--- /dev/null
+++ b/src/pages/PPMDetail.tsx
@@ -0,0 +1,406 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
+import { usePPMDetails, usePPMMutations } from '../hooks/usePPM';
+import { FaArrowLeft, FaSave, FaEdit, FaBuilding, FaTools, FaCalendarCheck, FaDollarSign } from 'react-icons/fa';
+import type { CreatePPMData } from '../services/ppmService';
+
+const PPMDetail: React.FC = () => {
+ const { ppmName } = useParams<{ ppmName: string }>();
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const duplicateFromPPM = searchParams.get('duplicate');
+
+ const isNewPPM = ppmName === 'new';
+ const isDuplicating = isNewPPM && !!duplicateFromPPM;
+
+ const { ppm, loading, error, refetch } = usePPMDetails(
+ isDuplicating ? duplicateFromPPM : (isNewPPM ? null : ppmName || null)
+ );
+ const { createPPM, updatePPM, loading: saving } = usePPMMutations();
+
+ const [isEditing, setIsEditing] = useState(isNewPPM);
+ const [formData, setFormData] = useState
({
+ company: '',
+ asset_name: '',
+ custom_asset_type: '',
+ maintenance_team: '',
+ custom_frequency: '',
+ custom_total_amount: 0,
+ custom_no_of_pms: 0,
+ custom_price_per_pm: 0,
+ });
+
+ useEffect(() => {
+ if (ppm) {
+ setFormData({
+ company: ppm.company || '',
+ asset_name: ppm.asset_name || '',
+ custom_asset_type: ppm.custom_asset_type || '',
+ maintenance_team: ppm.maintenance_team || '',
+ custom_frequency: ppm.custom_frequency || '',
+ custom_total_amount: ppm.custom_total_amount || 0,
+ custom_no_of_pms: ppm.custom_no_of_pms || 0,
+ custom_price_per_pm: ppm.custom_price_per_pm || 0,
+ });
+ }
+ }, [ppm, isDuplicating]);
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({
+ ...prev,
+ [name]: name.includes('amount') || name.includes('pms') || name.includes('price')
+ ? parseFloat(value) || 0
+ : value
+ }));
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!formData.asset_name) {
+ alert('Please enter Asset Name');
+ return;
+ }
+
+ try {
+ if (isNewPPM || isDuplicating) {
+ const result = await createPPM(formData);
+ const successMessage = isDuplicating
+ ? 'PPM schedule duplicated successfully!'
+ : 'PPM schedule created successfully!';
+ alert(successMessage);
+ if (result.asset_maintenance?.name) {
+ navigate(`/ppm/${result.asset_maintenance.name}`);
+ } else {
+ refetch();
+ navigate('/ppm');
+ }
+ } else if (ppmName) {
+ await updatePPM(ppmName, formData);
+ alert('PPM schedule updated successfully!');
+ setIsEditing(false);
+ refetch();
+ }
+ } catch (err) {
+ console.error('PPM save error:', err);
+ alert('Failed to save: ' + (err instanceof Error ? err.message : 'Unknown error'));
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+
Loading PPM schedule...
+
+
+ );
+ }
+
+ if (error && !isNewPPM && !isDuplicating) {
+ return (
+
+
+
Error: {error}
+
navigate('/ppm')}
+ className="mt-2 text-red-700 dark:text-red-400 underline hover:text-red-800 dark:hover:text-red-300"
+ >
+ Back to PPM schedules
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+ navigate('/ppm')}
+ className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 flex items-center gap-2"
+ >
+
+
+ {isDuplicating ? 'Duplicate PPM Schedule' : (isNewPPM ? 'New PPM Schedule' : 'PPM Schedule Details')}
+
+
+
+
+ {!isNewPPM && !isEditing && (
+ setIsEditing(true)}
+ className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2"
+ >
+
+ Edit
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default PPMDetail;
+
diff --git a/src/pages/PPMList.tsx b/src/pages/PPMList.tsx
new file mode 100644
index 0000000..46608ca
--- /dev/null
+++ b/src/pages/PPMList.tsx
@@ -0,0 +1,441 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { usePPMs, usePPMMutations } from '../hooks/usePPM';
+import { FaPlus, FaSearch, FaEdit, FaEye, FaTrash, FaCopy, FaEllipsisV, FaFileExport, FaCalendarCheck, FaTools, FaBuilding } from 'react-icons/fa';
+
+const PPMList: React.FC = () => {
+ const navigate = useNavigate();
+ const [page, setPage] = useState(0);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [companyFilter, setCompanyFilter] = useState('');
+ const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(null);
+ const [actionMenuOpen, setActionMenuOpen] = useState(null);
+ const dropdownRef = useRef(null);
+ const limit = 20;
+
+ const filters = companyFilter ? { company: companyFilter } : {};
+
+ const { ppms, totalCount, hasMore, loading, error, refetch } = usePPMs(
+ filters,
+ limit,
+ page * limit,
+ 'creation desc'
+ );
+
+ const { deletePPM, loading: mutationLoading } = usePPMMutations();
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
+ setActionMenuOpen(null);
+ }
+ };
+
+ if (actionMenuOpen) {
+ document.addEventListener('mousedown', handleClickOutside);
+ }
+
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [actionMenuOpen]);
+
+ const handleCreateNew = () => {
+ navigate('/ppm/new');
+ };
+
+ const handleView = (ppmName: string) => {
+ navigate(`/ppm/${ppmName}`);
+ };
+
+ const handleEdit = (ppmName: string) => {
+ navigate(`/ppm/${ppmName}`);
+ };
+
+ const handleDelete = async (ppmName: string) => {
+ try {
+ await deletePPM(ppmName);
+ setDeleteConfirmOpen(null);
+ refetch();
+ alert('PPM schedule deleted successfully!');
+ } catch (err) {
+ alert(`Failed to delete: ${err instanceof Error ? err.message : 'Unknown error'}`);
+ }
+ };
+
+ const handleDuplicate = (ppmName: string) => {
+ navigate(`/ppm/new?duplicate=${ppmName}`);
+ };
+
+ const handleExport = (ppm: any) => {
+ const dataStr = JSON.stringify(ppm, null, 2);
+ const dataBlob = new Blob([dataStr], { type: 'application/json' });
+ const url = URL.createObjectURL(dataBlob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `ppm_${ppm.name}.json`;
+ link.click();
+ URL.revokeObjectURL(url);
+ };
+
+ const handleExportAll = () => {
+ const headers = ['PPM ID', 'Company', 'Asset', 'Asset Type', 'Frequency', 'No. of PMs', 'Total Amount'];
+ const csvContent = [
+ headers.join(','),
+ ...ppms.map(ppm => [
+ ppm.name,
+ ppm.company || '',
+ ppm.asset_name || '',
+ ppm.custom_asset_type || '',
+ ppm.custom_frequency || '',
+ ppm.custom_no_of_pms || '',
+ ppm.custom_total_amount || ''
+ ].join(','))
+ ].join('\n');
+
+ const dataBlob = new Blob([csvContent], { type: 'text/csv' });
+ const url = URL.createObjectURL(dataBlob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `ppm_schedules_${new Date().toISOString().split('T')[0]}.csv`;
+ link.click();
+ URL.revokeObjectURL(url);
+ };
+
+ if (loading && page === 0) {
+ return (
+
+
+
+
Loading PPM schedules...
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
⚠️ PPM API Not Available
+
+
The PPM API endpoint is not deployed yet.
+
+ navigate('/ppm/new')}
+ className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded"
+ >
+ Try Creating New (Demo)
+
+
+ Try Again
+
+
+
+
+
+ Technical Error: {error}
+
+
+
+
+ );
+ }
+
+ const filteredPPMs = ppms.filter(ppm =>
+ ppm.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ ppm.asset_name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ ppm.company?.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ ppm.custom_asset_type?.toLowerCase().includes(searchTerm.toLowerCase())
+ );
+
+ return (
+
+ {/* Header */}
+
+
+
PPM Schedules
+
+ Total: {totalCount} PPM schedule{totalCount !== 1 ? 's' : ''}
+
+
+
+
+
+ Export All
+
+
+
+ New PPM Schedule
+
+
+
+
+ {/* Filters Bar */}
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="flex-1 outline-none text-gray-700 dark:text-gray-200 bg-transparent"
+ />
+
+
+
+
+ {
+ setCompanyFilter(e.target.value);
+ setPage(0);
+ }}
+ className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
+ />
+
+
+
+ {/* PPM Schedules Table */}
+
+
+
+
+
+
+ PPM ID
+
+
+ Company
+
+
+ Asset
+
+
+ Asset Type
+
+
+ Frequency
+
+
+ No. of PMs
+
+
+ Total Amount
+
+
+ Actions
+
+
+
+
+ {filteredPPMs.length === 0 ? (
+
+
+
+
+
No PPM schedules found
+
+ Create your first PPM schedule
+
+
+
+
+ ) : (
+ filteredPPMs.map((ppm) => (
+ handleView(ppm.name)}
+ >
+
+
+ {ppm.name}
+
+
+
+
+
+
+ {ppm.company || '-'}
+
+
+
+
+
+ {ppm.asset_name || '-'}
+
+
+
+
+ {ppm.custom_asset_type || '-'}
+
+
+
+
+
+
+ {ppm.custom_frequency || '-'}
+
+
+
+
+
+ {ppm.custom_no_of_pms || '-'}
+
+
+
+
+ {ppm.custom_total_amount ? `$${ppm.custom_total_amount.toLocaleString()}` : '-'}
+
+
+
+ e.stopPropagation()}>
+
setActionMenuOpen(actionMenuOpen === ppm.name ? null : ppm.name)}
+ className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
+ >
+
+
+ {actionMenuOpen === ppm.name && (
+
+
{
+ handleView(ppm.name);
+ setActionMenuOpen(null);
+ }}
+ className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
+ >
+
+ View
+
+
{
+ handleEdit(ppm.name);
+ setActionMenuOpen(null);
+ }}
+ className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
+ >
+
+ Edit
+
+
{
+ handleDuplicate(ppm.name);
+ setActionMenuOpen(null);
+ }}
+ className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
+ >
+
+ Duplicate
+
+
{
+ handleExport(ppm);
+ setActionMenuOpen(null);
+ }}
+ className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
+ >
+
+ Export
+
+
+
{
+ setDeleteConfirmOpen(ppm.name);
+ setActionMenuOpen(null);
+ }}
+ className="w-full text-left px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center gap-2"
+ >
+
+ Delete
+
+
+ )}
+
+
+
+ ))
+ )}
+
+
+
+
+ {/* Pagination */}
+ {(hasMore || page > 0) && (
+
+
+ Showing {page * limit + 1} to {Math.min((page + 1) * limit, totalCount)} of {totalCount} results
+
+
+ setPage(Math.max(0, page - 1))}
+ disabled={page === 0}
+ className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ Previous
+
+ setPage(page + 1)}
+ disabled={!hasMore}
+ className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ Next
+
+
+
+ )}
+
+
+ {/* Delete Confirmation Modal */}
+ {deleteConfirmOpen && (
+
+
+
Confirm Delete
+
+ Are you sure you want to delete this PPM schedule? This action cannot be undone.
+
+
+ setDeleteConfirmOpen(null)}
+ className="px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600"
+ >
+ Cancel
+
+ handleDelete(deleteConfirmOpen)}
+ disabled={mutationLoading}
+ className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50"
+ >
+ {mutationLoading ? 'Deleting...' : 'Delete'}
+
+
+
+
+ )}
+
+ );
+};
+
+export default PPMList;
+
diff --git a/src/pages/WorkOrderDetail.tsx b/src/pages/WorkOrderDetail.tsx
new file mode 100644
index 0000000..c338d49
--- /dev/null
+++ b/src/pages/WorkOrderDetail.tsx
@@ -0,0 +1,571 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
+import { useWorkOrderDetails, useWorkOrderMutations } from '../hooks/useWorkOrder';
+import { FaArrowLeft, FaSave, FaEdit, FaCheckCircle, FaClock } from 'react-icons/fa';
+import type { CreateWorkOrderData } from '../services/workOrderService';
+
+const WorkOrderDetail: React.FC = () => {
+ const { workOrderName } = useParams<{ workOrderName: string }>();
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const duplicateFromWorkOrder = searchParams.get('duplicate');
+
+ const isNewWorkOrder = workOrderName === 'new';
+ const isDuplicating = isNewWorkOrder && !!duplicateFromWorkOrder;
+
+ const { workOrder, loading, error } = useWorkOrderDetails(
+ isDuplicating ? duplicateFromWorkOrder : (isNewWorkOrder ? null : workOrderName || null)
+ );
+ const { createWorkOrder, updateWorkOrder, updateStatus, loading: saving } = useWorkOrderMutations();
+
+ const [isEditing, setIsEditing] = useState(isNewWorkOrder);
+ const [formData, setFormData] = useState({
+ company: '',
+ work_order_type: '',
+ asset: '',
+ asset_name: '',
+ description: '',
+ repair_status: 'Open',
+ workflow_state: '',
+ department: '',
+ custom_priority_: 'Normal',
+ asset_type: '',
+ manufacturer: '',
+ serial_number: '',
+ model: '',
+ custom_site_contractor: '',
+ custom_subcontractor: '',
+ failure_date: '',
+ custom_deadline_date: '',
+ });
+
+ useEffect(() => {
+ if (workOrder) {
+ setFormData({
+ company: workOrder.company || '',
+ work_order_type: workOrder.work_order_type || '',
+ asset: workOrder.asset || '',
+ asset_name: isDuplicating ? `${workOrder.asset_name} (Copy)` : (workOrder.asset_name || ''),
+ description: workOrder.description || '',
+ repair_status: isDuplicating ? 'Open' : (workOrder.repair_status || 'Open'),
+ workflow_state: workOrder.workflow_state || '',
+ department: workOrder.department || '',
+ custom_priority_: workOrder.custom_priority_ || 'Normal',
+ asset_type: workOrder.asset_type || '',
+ manufacturer: workOrder.manufacturer || '',
+ serial_number: workOrder.serial_number || '',
+ model: workOrder.model || '',
+ custom_site_contractor: workOrder.custom_site_contractor || '',
+ custom_subcontractor: workOrder.custom_subcontractor || '',
+ failure_date: workOrder.failure_date || '',
+ custom_deadline_date: workOrder.custom_deadline_date || '',
+ });
+ }
+ }, [workOrder, isDuplicating]);
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!formData.work_order_type) {
+ alert('Please select a Work Order Type');
+ return;
+ }
+
+ console.log('Submitting work order data:', formData);
+
+ try {
+ if (isNewWorkOrder || isDuplicating) {
+ const newWorkOrder = await createWorkOrder(formData);
+ const successMessage = isDuplicating
+ ? 'Work order duplicated successfully!'
+ : 'Work order created successfully!';
+ alert(successMessage);
+ navigate(`/work-orders/${newWorkOrder.name}`);
+ } else if (workOrderName) {
+ await updateWorkOrder(workOrderName, formData);
+ alert('Work order updated successfully!');
+ setIsEditing(false);
+ }
+ } catch (err) {
+ console.error('Work order save error:', err);
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error';
+
+ if (errorMessage.includes('404') || errorMessage.includes('not found') ||
+ errorMessage.includes('has no attribute') || errorMessage.includes('417')) {
+ alert(
+ '⚠️ Work Order API Not Deployed\n\n' +
+ 'The Work Order API endpoint is not deployed on your Frappe server yet.\n\n' +
+ 'Deploy work_order_api.py to: frappe-bench/apps/asset_lite/asset_lite/api/\n\n' +
+ 'Error: ' + errorMessage
+ );
+ } else {
+ alert('Failed to save work order:\n\n' + errorMessage);
+ }
+ }
+ };
+
+ const handleStatusUpdate = async (newStatus: string) => {
+ if (!workOrderName || isNewWorkOrder) return;
+
+ try {
+ await updateStatus(workOrderName, newStatus);
+ alert('Status updated successfully!');
+ window.location.reload();
+ } catch (err) {
+ alert('Failed to update status: ' + (err instanceof Error ? err.message : 'Unknown error'));
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+
Loading work order details...
+
+
+ );
+ }
+
+ if (error && !isNewWorkOrder && !isDuplicating) {
+ return (
+
+
+
Error: {error}
+
navigate('/work-orders')}
+ className="mt-2 text-red-700 dark:text-red-400 underline hover:text-red-800 dark:hover:text-red-300"
+ >
+ Back to work orders list
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+ navigate('/work-orders')}
+ className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 flex items-center gap-2"
+ >
+
+
+ {isDuplicating ? 'Duplicate Work Order' : (isNewWorkOrder ? 'New Work Order' : 'Work Order Details')}
+
+
+
+
+ {!isNewWorkOrder && !isEditing && (
+ <>
+ setIsEditing(true)}
+ className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg flex items-center gap-2"
+ >
+
+ Edit
+
+ {/* Quick Status Update Buttons */}
+ {workOrder?.repair_status !== 'Completed' && (
+ handleStatusUpdate('Completed')}
+ className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center gap-2"
+ disabled={saving}
+ >
+
+ Mark Complete
+
+ )}
+ {workOrder?.repair_status !== 'In Progress' && workOrder?.repair_status !== 'Completed' && (
+ handleStatusUpdate('In Progress')}
+ className="bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-2 rounded-lg flex items-center gap-2"
+ disabled={saving}
+ >
+
+ Start Work
+
+ )}
+ >
+ )}
+ {isEditing && (
+ <>
+ {
+ if (isNewWorkOrder) {
+ navigate('/work-orders');
+ } else {
+ setIsEditing(false);
+ }
+ }}
+ className="bg-gray-300 hover:bg-gray-400 text-gray-700 px-6 py-2 rounded-lg"
+ disabled={saving}
+ >
+ Cancel
+
+
+
+ {saving ? 'Saving...' : 'Save Changes'}
+
+ >
+ )}
+
+
+
+
+
+ );
+};
+
+export default WorkOrderDetail;
+
diff --git a/src/pages/WorkOrderList.tsx b/src/pages/WorkOrderList.tsx
new file mode 100644
index 0000000..2303e10
--- /dev/null
+++ b/src/pages/WorkOrderList.tsx
@@ -0,0 +1,513 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useWorkOrders, useWorkOrderMutations } from '../hooks/useWorkOrder';
+import { FaPlus, FaSearch, FaEdit, FaEye, FaTrash, FaCopy, FaEllipsisV, FaDownload, FaPrint, FaFileExport, FaCheckCircle, FaClock, FaExclamationTriangle } from 'react-icons/fa';
+
+const WorkOrderList: React.FC = () => {
+ const navigate = useNavigate();
+ const [page, setPage] = useState(0);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [statusFilter, setStatusFilter] = useState('');
+ const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(null);
+ const [actionMenuOpen, setActionMenuOpen] = useState(null);
+ const dropdownRef = useRef(null);
+ const limit = 20;
+
+ const filters = statusFilter ? { repair_status: statusFilter } : {};
+
+ const { workOrders, totalCount, hasMore, loading, error, refetch } = useWorkOrders(
+ filters,
+ limit,
+ page * limit,
+ 'creation desc'
+ );
+
+ const { deleteWorkOrder, loading: mutationLoading } = useWorkOrderMutations();
+
+ // Close dropdown when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
+ setActionMenuOpen(null);
+ }
+ };
+
+ if (actionMenuOpen) {
+ document.addEventListener('mousedown', handleClickOutside);
+ }
+
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [actionMenuOpen]);
+
+ const handleCreateNew = () => {
+ navigate('/work-orders/new');
+ };
+
+ const handleView = (workOrderName: string) => {
+ navigate(`/work-orders/${workOrderName}`);
+ };
+
+ const handleEdit = (workOrderName: string) => {
+ navigate(`/work-orders/${workOrderName}`);
+ };
+
+ const handleDelete = async (workOrderName: string) => {
+ try {
+ await deleteWorkOrder(workOrderName);
+ setDeleteConfirmOpen(null);
+ refetch();
+ alert('Work order deleted successfully!');
+ } catch (err) {
+ alert(`Failed to delete work order: ${err instanceof Error ? err.message : 'Unknown error'}`);
+ }
+ };
+
+ const handleDuplicate = (workOrderName: string) => {
+ navigate(`/work-orders/new?duplicate=${workOrderName}`);
+ };
+
+ const handleExport = (workOrder: any) => {
+ const dataStr = JSON.stringify(workOrder, null, 2);
+ const dataBlob = new Blob([dataStr], { type: 'application/json' });
+ const url = URL.createObjectURL(dataBlob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `work_order_${workOrder.name}.json`;
+ link.click();
+ URL.revokeObjectURL(url);
+ };
+
+ const handlePrint = (workOrderName: string) => {
+ window.open(`/work-orders/${workOrderName}?print=true`, '_blank');
+ };
+
+ const handleExportAll = () => {
+ const headers = ['Work Order ID', 'Asset', 'Type', 'Status', 'Department', 'Priority', 'Created'];
+ const csvContent = [
+ headers.join(','),
+ ...workOrders.map(wo => [
+ wo.name,
+ wo.asset_name || wo.asset || '',
+ wo.work_order_type || '',
+ wo.repair_status || '',
+ wo.department || '',
+ wo.custom_priority_ || '',
+ wo.creation ? new Date(wo.creation).toLocaleDateString() : ''
+ ].join(','))
+ ].join('\n');
+
+ const dataBlob = new Blob([csvContent], { type: 'text/csv' });
+ const url = URL.createObjectURL(dataBlob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `work_orders_export_${new Date().toISOString().split('T')[0]}.csv`;
+ link.click();
+ URL.revokeObjectURL(url);
+ };
+
+ const getStatusIcon = (status: string) => {
+ switch (status?.toLowerCase()) {
+ case 'completed':
+ return ;
+ case 'in progress':
+ return ;
+ case 'pending':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status?.toLowerCase()) {
+ case 'completed':
+ return 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300';
+ case 'in progress':
+ return 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300';
+ case 'pending':
+ case 'open':
+ return 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300';
+ case 'cancelled':
+ return 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300';
+ default:
+ return 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300';
+ }
+ };
+
+ const getPriorityColor = (priority: string) => {
+ switch (priority?.toLowerCase()) {
+ case 'high':
+ case 'urgent':
+ return 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300';
+ case 'medium':
+ return 'bg-orange-100 dark:bg-orange-900/30 text-orange-800 dark:text-orange-300';
+ case 'low':
+ return 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300';
+ default:
+ return 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300';
+ }
+ };
+
+ if (loading && page === 0) {
+ return (
+
+
+
+
Loading work orders...
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
⚠️ Work Order API Not Available
+
+
The Work Order API endpoint is not deployed yet.
+
To fix this, deploy the work_order_api.py file to your Frappe server.
+
+ navigate('/work-orders/new')}
+ className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded"
+ >
+ Try Creating New (Demo)
+
+
+ Try Again
+
+
+
+
+
+ Technical Error: {error}
+
+
+
+
+ );
+ }
+
+ // Filter work orders by search term
+ const filteredWorkOrders = workOrders.filter(wo =>
+ wo.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ wo.asset_name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ wo.description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ wo.asset?.toLowerCase().includes(searchTerm.toLowerCase())
+ );
+
+ return (
+
+ {/* Header */}
+
+
+
Work Orders
+
+ Total: {totalCount} work order{totalCount !== 1 ? 's' : ''}
+
+
+
+
+
+ Export All
+
+
+
+ New Work Order
+
+
+
+
+ {/* Filters Bar */}
+
+ {/* Search Bar */}
+
+
+
+ setSearchTerm(e.target.value)}
+ className="flex-1 outline-none text-gray-700 dark:text-gray-200 bg-transparent"
+ />
+
+
+
+ {/* Status Filter */}
+
+ {
+ setStatusFilter(e.target.value);
+ setPage(0);
+ }}
+ className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ All Statuses
+ Open
+ In Progress
+ Pending
+ Completed
+ Cancelled
+
+
+
+
+ {/* Work Orders Table */}
+
+
+
+
+
+
+ Work Order ID
+
+
+ Asset
+
+
+ Type
+
+
+ Department
+
+
+ Status
+
+
+ Priority
+
+
+ Actions
+
+
+
+
+ {filteredWorkOrders.length === 0 ? (
+
+
+
+
+
No work orders found
+
+ Create your first work order
+
+
+
+
+ ) : (
+ filteredWorkOrders.map((workOrder) => (
+ handleView(workOrder.name)}
+ >
+
+ {workOrder.name}
+
+ {workOrder.creation ? new Date(workOrder.creation).toLocaleDateString() : ''}
+
+
+
+ {workOrder.asset_name || '-'}
+ {workOrder.asset || ''}
+
+
+ {workOrder.work_order_type || '-'}
+
+
+ {workOrder.department || '-'}
+
+
+
+ {getStatusIcon(workOrder.repair_status || '')}
+
+ {workOrder.repair_status || 'Unknown'}
+
+
+
+
+
+ {workOrder.custom_priority_ || 'Normal'}
+
+
+
+ e.stopPropagation()}>
+
handleView(workOrder.name)}
+ className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 p-2 hover:bg-blue-50 dark:hover:bg-blue-900/30 rounded transition-colors"
+ title="View Details"
+ >
+
+
+
handleEdit(workOrder.name)}
+ className="text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 p-2 hover:bg-green-50 dark:hover:bg-green-900/30 rounded transition-colors"
+ title="Edit Work Order"
+ >
+
+
+
handleDuplicate(workOrder.name)}
+ className="text-purple-600 dark:text-purple-400 hover:text-purple-900 dark:hover:text-purple-300 p-2 hover:bg-purple-50 dark:hover:bg-purple-900/30 rounded transition-colors"
+ title="Duplicate Work Order"
+ >
+
+
+
setDeleteConfirmOpen(workOrder.name)}
+ className="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300 p-2 hover:bg-red-50 dark:hover:bg-red-900/30 rounded transition-colors"
+ title="Delete Work Order"
+ disabled={mutationLoading}
+ >
+
+
+
+ {/* More Actions Dropdown */}
+
+
setActionMenuOpen(actionMenuOpen === workOrder.name ? null : workOrder.name)}
+ className="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 p-2 hover:bg-gray-50 dark:hover:bg-gray-700 rounded transition-colors"
+ title="More Actions"
+ >
+
+
+
+ {actionMenuOpen === workOrder.name && (
+
+ {
+ handleExport(workOrder);
+ setActionMenuOpen(null);
+ }}
+ className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2 rounded-t-lg"
+ >
+
+ Export as JSON
+
+ {
+ handlePrint(workOrder.name);
+ setActionMenuOpen(null);
+ }}
+ className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2 rounded-b-lg"
+ >
+
+ Print Work Order
+
+
+ )}
+
+
+
+
+ ))
+ )}
+
+
+
+
+ {/* Pagination */}
+ {filteredWorkOrders.length > 0 && (
+
+
+ Showing {page * limit + 1} to{' '}
+
+ {Math.min((page + 1) * limit, totalCount)}
+ {' '}
+ of {totalCount} results
+
+
+ setPage(page - 1)}
+ className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
+ >
+ Previous
+
+ setPage(page + 1)}
+ className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
+ >
+ Next
+
+
+
+ )}
+
+
+ {/* Delete Confirmation Modal */}
+ {deleteConfirmOpen && (
+
+
+
+
+
+
+
+
+ Delete Work Order
+
+
+ Are you sure you want to delete this work order? This action cannot be undone.
+
+
+
+ Work Order ID: {deleteConfirmOpen}
+
+
+
+
setDeleteConfirmOpen(null)}
+ className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors"
+ disabled={mutationLoading}
+ >
+ Cancel
+
+
handleDelete(deleteConfirmOpen)}
+ className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors flex items-center gap-2 disabled:opacity-50"
+ disabled={mutationLoading}
+ >
+ {mutationLoading ? (
+ <>
+
+ Deleting...
+ >
+ ) : (
+ <>
+
+ Delete Work Order
+ >
+ )}
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default WorkOrderList;
+
diff --git a/src/services/assetMaintenanceService.ts b/src/services/assetMaintenanceService.ts
new file mode 100644
index 0000000..e729973
--- /dev/null
+++ b/src/services/assetMaintenanceService.ts
@@ -0,0 +1,220 @@
+import apiService from './apiService';
+import API_CONFIG from '../config/api';
+
+// Asset Maintenance Interfaces
+export interface AssetMaintenanceLog {
+ name: string;
+ asset_maintenance?: string;
+ naming_series?: string;
+ asset_name?: string;
+ custom_asset_type?: string;
+ item_code?: string;
+ item_name?: string;
+ custom_asset_names?: string;
+ custom_hospital_name?: string;
+ task?: string;
+ task_name?: string;
+ maintenance_type?: string;
+ periodicity?: string;
+ has_certificate?: number;
+ custom_early_completion?: number;
+ maintenance_status?: string;
+ custom_pm_overdue_reason?: string;
+ custom_accepted_by_moh?: string;
+ assign_to_name?: string;
+ due_date?: string;
+ custom_accepted_by_moh_?: string;
+ custom_template?: string;
+ workflow_state?: string;
+ creation?: string;
+ modified?: string;
+ owner?: string;
+ modified_by?: string;
+ docstatus?: number;
+ idx?: number;
+}
+
+export interface AssetMaintenanceListResponse {
+ asset_maintenance_logs: AssetMaintenanceLog[];
+ total_count: number;
+ limit: number;
+ offset: number;
+ has_more: boolean;
+}
+
+export interface MaintenanceFilters {
+ maintenance_status?: string;
+ asset_name?: string;
+ custom_hospital_name?: string;
+ maintenance_type?: string;
+ [key: string]: any;
+}
+
+export interface CreateMaintenanceData {
+ asset_name?: string;
+ task?: string;
+ task_name?: string;
+ maintenance_type?: string;
+ periodicity?: string;
+ maintenance_status?: string;
+ due_date?: string;
+ assign_to_name?: string;
+ description?: string;
+ [key: string]: any;
+}
+
+class AssetMaintenanceService {
+ /**
+ * Get list of asset maintenance logs with optional filters and pagination
+ */
+ async getMaintenanceLogs(
+ filters?: MaintenanceFilters,
+ fields?: string[],
+ limit: number = 20,
+ offset: number = 0,
+ orderBy?: string
+ ): Promise {
+ const params = new URLSearchParams();
+
+ if (filters) {
+ params.append('filters', JSON.stringify(filters));
+ }
+
+ if (fields && fields.length > 0) {
+ params.append('fields', JSON.stringify(fields));
+ }
+
+ params.append('limit', limit.toString());
+ params.append('offset', offset.toString());
+
+ if (orderBy) {
+ params.append('order_by', orderBy);
+ }
+
+ const endpoint = `${API_CONFIG.ENDPOINTS.GET_ASSET_MAINTENANCE_LOGS}?${params.toString()}`;
+ return apiService.apiCall(endpoint);
+ }
+
+ /**
+ * Get detailed information about a specific maintenance log
+ */
+ async getMaintenanceLogDetails(logName: string): Promise {
+ const endpoint = `${API_CONFIG.ENDPOINTS.GET_ASSET_MAINTENANCE_LOG_DETAILS}?log_name=${encodeURIComponent(logName)}`;
+ return apiService.apiCall(endpoint);
+ }
+
+ /**
+ * Create a new maintenance log
+ */
+ async createMaintenanceLog(logData: CreateMaintenanceData): Promise<{ success: boolean; asset_maintenance_log: AssetMaintenanceLog; message: string }> {
+ return apiService.apiCall(API_CONFIG.ENDPOINTS.CREATE_ASSET_MAINTENANCE_LOG, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ log_data: logData })
+ });
+ }
+
+ /**
+ * Update an existing maintenance log
+ */
+ async updateMaintenanceLog(
+ logName: string,
+ logData: Partial
+ ): Promise<{ success: boolean; asset_maintenance_log: AssetMaintenanceLog; message: string }> {
+ return apiService.apiCall(API_CONFIG.ENDPOINTS.UPDATE_ASSET_MAINTENANCE_LOG, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ log_name: logName,
+ log_data: logData
+ })
+ });
+ }
+
+ /**
+ * Delete a maintenance log
+ */
+ async deleteMaintenanceLog(logName: string): Promise<{ success: boolean; message: string }> {
+ return apiService.apiCall(API_CONFIG.ENDPOINTS.DELETE_ASSET_MAINTENANCE_LOG, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ log_name: logName })
+ });
+ }
+
+ /**
+ * Update maintenance log status
+ */
+ async updateMaintenanceStatus(
+ logName: string,
+ maintenanceStatus?: string,
+ workflowState?: string
+ ): Promise<{ success: boolean; asset_maintenance_log: AssetMaintenanceLog; message: string }> {
+ return apiService.apiCall(API_CONFIG.ENDPOINTS.UPDATE_MAINTENANCE_STATUS, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ log_name: logName,
+ maintenance_status: maintenanceStatus,
+ workflow_state: workflowState
+ })
+ });
+ }
+
+ /**
+ * Get maintenance logs for a specific asset
+ */
+ async getMaintenanceLogsByAsset(
+ assetName: string,
+ filters?: MaintenanceFilters,
+ limit: number = 20,
+ offset: number = 0
+ ): Promise {
+ const params = new URLSearchParams();
+ params.append('asset_name', assetName);
+
+ if (filters) {
+ params.append('filters', JSON.stringify(filters));
+ }
+
+ params.append('limit', limit.toString());
+ params.append('offset', offset.toString());
+
+ const endpoint = `${API_CONFIG.ENDPOINTS.GET_MAINTENANCE_LOGS_BY_ASSET}?${params.toString()}`;
+ return apiService.apiCall(endpoint);
+ }
+
+ /**
+ * Get overdue maintenance logs
+ */
+ async getOverdueMaintenanceLogs(
+ filters?: MaintenanceFilters,
+ limit: number = 20,
+ offset: number = 0
+ ): Promise {
+ const params = new URLSearchParams();
+
+ if (filters) {
+ params.append('filters', JSON.stringify(filters));
+ }
+
+ params.append('limit', limit.toString());
+ params.append('offset', offset.toString());
+
+ const endpoint = `${API_CONFIG.ENDPOINTS.GET_OVERDUE_MAINTENANCE_LOGS}?${params.toString()}`;
+ return apiService.apiCall(endpoint);
+ }
+}
+
+// Create and export singleton instance
+const assetMaintenanceService = new AssetMaintenanceService();
+export default assetMaintenanceService;
+
diff --git a/src/services/ppmService.ts b/src/services/ppmService.ts
new file mode 100644
index 0000000..0c3d507
--- /dev/null
+++ b/src/services/ppmService.ts
@@ -0,0 +1,242 @@
+import apiService from './apiService';
+import API_CONFIG from '../config/api';
+
+// PPM (Asset Maintenance) Interfaces
+export interface AssetMaintenance {
+ name: string;
+ company?: string;
+ asset_name?: string;
+ custom_asset_type?: string;
+ asset_category?: string;
+ custom_type_of_maintenance?: string;
+ custom_asset_name?: string;
+ item_code?: string;
+ item_name?: string;
+ maintenance_team?: string;
+ custom_pm_schedule?: string;
+ maintenance_manager?: string;
+ maintenance_manager_name?: string;
+ custom_warranty?: string;
+ custom_warranty_status?: string;
+ custom_service_contract?: number;
+ custom_service_contract_status?: string;
+ custom_frequency?: string;
+ custom_total_amount?: number;
+ custom_no_of_pms?: number;
+ custom_price_per_pm?: number;
+ creation?: string;
+ modified?: string;
+ owner?: string;
+ modified_by?: string;
+ docstatus?: number;
+ idx?: number;
+}
+
+export interface AssetMaintenanceListResponse {
+ asset_maintenances: AssetMaintenance[];
+ total_count: number;
+ limit: number;
+ offset: number;
+ has_more: boolean;
+}
+
+export interface MaintenanceTask {
+ name: string;
+ parent?: string;
+ task?: string;
+ task_name?: string;
+ start_date?: string;
+ end_date?: string;
+ periodicity?: string;
+ maintenance_type?: string;
+ maintenance_status?: string;
+ assign_to?: string;
+ assign_to_name?: string;
+ next_due_date?: string;
+ last_completion_date?: string;
+ [key: string]: any;
+}
+
+export interface ServiceCoverage {
+ name: string;
+ parent?: string;
+ [key: string]: any;
+}
+
+export interface PPMFilters {
+ company?: string;
+ asset_name?: string;
+ custom_asset_type?: string;
+ maintenance_team?: string;
+ custom_service_contract?: number;
+ [key: string]: any;
+}
+
+export interface CreatePPMData {
+ company?: string;
+ asset_name?: string;
+ custom_asset_type?: string;
+ maintenance_team?: string;
+ custom_frequency?: string;
+ custom_total_amount?: number;
+ [key: string]: any;
+}
+
+class PPMService {
+ /**
+ * Get list of asset maintenances (PPM schedules) with optional filters and pagination
+ */
+ async getAssetMaintenances(
+ filters?: PPMFilters,
+ fields?: string[],
+ limit: number = 20,
+ offset: number = 0,
+ orderBy?: string
+ ): Promise {
+ const params = new URLSearchParams();
+
+ if (filters) {
+ params.append('filters', JSON.stringify(filters));
+ }
+
+ if (fields && fields.length > 0) {
+ params.append('fields', JSON.stringify(fields));
+ }
+
+ params.append('limit', limit.toString());
+ params.append('offset', offset.toString());
+
+ if (orderBy) {
+ params.append('order_by', orderBy);
+ }
+
+ const endpoint = `${API_CONFIG.ENDPOINTS.GET_ASSET_MAINTENANCES}?${params.toString()}`;
+ return apiService.apiCall(endpoint);
+ }
+
+ /**
+ * Get detailed information about a specific asset maintenance
+ */
+ async getAssetMaintenanceDetails(maintenanceName: string): Promise {
+ const params = new URLSearchParams();
+ params.append('maintenance_name', maintenanceName);
+ const endpoint = `${API_CONFIG.ENDPOINTS.GET_ASSET_MAINTENANCE_DETAILS}?${params.toString()}`;
+ return apiService.apiCall(endpoint);
+ }
+
+ /**
+ * Create a new asset maintenance (PPM schedule)
+ */
+ async createAssetMaintenance(data: CreatePPMData): Promise<{ success: boolean; asset_maintenance: AssetMaintenance; message?: string }> {
+ const endpoint = `${API_CONFIG.ENDPOINTS.CREATE_ASSET_MAINTENANCE}`;
+ return apiService.apiCall<{ success: boolean; asset_maintenance: AssetMaintenance; message?: string }>(
+ endpoint,
+ {
+ method: 'POST',
+ body: JSON.stringify({ maintenance_data: JSON.stringify(data) })
+ }
+ );
+ }
+
+ /**
+ * Update an existing asset maintenance
+ */
+ async updateAssetMaintenance(
+ maintenanceName: string,
+ data: Partial
+ ): Promise<{ success: boolean; asset_maintenance: AssetMaintenance; message?: string }> {
+ const endpoint = `${API_CONFIG.ENDPOINTS.UPDATE_ASSET_MAINTENANCE}`;
+ return apiService.apiCall<{ success: boolean; asset_maintenance: AssetMaintenance; message?: string }>(
+ endpoint,
+ {
+ method: 'POST',
+ body: JSON.stringify({
+ maintenance_name: maintenanceName,
+ maintenance_data: JSON.stringify(data)
+ })
+ }
+ );
+ }
+
+ /**
+ * Delete an asset maintenance
+ */
+ async deleteAssetMaintenance(maintenanceName: string): Promise<{ success: boolean; message?: string }> {
+ const endpoint = `${API_CONFIG.ENDPOINTS.DELETE_ASSET_MAINTENANCE}`;
+ return apiService.apiCall<{ success: boolean; message?: string }>(
+ endpoint,
+ {
+ method: 'POST',
+ body: JSON.stringify({ maintenance_name: maintenanceName })
+ }
+ );
+ }
+
+ /**
+ * Get all maintenance tasks for a specific asset maintenance
+ */
+ async getMaintenanceTasks(maintenanceName: string): Promise<{ maintenance_tasks: MaintenanceTask[]; total_count: number }> {
+ const params = new URLSearchParams();
+ params.append('maintenance_name', maintenanceName);
+ const endpoint = `${API_CONFIG.ENDPOINTS.GET_MAINTENANCE_TASKS}?${params.toString()}`;
+ return apiService.apiCall<{ maintenance_tasks: MaintenanceTask[]; total_count: number }>(endpoint);
+ }
+
+ /**
+ * Get service coverage for a specific asset maintenance
+ */
+ async getServiceCoverage(maintenanceName: string): Promise<{ service_coverage: ServiceCoverage[]; total_count: number }> {
+ const params = new URLSearchParams();
+ params.append('maintenance_name', maintenanceName);
+ const endpoint = `${API_CONFIG.ENDPOINTS.GET_SERVICE_COVERAGE}?${params.toString()}`;
+ return apiService.apiCall<{ service_coverage: ServiceCoverage[]; total_count: number }>(endpoint);
+ }
+
+ /**
+ * Get all maintenance schedules for a specific asset
+ */
+ async getMaintenancesByAsset(
+ assetName: string,
+ filters?: PPMFilters,
+ limit: number = 20,
+ offset: number = 0
+ ): Promise {
+ const params = new URLSearchParams();
+ params.append('asset_name', assetName);
+
+ if (filters) {
+ params.append('filters', JSON.stringify(filters));
+ }
+
+ params.append('limit', limit.toString());
+ params.append('offset', offset.toString());
+
+ const endpoint = `${API_CONFIG.ENDPOINTS.GET_MAINTENANCES_BY_ASSET}?${params.toString()}`;
+ return apiService.apiCall(endpoint);
+ }
+
+ /**
+ * Get all asset maintenances with active service contracts
+ */
+ async getActiveServiceContracts(
+ filters?: PPMFilters,
+ limit: number = 20,
+ offset: number = 0
+ ): Promise {
+ const params = new URLSearchParams();
+
+ if (filters) {
+ params.append('filters', JSON.stringify(filters));
+ }
+
+ params.append('limit', limit.toString());
+ params.append('offset', offset.toString());
+
+ const endpoint = `${API_CONFIG.ENDPOINTS.GET_ACTIVE_SERVICE_CONTRACTS}?${params.toString()}`;
+ return apiService.apiCall(endpoint);
+ }
+}
+
+const ppmService = new PPMService();
+export default ppmService;
+
diff --git a/src/services/workOrderService.ts b/src/services/workOrderService.ts
new file mode 100644
index 0000000..b667cc2
--- /dev/null
+++ b/src/services/workOrderService.ts
@@ -0,0 +1,205 @@
+import apiService from './apiService';
+import API_CONFIG from '../config/api';
+
+// Work Order Interfaces
+export interface WorkOrder {
+ name: string;
+ company?: string;
+ naming_series?: string;
+ work_order_type?: string;
+ asset_type?: string;
+ manufacturer?: string;
+ serial_number?: string;
+ custom_priority_?: string;
+ asset?: string;
+ custom_maintenance_manager?: string;
+ department?: string;
+ repair_status?: string;
+ asset_name?: string;
+ supplier?: string;
+ custom_pending_reason?: string;
+ model?: string;
+ custom_site_contractor?: string;
+ custom_subcontractor?: string;
+ custom_service_agreement?: string;
+ custom_service_coverage?: string;
+ custom_start_date?: string;
+ custom_end_date?: string;
+ custom_total_amount?: number;
+ warranty?: string;
+ service_contract?: string;
+ covering_spare_parts?: string;
+ spare_parts_labour?: string;
+ covering_labour?: string;
+ ppm_only?: number;
+ failure_date?: string;
+ total_hours_spent?: number;
+ job_completed?: string;
+ custom_difference?: number;
+ custom_vendors_hrs?: number;
+ custom_deadline_date?: string;
+ custom_diffrence?: number;
+ feedback_rating?: number;
+ first_responded_on?: string;
+ penalty?: number;
+ custom_assigned_supervisor?: string;
+ stock_consumption?: number;
+ need_procurement?: number;
+ repair_cost?: number;
+ total_repair_cost?: number;
+ capitalize_repair_cost?: number;
+ increase_in_asset_life?: number;
+ description?: string;
+ actions_performed?: string;
+ bio_med_dept?: string;
+ workflow_state?: string;
+ creation?: string;
+ modified?: string;
+ owner?: string;
+ modified_by?: string;
+ docstatus?: number;
+ idx?: number;
+}
+
+export interface WorkOrderListResponse {
+ work_orders: WorkOrder[];
+ total_count: number;
+ limit: number;
+ offset: number;
+ has_more: boolean;
+}
+
+export interface WorkOrderFilters {
+ company?: string;
+ department?: string;
+ work_order_type?: string;
+ repair_status?: string;
+ workflow_state?: string;
+ asset?: string;
+ [key: string]: any;
+}
+
+export interface CreateWorkOrderData {
+ company?: string;
+ work_order_type?: string;
+ asset?: string;
+ asset_name?: string;
+ description?: string;
+ repair_status?: string;
+ workflow_state?: string;
+ department?: string;
+ custom_priority_?: string;
+ [key: string]: any;
+}
+
+class WorkOrderService {
+ /**
+ * Get list of work orders with optional filters and pagination
+ */
+ async getWorkOrders(
+ filters?: WorkOrderFilters,
+ fields?: string[],
+ limit: number = 20,
+ offset: number = 0,
+ orderBy?: string
+ ): Promise {
+ const params = new URLSearchParams();
+
+ if (filters) {
+ params.append('filters', JSON.stringify(filters));
+ }
+
+ if (fields && fields.length > 0) {
+ params.append('fields', JSON.stringify(fields));
+ }
+
+ params.append('limit', limit.toString());
+ params.append('offset', offset.toString());
+
+ if (orderBy) {
+ params.append('order_by', orderBy);
+ }
+
+ const endpoint = `${API_CONFIG.ENDPOINTS.GET_WORK_ORDERS}?${params.toString()}`;
+ return apiService.apiCall(endpoint);
+ }
+
+ /**
+ * Get detailed information about a specific work order
+ */
+ async getWorkOrderDetails(workOrderName: string): Promise {
+ const endpoint = `${API_CONFIG.ENDPOINTS.GET_WORK_ORDER_DETAILS}?work_order_name=${encodeURIComponent(workOrderName)}`;
+ return apiService.apiCall(endpoint);
+ }
+
+ /**
+ * Create a new work order
+ */
+ async createWorkOrder(workOrderData: CreateWorkOrderData): Promise<{ success: boolean; work_order: WorkOrder; message: string }> {
+ return apiService.apiCall(API_CONFIG.ENDPOINTS.CREATE_WORK_ORDER, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ work_order_data: workOrderData })
+ });
+ }
+
+ /**
+ * Update an existing work order
+ */
+ async updateWorkOrder(
+ workOrderName: string,
+ workOrderData: Partial
+ ): Promise<{ success: boolean; work_order: WorkOrder; message: string }> {
+ return apiService.apiCall(API_CONFIG.ENDPOINTS.UPDATE_WORK_ORDER, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ work_order_name: workOrderName,
+ work_order_data: workOrderData
+ })
+ });
+ }
+
+ /**
+ * Delete a work order
+ */
+ async deleteWorkOrder(workOrderName: string): Promise<{ success: boolean; message: string }> {
+ return apiService.apiCall(API_CONFIG.ENDPOINTS.DELETE_WORK_ORDER, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ work_order_name: workOrderName })
+ });
+ }
+
+ /**
+ * Update work order status
+ */
+ async updateWorkOrderStatus(
+ workOrderName: string,
+ repairStatus?: string,
+ workflowState?: string
+ ): Promise<{ success: boolean; work_order: WorkOrder; message: string }> {
+ return apiService.apiCall(API_CONFIG.ENDPOINTS.UPDATE_WORK_ORDER_STATUS, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ work_order_name: workOrderName,
+ repair_status: repairStatus,
+ workflow_state: workflowState
+ })
+ });
+ }
+}
+
+// Create and export singleton instance
+const workOrderService = new WorkOrderService();
+export default workOrderService;
+