mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 20:40:55 +00:00
rule engine UI, services and API integration
This commit is contained in:
@@ -0,0 +1,874 @@
|
||||
// src/components/ruleEngine/ContractType.tsx
|
||||
import { createCargoType } from '@/services/rule.engine/cargoType';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
// ==================== API Service ====================
|
||||
const API_BASE_URL = 'http://localhost:3001/api';
|
||||
|
||||
const apiFetch = async (endpoint: string, options?: RequestInit): Promise<any> => {
|
||||
try {
|
||||
const url = `${API_BASE_URL}${endpoint}`;
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`HTTP ${response.status}: ${errorText || response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`API Error (${endpoint}):`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const apiService = {
|
||||
getCargoTypes: (): Promise<any[]> => apiFetch('/cargo-types'),
|
||||
createCargoType: (data: any): Promise<any> => apiFetch('/cargo-types', { method: 'POST', body: JSON.stringify(data) }),
|
||||
updateCargoType: (id: string, data: any): Promise<any> => apiFetch(`/cargo-types/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
|
||||
deleteCargoType: (id: string): Promise<any> => apiFetch(`/cargo-types/${id}`, { method: 'DELETE' }),
|
||||
|
||||
getContainerTypes: (): Promise<any[]> => apiFetch('/container-types'),
|
||||
createContainerType: (data: any): Promise<any> => apiFetch('/container-types', { method: 'POST', body: JSON.stringify(data) }),
|
||||
updateContainerType: (id: string, data: any): Promise<any> => apiFetch(`/container-types/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
|
||||
deleteContainerType: (id: string): Promise<any> => apiFetch(`/container-types/${id}`, { method: 'DELETE' }),
|
||||
|
||||
getPriorityRules: (): Promise<any[]> => apiFetch('/priority-rules'),
|
||||
createPriorityRule: (data: any): Promise<any> => apiFetch('/priority-rules', { method: 'POST', body: JSON.stringify(data) }),
|
||||
updatePriorityRule: (id: string, data: any): Promise<any> => apiFetch(`/priority-rules/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
|
||||
deletePriorityRule: (id: string): Promise<any> => apiFetch(`/priority-rules/${id}`, { method: 'DELETE' }),
|
||||
|
||||
getServiceTypes: (): Promise<any[]> => apiFetch('/service-types'),
|
||||
createServiceType: (data: any): Promise<any> => apiFetch('/service-types', { method: 'POST', body: JSON.stringify(data) }),
|
||||
updateServiceType: (id: string, data: any): Promise<any> => apiFetch(`/service-types/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
|
||||
deleteServiceType: (id: string): Promise<any> => apiFetch(`/service-types/${id}`, { method: 'DELETE' }),
|
||||
|
||||
getSurchargeTypes: (): Promise<any[]> => apiFetch('/surcharge-types'),
|
||||
createSurchargeType: (data: any): Promise<any> => apiFetch('/surcharge-types', { method: 'POST', body: JSON.stringify(data) }),
|
||||
updateSurchargeType: (id: string, data: any): Promise<any> => apiFetch(`/surcharge-types/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
|
||||
deleteSurchargeType: (id: string): Promise<any> => apiFetch(`/surcharge-types/${id}`, { method: 'DELETE' }),
|
||||
|
||||
getSurcharges: (): Promise<any[]> => apiFetch('/surcharges'),
|
||||
createSurcharge: (data: any): Promise<any> => apiFetch('/surcharges', { method: 'POST', body: JSON.stringify(data) }),
|
||||
updateSurcharge: (id: string, data: any): Promise<any> => apiFetch(`/surcharges/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
|
||||
deleteSurcharge: (id: string): Promise<any> => apiFetch(`/surcharges/${id}`, { method: 'DELETE' }),
|
||||
|
||||
getWeightLimitRules: (): Promise<any[]> => apiFetch('/weight-limit-rules'),
|
||||
createWeightLimitRule: (data: any): Promise<any> => apiFetch('/weight-limit-rules', { method: 'POST', body: JSON.stringify(data) }),
|
||||
updateWeightLimitRule: (id: string, data: any): Promise<any> => apiFetch(`/weight-limit-rules/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
|
||||
deleteWeightLimitRule: (id: string): Promise<any> => apiFetch(`/weight-limit-rules/${id}`, { method: 'DELETE' }),
|
||||
};
|
||||
|
||||
// ==================== Toast Component ====================
|
||||
const Toast = ({ message, type, onClose }: { message: string; type: string; onClose: () => void }) => {
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(onClose, 3000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [onClose]);
|
||||
|
||||
const bgColor = type === 'success' ? 'bg-green-500' : 'bg-red-500';
|
||||
return (
|
||||
<div className={`fixed bottom-4 right-4 ${bgColor} text-white px-6 py-3 rounded-lg shadow-lg z-50`}>
|
||||
{message}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ==================== Modal Component ====================
|
||||
const Modal = ({ isOpen, onClose, title, children }: { isOpen: boolean; onClose: () => void; title: string; children: React.ReactNode }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center px-6 py-4 border-b border-gray-200 sticky top-0 bg-white">
|
||||
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 text-2xl">×</button>
|
||||
</div>
|
||||
<div className="p-6">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ==================== Form Components ====================
|
||||
|
||||
// 1. Cargo Type Form
|
||||
const CargoTypeForm = ({ initialData, onSubmit, onCancel, isSubmitting }: any) => {
|
||||
const [formData, setFormData] = useState({
|
||||
code: initialData?.code || '',
|
||||
cargoTypeName: initialData?.cargoTypeName || '',
|
||||
parentGroupId: initialData?.parentGroupId || '',
|
||||
showFreeTextBox: initialData?.showFreeTextBox || false,
|
||||
requiresDirectorApproval: initialData?.requiresDirectorApproval || false,
|
||||
isActive: initialData?.isActive !== undefined ? initialData.isActive : true,
|
||||
displayOrder: initialData?.displayOrder || 1,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const submitData = {
|
||||
code: formData.code.toUpperCase(),
|
||||
cargoTypeName: formData.cargoTypeName,
|
||||
parentGroupId: formData.parentGroupId || undefined,
|
||||
showFreeTextBox: formData.showFreeTextBox,
|
||||
requiresDirectorApproval: formData.requiresDirectorApproval,
|
||||
isActive: formData.isActive,
|
||||
displayOrder: Number(formData.displayOrder),
|
||||
};
|
||||
onSubmit(submitData);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Code *</label>
|
||||
<input type="text" className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.code} onChange={(e) => setFormData({...formData, code: e.target.value})} required />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Name *</label>
|
||||
<input type="text" className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.cargoTypeName} onChange={(e) => setFormData({...formData, cargoTypeName: e.target.value})} required />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Parent Group ID</label>
|
||||
<input type="text" className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.parentGroupId} onChange={(e) => setFormData({...formData, parentGroupId: e.target.value})} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Display Order</label>
|
||||
<input type="number" className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.displayOrder} onChange={(e) => setFormData({...formData, displayOrder: parseInt(e.target.value)})} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 mb-6">
|
||||
<label className="flex items-center"><input type="checkbox" className="mr-2" checked={formData.showFreeTextBox} onChange={(e) => setFormData({...formData, showFreeTextBox: e.target.checked})} /> Show Free Text Box</label>
|
||||
<label className="flex items-center"><input type="checkbox" className="mr-2" checked={formData.requiresDirectorApproval} onChange={(e) => setFormData({...formData, requiresDirectorApproval: e.target.checked})} /> Requires Director Approval</label>
|
||||
<label className="flex items-center"><input type="checkbox" className="mr-2" checked={formData.isActive} onChange={(e) => setFormData({...formData, isActive: e.target.checked})} /> Active</label>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||
<button type="button" onClick={onCancel} className="px-4 py-2 text-gray-700 bg-white border rounded-md hover:bg-gray-50">Cancel</button>
|
||||
<button type="submit" disabled={isSubmitting} className="px-4 py-2 text-white bg-green-600 rounded-md hover:bg-green-700 disabled:opacity-50">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
// 2. Container Type Form
|
||||
const ContainerTypeForm = ({ initialData, onSubmit, onCancel, isSubmitting }: any) => {
|
||||
const [formData, setFormData] = useState({
|
||||
sizeCode: initialData?.sizeCode || '',
|
||||
description: initialData?.description || '',
|
||||
containersPerWagon: initialData?.containersPerWagon || 1,
|
||||
isActive: initialData?.isActive !== undefined ? initialData.isActive : true,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSubmit(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Size Code *</label>
|
||||
<input type="text" className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.sizeCode} onChange={(e) => setFormData({...formData, sizeCode: e.target.value})} required />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Description</label>
|
||||
<textarea className="w-full px-3 py-2 border border-gray-300 rounded-md" rows={3} value={formData.description} onChange={(e) => setFormData({...formData, description: e.target.value})} />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Containers Per Wagon *</label>
|
||||
<input type="number" className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.containersPerWagon} onChange={(e) => setFormData({...formData, containersPerWagon: parseInt(e.target.value)})} required />
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label className="flex items-center"><input type="checkbox" className="mr-2" checked={formData.isActive} onChange={(e) => setFormData({...formData, isActive: e.target.checked})} /> Active</label>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||
<button type="button" onClick={onCancel} className="px-4 py-2 text-gray-700 bg-white border rounded-md hover:bg-gray-50">Cancel</button>
|
||||
<button type="submit" disabled={isSubmitting} className="px-4 py-2 text-white bg-green-600 rounded-md hover:bg-green-700 disabled:opacity-50">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
// 3. Priority Rule Form
|
||||
const PriorityRuleForm = ({ initialData, onSubmit, onCancel, isSubmitting }: any) => {
|
||||
const [formData, setFormData] = useState({
|
||||
priorityType: initialData?.priorityType || 'MEDIUM',
|
||||
ruleName: initialData?.ruleName || '',
|
||||
description: initialData?.description || '',
|
||||
activationCondition: initialData?.activationCondition || '',
|
||||
bonusPoints: initialData?.bonusPoints || 0,
|
||||
isActive: initialData?.isActive !== undefined ? initialData.isActive : true,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSubmit(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Priority Type *</label>
|
||||
<select className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.priorityType} onChange={(e) => setFormData({...formData, priorityType: e.target.value})}>
|
||||
<option value="HIGH">HIGH</option>
|
||||
<option value="MEDIUM">MEDIUM</option>
|
||||
<option value="LOW">LOW</option>
|
||||
<option value="URGENT">URGENT</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Bonus Points</label>
|
||||
<input type="number" className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.bonusPoints} onChange={(e) => setFormData({...formData, bonusPoints: parseInt(e.target.value)})} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Rule Name *</label>
|
||||
<input type="text" className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.ruleName} onChange={(e) => setFormData({...formData, ruleName: e.target.value})} required />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Description</label>
|
||||
<textarea className="w-full px-3 py-2 border border-gray-300 rounded-md" rows={2} value={formData.description} onChange={(e) => setFormData({...formData, description: e.target.value})} />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Activation Condition</label>
|
||||
<textarea className="w-full px-3 py-2 border border-gray-300 rounded-md" rows={2} value={formData.activationCondition} onChange={(e) => setFormData({...formData, activationCondition: e.target.value})} placeholder="e.g., weight > 1000" />
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label className="flex items-center"><input type="checkbox" className="mr-2" checked={formData.isActive} onChange={(e) => setFormData({...formData, isActive: e.target.checked})} /> Active</label>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||
<button type="button" onClick={onCancel} className="px-4 py-2 text-gray-700 bg-white border rounded-md hover:bg-gray-50">Cancel</button>
|
||||
<button type="submit" disabled={isSubmitting} className="px-4 py-2 text-white bg-green-600 rounded-md hover:bg-green-700 disabled:opacity-50">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
// 4. Service Type Form
|
||||
const ServiceTypeForm = ({ initialData, onSubmit, onCancel, isSubmitting }: any) => {
|
||||
const [formData, setFormData] = useState({
|
||||
code: initialData?.code || '',
|
||||
serviceName: initialData?.serviceName || '',
|
||||
description: initialData?.description || '',
|
||||
canBeBookedAlone: initialData?.canBeBookedAlone !== undefined ? initialData.canBeBookedAlone : true,
|
||||
includesFirstMile: initialData?.includesFirstMile || false,
|
||||
includesLastMile: initialData?.includesLastMile || false,
|
||||
includesCustoms: initialData?.includesCustoms || false,
|
||||
priorityBonusPoints: initialData?.priorityBonusPoints || 0,
|
||||
isActive: initialData?.isActive !== undefined ? initialData.isActive : true,
|
||||
displayOrder: initialData?.displayOrder || 1,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSubmit(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Code *</label>
|
||||
<input type="text" className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.code} onChange={(e) => setFormData({...formData, code: e.target.value.toUpperCase()})} required />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Service Name *</label>
|
||||
<input type="text" className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.serviceName} onChange={(e) => setFormData({...formData, serviceName: e.target.value})} required />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Description</label>
|
||||
<textarea className="w-full px-3 py-2 border border-gray-300 rounded-md" rows={2} value={formData.description} onChange={(e) => setFormData({...formData, description: e.target.value})} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Priority Bonus Points</label>
|
||||
<input type="number" className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.priorityBonusPoints} onChange={(e) => setFormData({...formData, priorityBonusPoints: parseInt(e.target.value)})} />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Display Order</label>
|
||||
<input type="number" className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.displayOrder} onChange={(e) => setFormData({...formData, displayOrder: parseInt(e.target.value)})} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 mb-6">
|
||||
<label className="flex items-center"><input type="checkbox" className="mr-2" checked={formData.canBeBookedAlone} onChange={(e) => setFormData({...formData, canBeBookedAlone: e.target.checked})} /> Can Be Booked Alone</label>
|
||||
<label className="flex items-center"><input type="checkbox" className="mr-2" checked={formData.includesFirstMile} onChange={(e) => setFormData({...formData, includesFirstMile: e.target.checked})} /> Includes First Mile</label>
|
||||
<label className="flex items-center"><input type="checkbox" className="mr-2" checked={formData.includesLastMile} onChange={(e) => setFormData({...formData, includesLastMile: e.target.checked})} /> Includes Last Mile</label>
|
||||
<label className="flex items-center"><input type="checkbox" className="mr-2" checked={formData.includesCustoms} onChange={(e) => setFormData({...formData, includesCustoms: e.target.checked})} /> Includes Customs</label>
|
||||
<label className="flex items-center"><input type="checkbox" className="mr-2" checked={formData.isActive} onChange={(e) => setFormData({...formData, isActive: e.target.checked})} /> Active</label>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||
<button type="button" onClick={onCancel} className="px-4 py-2 text-gray-700 bg-white border rounded-md hover:bg-gray-50">Cancel</button>
|
||||
<button type="submit" disabled={isSubmitting} className="px-4 py-2 text-white bg-green-600 rounded-md hover:bg-green-700 disabled:opacity-50">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
// 5. Surcharge Type Form
|
||||
const SurchargeTypeForm = ({ initialData, onSubmit, onCancel, isSubmitting }: any) => {
|
||||
const [formData, setFormData] = useState({
|
||||
code: initialData?.code || '',
|
||||
name: initialData?.name || '',
|
||||
description: initialData?.description || '',
|
||||
isActive: initialData?.isActive !== undefined ? initialData.isActive : true,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSubmit(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Code *</label>
|
||||
<input type="text" className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.code} onChange={(e) => setFormData({...formData, code: e.target.value.toUpperCase()})} required />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Name *</label>
|
||||
<input type="text" className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.name} onChange={(e) => setFormData({...formData, name: e.target.value})} required />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Description</label>
|
||||
<textarea className="w-full px-3 py-2 border border-gray-300 rounded-md" rows={3} value={formData.description} onChange={(e) => setFormData({...formData, description: e.target.value})} />
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label className="flex items-center"><input type="checkbox" className="mr-2" checked={formData.isActive} onChange={(e) => setFormData({...formData, isActive: e.target.checked})} /> Active</label>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||
<button type="button" onClick={onCancel} className="px-4 py-2 text-gray-700 bg-white border rounded-md hover:bg-gray-50">Cancel</button>
|
||||
<button type="submit" disabled={isSubmitting} className="px-4 py-2 text-white bg-green-600 rounded-md hover:bg-green-700 disabled:opacity-50">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
// 6. Surcharge Form
|
||||
const SurchargeForm = ({ initialData, onSubmit, onCancel, isSubmitting, surchargeTypes }: any) => {
|
||||
const [formData, setFormData] = useState({
|
||||
surchargeTypeId: initialData?.surchargeTypeId || '',
|
||||
feeName: initialData?.feeName || '',
|
||||
triggerDescription: initialData?.triggerDescription || '',
|
||||
calculationMethod: initialData?.calculationMethod || 'FLAT',
|
||||
rate: initialData?.rate || 0,
|
||||
currency: initialData?.currency || 'USD',
|
||||
applyToRail: initialData?.applyToRail || false,
|
||||
applyToFirstMile: initialData?.applyToFirstMile || false,
|
||||
applyToLastMile: initialData?.applyToLastMile || false,
|
||||
isActive: initialData?.isActive !== undefined ? initialData.isActive : true,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSubmit(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Surcharge Type *</label>
|
||||
<select className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.surchargeTypeId} onChange={(e) => setFormData({...formData, surchargeTypeId: e.target.value})} required>
|
||||
<option value="">Select Surcharge Type</option>
|
||||
{surchargeTypes?.map((type: any) => (<option key={type.id} value={type.id}>{type.name}</option>))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Fee Name *</label>
|
||||
<input type="text" className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.feeName} onChange={(e) => setFormData({...formData, feeName: e.target.value})} required />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Calculation Method *</label>
|
||||
<select className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.calculationMethod} onChange={(e) => setFormData({...formData, calculationMethod: e.target.value})}>
|
||||
<option value="PER_TON">Per Ton</option>
|
||||
<option value="FLAT">Flat</option>
|
||||
<option value="PERCENTAGE">Percentage</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Rate *</label>
|
||||
<input type="number" step="0.01" className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.rate} onChange={(e) => setFormData({...formData, rate: parseFloat(e.target.value)})} required />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Currency *</label>
|
||||
<input type="text" className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.currency} onChange={(e) => setFormData({...formData, currency: e.target.value.toUpperCase()})} maxLength={3} required />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Trigger Description</label>
|
||||
<textarea className="w-full px-3 py-2 border border-gray-300 rounded-md" rows={2} value={formData.triggerDescription} onChange={(e) => setFormData({...formData, triggerDescription: e.target.value})} />
|
||||
</div>
|
||||
<div className="space-y-2 mb-6">
|
||||
<label className="flex items-center"><input type="checkbox" className="mr-2" checked={formData.applyToRail} onChange={(e) => setFormData({...formData, applyToRail: e.target.checked})} /> Apply to Rail</label>
|
||||
<label className="flex items-center"><input type="checkbox" className="mr-2" checked={formData.applyToFirstMile} onChange={(e) => setFormData({...formData, applyToFirstMile: e.target.checked})} /> Apply to First Mile</label>
|
||||
<label className="flex items-center"><input type="checkbox" className="mr-2" checked={formData.applyToLastMile} onChange={(e) => setFormData({...formData, applyToLastMile: e.target.checked})} /> Apply to Last Mile</label>
|
||||
<label className="flex items-center"><input type="checkbox" className="mr-2" checked={formData.isActive} onChange={(e) => setFormData({...formData, isActive: e.target.checked})} /> Active</label>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||
<button type="button" onClick={onCancel} className="px-4 py-2 text-gray-700 bg-white border rounded-md hover:bg-gray-50">Cancel</button>
|
||||
<button type="submit" disabled={isSubmitting} className="px-4 py-2 text-white bg-green-600 rounded-md hover:bg-green-700 disabled:opacity-50">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
// 7. Weight Limit Rule Form
|
||||
const WeightLimitRuleForm = ({ initialData, onSubmit, onCancel, isSubmitting, containerTypes, surcharges }: any) => {
|
||||
const [formData, setFormData] = useState({
|
||||
containerTypeId: initialData?.containerTypeId || '',
|
||||
tradeDirection: initialData?.tradeDirection || 'IMPORT',
|
||||
maxWeightTons: initialData?.maxWeightTons || 20,
|
||||
warningThresholdTons: initialData?.warningThresholdTons || 18,
|
||||
exceededAction: initialData?.exceededAction || 'WARNING_ONLY',
|
||||
surchargeId: initialData?.surchargeId || '',
|
||||
isActive: initialData?.isActive !== undefined ? initialData.isActive : true,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSubmit(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Container Type *</label>
|
||||
<select className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.containerTypeId} onChange={(e) => setFormData({...formData, containerTypeId: e.target.value})} required>
|
||||
<option value="">Select Container Type</option>
|
||||
{containerTypes?.map((type: any) => (<option key={type.id} value={type.id}>{type.sizeCode}</option>))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Trade Direction *</label>
|
||||
<select className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.tradeDirection} onChange={(e) => setFormData({...formData, tradeDirection: e.target.value})}>
|
||||
<option value="IMPORT">Import</option>
|
||||
<option value="EXPORT">Export</option>
|
||||
<option value="DOMESTIC">Domestic</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Exceeded Action</label>
|
||||
<select className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.exceededAction} onChange={(e) => setFormData({...formData, exceededAction: e.target.value})}>
|
||||
<option value="WARNING_ONLY">Warning Only</option>
|
||||
<option value="BLOCK">Block</option>
|
||||
<option value="SURCHARGE">Surcharge</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Max Weight (Tons) *</label>
|
||||
<input type="number" step="0.1" className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.maxWeightTons} onChange={(e) => setFormData({...formData, maxWeightTons: parseFloat(e.target.value)})} required />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Warning Threshold (Tons) *</label>
|
||||
<input type="number" step="0.1" className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.warningThresholdTons} onChange={(e) => setFormData({...formData, warningThresholdTons: parseFloat(e.target.value)})} required />
|
||||
</div>
|
||||
</div>
|
||||
{formData.exceededAction === 'SURCHARGE' && (
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Surcharge</label>
|
||||
<select className="w-full px-3 py-2 border border-gray-300 rounded-md" value={formData.surchargeId} onChange={(e) => setFormData({...formData, surchargeId: e.target.value})}>
|
||||
<option value="">Select Surcharge</option>
|
||||
{surcharges?.map((surcharge: any) => (<option key={surcharge.id} value={surcharge.id}>{surcharge.feeName}</option>))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-6">
|
||||
<label className="flex items-center"><input type="checkbox" className="mr-2" checked={formData.isActive} onChange={(e) => setFormData({...formData, isActive: e.target.checked})} /> Active</label>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||
<button type="button" onClick={onCancel} className="px-4 py-2 text-gray-700 bg-white border rounded-md hover:bg-gray-50">Cancel</button>
|
||||
<button type="submit" disabled={isSubmitting} className="px-4 py-2 text-white bg-green-600 rounded-md hover:bg-green-700 disabled:opacity-50">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
// ==================== Entity Table Component ====================
|
||||
const EntityTable = ({ title, data, columns, onAdd, onEdit, onDelete, isLoading }: any) => {
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const filteredData = Array.isArray(data) ? data.filter((item: any) =>
|
||||
Object.values(item).some(value =>
|
||||
String(value).toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
) : [];
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-sm mb-6 overflow-hidden">
|
||||
<div className="flex items-center justify-between px-6 py-4 bg-gray-50 border-b border-gray-200">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-bold text-green-600">▼</span>
|
||||
<h3 className="text-base font-semibold text-gray-800">{title}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-4 border-green-500 border-t-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-sm mb-6 overflow-hidden">
|
||||
<div className="flex items-center justify-between px-6 py-4 bg-gray-50 border-b border-gray-200 cursor-pointer hover:bg-gray-100" onClick={() => setExpanded(!expanded)}>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-bold text-green-600">{expanded ? '▼' : '▶'}</span>
|
||||
<h3 className="text-base font-semibold text-gray-800">{title}</h3>
|
||||
<span className="px-2 py-0.5 text-xs font-medium bg-gray-200 text-gray-700 rounded-full">{filteredData.length} items</span>
|
||||
</div>
|
||||
</div>
|
||||
{expanded && (
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-6 gap-4 flex-wrap">
|
||||
<button className="bg-green-600 text-white px-4 py-2 rounded-md text-sm font-medium hover:bg-green-700" onClick={onAdd}>+ Add {title.slice(0, -1)}</button>
|
||||
<div className="relative">
|
||||
<input type="text" placeholder="Search..." className="w-80 px-3 py-2 pl-10 border border-gray-300 rounded-md" value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} />
|
||||
<svg className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0118 0z" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
{columns.map((col: any) => (<th key={col.key} className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{col.label}</th>))}
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{filteredData.map((item: any) => (
|
||||
<tr key={item.id} className="hover:bg-gray-50">
|
||||
{columns.map((col: any) => (<td key={col.key} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{col.render ? col.render(item[col.key], item) : item[col.key]}</td>))}
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
||||
<button className="bg-yellow-500 text-gray-900 px-3 py-1 rounded text-xs mr-2 hover:bg-yellow-600" onClick={() => onEdit(item)}>Edit</button>
|
||||
<button className="bg-red-600 text-white px-3 py-1 rounded text-xs hover:bg-red-700" onClick={() => onDelete(item)}>Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{filteredData.length === 0 && (<div className="text-center py-12 text-gray-500">No data found. Click "Add" to create one.</div>)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ==================== Main Component ====================
|
||||
const ContractTypePage = () => {
|
||||
const [activeTab, setActiveTab] = useState('cargo-types');
|
||||
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingItem, setEditingItem] = useState<any>(null);
|
||||
const [currentEntity, setCurrentEntity] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const [cargoTypes, setCargoTypes] = useState<any[]>([]);
|
||||
const [containerTypes, setContainerTypes] = useState<any[]>([]);
|
||||
const [priorityRules, setPriorityRules] = useState<any[]>([]);
|
||||
const [serviceTypes, setServiceTypes] = useState<any[]>([]);
|
||||
const [surchargeTypes, setSurchargeTypes] = useState<any[]>([]);
|
||||
const [surcharges, setSurcharges] = useState<any[]>([]);
|
||||
const [weightLimitRules, setWeightLimitRules] = useState<any[]>([]);
|
||||
|
||||
const showToast = (message: string, type: 'success' | 'error') => setToast({ message, type });
|
||||
|
||||
useEffect(() => { loadAllData(); }, []);
|
||||
|
||||
const loadAllData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [cargo, container, priority, service, surchargeType, surcharge, weight] = await Promise.all([
|
||||
apiService.getCargoTypes().catch(() => []),
|
||||
apiService.getContainerTypes().catch(() => []),
|
||||
apiService.getPriorityRules().catch(() => []),
|
||||
apiService.getServiceTypes().catch(() => []),
|
||||
apiService.getSurchargeTypes().catch(() => []),
|
||||
apiService.getSurcharges().catch(() => []),
|
||||
apiService.getWeightLimitRules().catch(() => []),
|
||||
]);
|
||||
setCargoTypes(cargo);
|
||||
setContainerTypes(container);
|
||||
setPriorityRules(priority);
|
||||
setServiceTypes(service);
|
||||
setSurchargeTypes(surchargeType);
|
||||
setSurcharges(surcharge);
|
||||
setWeightLimitRules(weight);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdd = (entity: string) => {
|
||||
setCurrentEntity(entity);
|
||||
setEditingItem(null);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleEdit = (entity: string, item: any) => {
|
||||
setCurrentEntity(entity);
|
||||
setEditingItem(item);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleSubmitForm = async (formData: any) => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
let result: any;
|
||||
|
||||
switch(currentEntity) {
|
||||
case 'cargo-types':
|
||||
if (editingItem) {
|
||||
result = await apiService.updateCargoType(editingItem.id, formData);
|
||||
setCargoTypes(cargoTypes.map(c => c.id === editingItem.id ? result : c));
|
||||
} else {
|
||||
result = await createCargoType(formData);
|
||||
setCargoTypes([...cargoTypes, result]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'container-types':
|
||||
if (editingItem) {
|
||||
result = await apiService.updateContainerType(editingItem.id, formData);
|
||||
setContainerTypes(containerTypes.map(c => c.id === editingItem.id ? result : c));
|
||||
} else {
|
||||
result = await apiService.createContainerType(formData);
|
||||
setContainerTypes([...containerTypes, result]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'priority-rules':
|
||||
if (editingItem) {
|
||||
result = await apiService.updatePriorityRule(editingItem.id, formData);
|
||||
setPriorityRules(priorityRules.map(p => p.id === editingItem.id ? result : p));
|
||||
} else {
|
||||
result = await apiService.createPriorityRule(formData);
|
||||
setPriorityRules([...priorityRules, result]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'service-types':
|
||||
if (editingItem) {
|
||||
result = await apiService.updateServiceType(editingItem.id, formData);
|
||||
setServiceTypes(serviceTypes.map(s => s.id === editingItem.id ? result : s));
|
||||
} else {
|
||||
result = await apiService.createServiceType(formData);
|
||||
setServiceTypes([...serviceTypes, result]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'surcharge-types':
|
||||
if (editingItem) {
|
||||
result = await apiService.updateSurchargeType(editingItem.id, formData);
|
||||
setSurchargeTypes(surchargeTypes.map(s => s.id === editingItem.id ? result : s));
|
||||
} else {
|
||||
result = await apiService.createSurchargeType(formData);
|
||||
setSurchargeTypes([...surchargeTypes, result]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'surcharges':
|
||||
if (editingItem) {
|
||||
result = await apiService.updateSurcharge(editingItem.id, formData);
|
||||
setSurcharges(surcharges.map(s => s.id === editingItem.id ? result : s));
|
||||
} else {
|
||||
result = await apiService.createSurcharge(formData);
|
||||
setSurcharges([...surcharges, result]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'weight-limit-rules':
|
||||
if (editingItem) {
|
||||
result = await apiService.updateWeightLimitRule(editingItem.id, formData);
|
||||
setWeightLimitRules(weightLimitRules.map(w => w.id === editingItem.id ? result : w));
|
||||
} else {
|
||||
result = await apiService.createWeightLimitRule(formData);
|
||||
setWeightLimitRules([...weightLimitRules, result]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
showToast(`${currentEntity} ${editingItem ? 'updated' : 'created'} successfully!`, 'success');
|
||||
setModalOpen(false);
|
||||
setEditingItem(null);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Submit error:', error);
|
||||
showToast(error.message || `Failed to ${editingItem ? 'update' : 'create'}`, 'error');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (entity: string, item: any) => {
|
||||
if (!confirm(`Delete this ${entity}?`)) return;
|
||||
try {
|
||||
switch(entity) {
|
||||
case 'cargo-types':
|
||||
await apiService.deleteCargoType(item.id);
|
||||
setCargoTypes(cargoTypes.filter(c => c.id !== item.id));
|
||||
break;
|
||||
case 'container-types':
|
||||
await apiService.deleteContainerType(item.id);
|
||||
setContainerTypes(containerTypes.filter(c => c.id !== item.id));
|
||||
break;
|
||||
case 'priority-rules':
|
||||
await apiService.deletePriorityRule(item.id);
|
||||
setPriorityRules(priorityRules.filter(p => p.id !== item.id));
|
||||
break;
|
||||
case 'service-types':
|
||||
await apiService.deleteServiceType(item.id);
|
||||
setServiceTypes(serviceTypes.filter(s => s.id !== item.id));
|
||||
break;
|
||||
case 'surcharge-types':
|
||||
await apiService.deleteSurchargeType(item.id);
|
||||
setSurchargeTypes(surchargeTypes.filter(s => s.id !== item.id));
|
||||
break;
|
||||
case 'surcharges':
|
||||
await apiService.deleteSurcharge(item.id);
|
||||
setSurcharges(surcharges.filter(s => s.id !== item.id));
|
||||
break;
|
||||
case 'weight-limit-rules':
|
||||
await apiService.deleteWeightLimitRule(item.id);
|
||||
setWeightLimitRules(weightLimitRules.filter(w => w.id !== item.id));
|
||||
break;
|
||||
}
|
||||
showToast(`${entity} deleted successfully!`, 'success');
|
||||
} catch (error: any) {
|
||||
showToast(error.message || `Failed to delete`, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const getColumns = (entity: string) => {
|
||||
const baseStatus = { key: 'isActive', label: 'Status', render: (val: boolean) => val ? '✅ Active' : '❌ Inactive' };
|
||||
switch(entity) {
|
||||
case 'cargo-types':
|
||||
return [{ key: 'code', label: 'Code' }, { key: 'cargoTypeName', label: 'Name' }, { key: 'displayOrder', label: 'Order' }, baseStatus];
|
||||
case 'container-types':
|
||||
return [{ key: 'sizeCode', label: 'Size Code' }, { key: 'description', label: 'Description' }, { key: 'containersPerWagon', label: 'Containers/Wagon' }, baseStatus];
|
||||
case 'priority-rules':
|
||||
return [{ key: 'priorityType', label: 'Priority Type' }, { key: 'ruleName', label: 'Rule Name' }, { key: 'bonusPoints', label: 'Bonus Points' }, baseStatus];
|
||||
case 'service-types':
|
||||
return [{ key: 'code', label: 'Code' }, { key: 'serviceName', label: 'Service Name' }, { key: 'displayOrder', label: 'Order' }, baseStatus];
|
||||
case 'surcharge-types':
|
||||
return [{ key: 'code', label: 'Code' }, { key: 'name', label: 'Name' }, baseStatus];
|
||||
case 'surcharges':
|
||||
return [{ key: 'feeName', label: 'Fee Name' }, { key: 'calculationMethod', label: 'Method' }, { key: 'rate', label: 'Rate', render: (val: number, item: any) => `${val} ${item.currency}` }, baseStatus];
|
||||
case 'weight-limit-rules':
|
||||
return [{ key: 'tradeDirection', label: 'Direction' }, { key: 'maxWeightTons', label: 'Max Weight', render: (val: number) => `${val} tons` }, { key: 'exceededAction', label: 'Action' }, baseStatus];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const getEntityData = (entity: string) => {
|
||||
switch(entity) {
|
||||
case 'cargo-types': return cargoTypes;
|
||||
case 'container-types': return containerTypes;
|
||||
case 'priority-rules': return priorityRules;
|
||||
case 'service-types': return serviceTypes;
|
||||
case 'surcharge-types': return surchargeTypes;
|
||||
case 'surcharges': return surcharges;
|
||||
case 'weight-limit-rules': return weightLimitRules;
|
||||
default: return [];
|
||||
}
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ id: 'cargo-types', label: 'Cargo Types', Form: CargoTypeForm },
|
||||
{ id: 'container-types', label: 'Container Types', Form: ContainerTypeForm },
|
||||
{ id: 'priority-rules', label: 'Priority Rules', Form: PriorityRuleForm },
|
||||
{ id: 'service-types', label: 'Service Types', Form: ServiceTypeForm },
|
||||
{ id: 'surcharge-types', label: 'Surcharge Types', Form: SurchargeTypeForm },
|
||||
{ id: 'surcharges', label: 'Surcharges', Form: SurchargeForm },
|
||||
{ id: 'weight-limit-rules', label: 'Weight Limit Rules', Form: WeightLimitRuleForm },
|
||||
];
|
||||
|
||||
const currentTab = tabs.find(t => t.id === currentEntity);
|
||||
const FormComponent = currentTab?.Form;
|
||||
|
||||
return (
|
||||
<div className="contract-type-page">
|
||||
{toast && <Toast message={toast.message} type={toast.type} onClose={() => setToast(null)} />}
|
||||
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-800">Rule Engine - Master Data</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">Manage cargo types, container types, priority rules, and more</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border-b border-gray-200 sticky top-0 z-10">
|
||||
<div className="flex space-x-1 overflow-x-auto">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
className={`px-4 py-2 text-sm font-medium transition-all duration-200 whitespace-nowrap ${activeTab === tab.id ? 'text-green-600 border-b-2 border-green-600' : 'text-gray-600 hover:text-green-600 hover:border-b-2 hover:border-green-300'}`}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
{tabs.map((tab) => (
|
||||
<div key={tab.id} className={activeTab === tab.id ? 'block' : 'hidden'}>
|
||||
<EntityTable
|
||||
title={tab.label}
|
||||
data={getEntityData(tab.id)}
|
||||
columns={getColumns(tab.id)}
|
||||
onAdd={() => handleAdd(tab.id)}
|
||||
onEdit={(item: any) => handleEdit(tab.id, item)}
|
||||
onDelete={(item: any) => handleDelete(tab.id, item)}
|
||||
isLoading={loading}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
isOpen={modalOpen}
|
||||
onClose={() => { setModalOpen(false); setEditingItem(null); }}
|
||||
title={editingItem ? `Edit ${currentEntity?.replace('-', ' ')}` : `Add ${currentEntity?.replace('-', ' ')}`}
|
||||
>
|
||||
{FormComponent && (
|
||||
<FormComponent
|
||||
initialData={editingItem}
|
||||
onSubmit={handleSubmitForm}
|
||||
onCancel={() => { setModalOpen(false); setEditingItem(null); }}
|
||||
isSubmitting={isSubmitting}
|
||||
surchargeTypes={surchargeTypes}
|
||||
containerTypes={containerTypes}
|
||||
surcharges={surcharges}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContractTypePage;
|
||||
@@ -1,4 +1,5 @@
|
||||
// src/components/ruleEngine/ContractType.tsx
|
||||
import { createCargoType } from '@/services/rule.engine/cargoType';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
// ==================== API Service ====================
|
||||
@@ -645,7 +646,7 @@ const ContractTypePage = () => {
|
||||
result = await apiService.updateCargoType(editingItem.id, formData);
|
||||
setCargoTypes(cargoTypes.map(c => c.id === editingItem.id ? result : c));
|
||||
} else {
|
||||
result = await apiService.createCargoType(formData);
|
||||
result = await createCargoType(formData);
|
||||
setCargoTypes([...cargoTypes, result]);
|
||||
}
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user