514 lines
23 KiB
TypeScript
514 lines
23 KiB
TypeScript
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<string>('');
|
||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState<string | null>(null);
|
||
const [actionMenuOpen, setActionMenuOpen] = useState<string | null>(null);
|
||
const dropdownRef = useRef<HTMLDivElement>(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 <FaCheckCircle className="text-green-500" />;
|
||
case 'in progress':
|
||
return <FaClock className="text-blue-500" />;
|
||
case 'pending':
|
||
return <FaExclamationTriangle className="text-yellow-500" />;
|
||
default:
|
||
return <FaClock className="text-gray-400" />;
|
||
}
|
||
};
|
||
|
||
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 (
|
||
<div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-gray-900">
|
||
<div className="text-center">
|
||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
|
||
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading work orders...</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="p-6 bg-gray-50 dark:bg-gray-900 min-h-screen">
|
||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-6">
|
||
<h2 className="text-xl font-bold text-yellow-800 dark:text-yellow-300 mb-4">⚠️ Work Order API Not Available</h2>
|
||
<div className="text-yellow-700 dark:text-yellow-400 space-y-3">
|
||
<p><strong>The Work Order API endpoint is not deployed yet.</strong></p>
|
||
<p>To fix this, deploy the work_order_api.py file to your Frappe server.</p>
|
||
<div className="mt-4 flex gap-3">
|
||
<button
|
||
onClick={() => navigate('/work-orders/new')}
|
||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded"
|
||
>
|
||
Try Creating New (Demo)
|
||
</button>
|
||
<button
|
||
onClick={refetch}
|
||
className="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded"
|
||
>
|
||
Try Again
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div className="mt-4 p-4 bg-white dark:bg-gray-800 rounded border border-yellow-300 dark:border-yellow-700">
|
||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||
<strong>Technical Error:</strong> {error}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 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 (
|
||
<div className="p-6 bg-gray-50 dark:bg-gray-900 min-h-screen">
|
||
{/* Header */}
|
||
<div className="mb-6 flex justify-between items-center">
|
||
<div>
|
||
<h1 className="text-3xl font-bold text-gray-800 dark:text-white">Work Orders</h1>
|
||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||
Total: {totalCount} work order{totalCount !== 1 ? 's' : ''}
|
||
</p>
|
||
</div>
|
||
<div className="flex gap-3">
|
||
<button
|
||
onClick={handleExportAll}
|
||
className="bg-green-600 hover:bg-green-700 text-white px-4 py-3 rounded-lg flex items-center gap-2 shadow transition-all"
|
||
disabled={workOrders.length === 0}
|
||
>
|
||
<FaFileExport />
|
||
<span className="font-medium">Export All</span>
|
||
</button>
|
||
<button
|
||
onClick={handleCreateNew}
|
||
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg flex items-center gap-2 shadow-lg transition-all hover:shadow-xl"
|
||
>
|
||
<FaPlus />
|
||
<span className="font-medium">New Work Order</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Filters Bar */}
|
||
<div className="mb-6 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
{/* Search Bar */}
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
||
<div className="flex items-center gap-2 border border-gray-300 dark:border-gray-600 rounded-lg px-4 py-2 bg-white dark:bg-gray-700">
|
||
<FaSearch className="text-gray-400 dark:text-gray-500" />
|
||
<input
|
||
type="text"
|
||
placeholder="Search by ID, asset name, description..."
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
className="flex-1 outline-none text-gray-700 dark:text-gray-200 bg-transparent"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Status Filter */}
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
||
<select
|
||
value={statusFilter}
|
||
onChange={(e) => {
|
||
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"
|
||
>
|
||
<option value="">All Statuses</option>
|
||
<option value="Open">Open</option>
|
||
<option value="In Progress">In Progress</option>
|
||
<option value="Pending">Pending</option>
|
||
<option value="Completed">Completed</option>
|
||
<option value="Cancelled">Cancelled</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Work Orders Table */}
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full">
|
||
<thead className="bg-gray-100 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
||
<tr>
|
||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||
Work Order ID
|
||
</th>
|
||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||
Asset
|
||
</th>
|
||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||
Type
|
||
</th>
|
||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||
Department
|
||
</th>
|
||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||
Status
|
||
</th>
|
||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||
Priority
|
||
</th>
|
||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||
Actions
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||
{filteredWorkOrders.length === 0 ? (
|
||
<tr>
|
||
<td colSpan={7} className="px-6 py-12 text-center text-gray-500 dark:text-gray-400">
|
||
<div className="flex flex-col items-center">
|
||
<FaSearch className="text-4xl text-gray-300 dark:text-gray-600 mb-2" />
|
||
<p>No work orders found</p>
|
||
<button
|
||
onClick={handleCreateNew}
|
||
className="mt-4 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 underline"
|
||
>
|
||
Create your first work order
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
) : (
|
||
filteredWorkOrders.map((workOrder) => (
|
||
<tr
|
||
key={workOrder.name}
|
||
className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer"
|
||
onClick={() => handleView(workOrder.name)}
|
||
>
|
||
<td className="px-6 py-4 whitespace-nowrap">
|
||
<div className="font-medium text-gray-900 dark:text-white">{workOrder.name}</div>
|
||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||
{workOrder.creation ? new Date(workOrder.creation).toLocaleDateString() : ''}
|
||
</div>
|
||
</td>
|
||
<td className="px-6 py-4 whitespace-nowrap">
|
||
<div className="text-sm text-gray-900 dark:text-white">{workOrder.asset_name || '-'}</div>
|
||
<div className="text-xs text-gray-500 dark:text-gray-400">{workOrder.asset || ''}</div>
|
||
</td>
|
||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
|
||
{workOrder.work_order_type || '-'}
|
||
</td>
|
||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
|
||
{workOrder.department || '-'}
|
||
</td>
|
||
<td className="px-6 py-4 whitespace-nowrap">
|
||
<div className="flex items-center gap-2">
|
||
{getStatusIcon(workOrder.repair_status || '')}
|
||
<span className={`px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(workOrder.repair_status || '')}`}>
|
||
{workOrder.repair_status || 'Unknown'}
|
||
</span>
|
||
</div>
|
||
</td>
|
||
<td className="px-6 py-4 whitespace-nowrap">
|
||
<span className={`px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${getPriorityColor(workOrder.custom_priority_ || '')}`}>
|
||
{workOrder.custom_priority_ || 'Normal'}
|
||
</span>
|
||
</td>
|
||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
||
<button
|
||
onClick={() => 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"
|
||
>
|
||
<FaEye />
|
||
</button>
|
||
<button
|
||
onClick={() => 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"
|
||
>
|
||
<FaEdit />
|
||
</button>
|
||
<button
|
||
onClick={() => 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"
|
||
>
|
||
<FaCopy />
|
||
</button>
|
||
<button
|
||
onClick={() => 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}
|
||
>
|
||
<FaTrash />
|
||
</button>
|
||
|
||
{/* More Actions Dropdown */}
|
||
<div className="relative" ref={actionMenuOpen === workOrder.name ? dropdownRef : null}>
|
||
<button
|
||
onClick={() => 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"
|
||
>
|
||
<FaEllipsisV />
|
||
</button>
|
||
|
||
{actionMenuOpen === workOrder.name && (
|
||
<div className="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-10">
|
||
<button
|
||
onClick={() => {
|
||
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"
|
||
>
|
||
<FaDownload className="text-blue-500" />
|
||
Export as JSON
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
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"
|
||
>
|
||
<FaPrint className="text-purple-500" />
|
||
Print Work Order
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
))
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* Pagination */}
|
||
{filteredWorkOrders.length > 0 && (
|
||
<div className="bg-gray-50 dark:bg-gray-700 px-6 py-4 flex items-center justify-between border-t border-gray-200 dark:border-gray-600">
|
||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||
Showing <span className="font-medium">{page * limit + 1}</span> to{' '}
|
||
<span className="font-medium">
|
||
{Math.min((page + 1) * limit, totalCount)}
|
||
</span>{' '}
|
||
of <span className="font-medium">{totalCount}</span> results
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<button
|
||
disabled={page === 0}
|
||
onClick={() => 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
|
||
</button>
|
||
<button
|
||
disabled={!hasMore}
|
||
onClick={() => 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
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Delete Confirmation Modal */}
|
||
{deleteConfirmOpen && (
|
||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4 shadow-2xl">
|
||
<div className="flex items-start gap-4">
|
||
<div className="flex-shrink-0 w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
|
||
<FaTrash className="text-red-600 dark:text-red-400 text-xl" />
|
||
</div>
|
||
<div className="flex-1">
|
||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||
Delete Work Order
|
||
</h3>
|
||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||
Are you sure you want to delete this work order? This action cannot be undone.
|
||
</p>
|
||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-md p-3 mb-4">
|
||
<p className="text-xs text-yellow-800 dark:text-yellow-300">
|
||
<strong>Work Order ID:</strong> {deleteConfirmOpen}
|
||
</p>
|
||
</div>
|
||
<div className="flex gap-3 justify-end">
|
||
<button
|
||
onClick={() => 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
|
||
</button>
|
||
<button
|
||
onClick={() => 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 ? (
|
||
<>
|
||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||
Deleting...
|
||
</>
|
||
) : (
|
||
<>
|
||
<FaTrash />
|
||
Delete Work Order
|
||
</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default WorkOrderList;
|
||
|