mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
rure engine
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "19.2.6",
|
||||
"react-dom": "19.2.6",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-router-dom": "^6.27.0",
|
||||
"recharts": "^3.8.1",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Navigate, Outlet, Route, Routes, useLocation, useNavigate } from "react-router-dom";
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { DashboardLayout, type SidebarItem } from "@edr/ui-common";
|
||||
import { LayoutDashboard, Network, Settings } from "lucide-react";
|
||||
|
||||
@@ -12,6 +13,17 @@ import UserManagementPage from "./pages/dashboard/user-management/UserManagement
|
||||
import LoadingScreen from "./components/LoadingScreen";
|
||||
import { RuleEnginePage } from "./pages/ruleEngine/RuleEngine";
|
||||
|
||||
// Create a QueryClient instance
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const sidebarItems: SidebarItem[] = [
|
||||
{
|
||||
label: "Overview",
|
||||
@@ -76,33 +88,34 @@ const App = () => {
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/auth" element={<LoginPage />} />
|
||||
<Route path="*" element={<Navigate to="/auth" replace />} />
|
||||
</Routes>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Routes>
|
||||
<Route path="/auth" element={<LoginPage />} />
|
||||
<Route path="*" element={<Navigate to="/auth" replace />} />
|
||||
</Routes>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/dashboard/overview" replace />} />
|
||||
<Route path="/dashboard" element={<Navigate to="/dashboard/overview" replace />} />
|
||||
<Route path="/dashboard" element={<DashboardShell />}>
|
||||
<Route path="overview" element={<OverviewPage />} />
|
||||
<Route path="user-management" element={<UserManagementPage />} />
|
||||
|
||||
<Route path="rule-engine" element={<RuleEnginePage />} />
|
||||
|
||||
<Route path="user-management/employees" element={<EmployeesPage />} />
|
||||
<Route path="user-management/permissions" element={<PermissionsPage />} />
|
||||
<Route path="user-management/roles" element={<RolesPage />} />
|
||||
|
||||
<Route path="org-structure" element={<Navigate to="/dashboard/user-management" replace />} />
|
||||
<Route path="org-structure/*" element={<Navigate to="/dashboard/user-management" replace />} />
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/dashboard/overview" replace />} />
|
||||
</Routes>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/dashboard/overview" replace />} />
|
||||
<Route path="/dashboard" element={<Navigate to="/dashboard/overview" replace />} />
|
||||
<Route path="/dashboard" element={<DashboardShell />}>
|
||||
<Route path="overview" element={<OverviewPage />} />
|
||||
<Route path="user-management" element={<UserManagementPage />} />
|
||||
<Route path="rule-engine" element={<RuleEnginePage />} />
|
||||
<Route path="user-management/employees" element={<EmployeesPage />} />
|
||||
<Route path="user-management/permissions" element={<PermissionsPage />} />
|
||||
<Route path="user-management/roles" element={<RolesPage />} />
|
||||
<Route path="org-structure" element={<Navigate to="/dashboard/user-management" replace />} />
|
||||
<Route path="org-structure/*" element={<Navigate to="/dashboard/user-management" replace />} />
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/dashboard/overview" replace />} />
|
||||
</Routes>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
@@ -0,0 +1,718 @@
|
||||
// src/components/ruleEngine/ContractType.tsx
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
// ==================== Toast Notification 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' : type === 'error' ? 'bg-red-500' : 'bg-blue-500';
|
||||
|
||||
return (
|
||||
<div className={`fixed bottom-4 right-4 ${bgColor} text-white px-6 py-3 rounded-lg shadow-lg z-50`}>
|
||||
{message}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ==================== API Service ====================
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api';
|
||||
|
||||
const apiService = {
|
||||
// Cargo Types
|
||||
getCargoTypes: () => fetch(`${API_BASE_URL}/cargo-types`).then(res => res.json()),
|
||||
createCargoType: (data: any) => fetch(`${API_BASE_URL}/cargo-types`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
}).then(res => res.json()),
|
||||
updateCargoType: (id: string, data: any) => fetch(`${API_BASE_URL}/cargo-types/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
}).then(res => res.json()),
|
||||
deleteCargoType: (id: string) => fetch(`${API_BASE_URL}/cargo-types/${id}`, {
|
||||
method: 'DELETE'
|
||||
}).then(res => res.json()),
|
||||
|
||||
// Container Types
|
||||
getContainerTypes: () => fetch(`${API_BASE_URL}/container-types`).then(res => res.json()),
|
||||
createContainerType: (data: any) => fetch(`${API_BASE_URL}/container-types`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
}).then(res => res.json()),
|
||||
updateContainerType: (id: string, data: any) => fetch(`${API_BASE_URL}/container-types/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
}).then(res => res.json()),
|
||||
deleteContainerType: (id: string) => fetch(`${API_BASE_URL}/container-types/${id}`, {
|
||||
method: 'DELETE'
|
||||
}).then(res => res.json()),
|
||||
|
||||
// Priority Rules
|
||||
getPriorityRules: () => fetch(`${API_BASE_URL}/priority-rules`).then(res => res.json()),
|
||||
createPriorityRule: (data: any) => fetch(`${API_BASE_URL}/priority-rules`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
}).then(res => res.json()),
|
||||
updatePriorityRule: (id: string, data: any) => fetch(`${API_BASE_URL}/priority-rules/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
}).then(res => res.json()),
|
||||
deletePriorityRule: (id: string) => fetch(`${API_BASE_URL}/priority-rules/${id}`, {
|
||||
method: 'DELETE'
|
||||
}).then(res => res.json()),
|
||||
|
||||
// Service Types
|
||||
getServiceTypes: () => fetch(`${API_BASE_URL}/service-types`).then(res => res.json()),
|
||||
createServiceType: (data: any) => fetch(`${API_BASE_URL}/service-types`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
}).then(res => res.json()),
|
||||
updateServiceType: (id: string, data: any) => fetch(`${API_BASE_URL}/service-types/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
}).then(res => res.json()),
|
||||
deleteServiceType: (id: string) => fetch(`${API_BASE_URL}/service-types/${id}`, {
|
||||
method: 'DELETE'
|
||||
}).then(res => res.json()),
|
||||
|
||||
// Surcharge Types
|
||||
getSurchargeTypes: () => fetch(`${API_BASE_URL}/surcharge-types`).then(res => res.json()),
|
||||
createSurchargeType: (data: any) => fetch(`${API_BASE_URL}/surcharge-types`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
}).then(res => res.json()),
|
||||
updateSurchargeType: (id: string, data: any) => fetch(`${API_BASE_URL}/surcharge-types/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
}).then(res => res.json()),
|
||||
deleteSurchargeType: (id: string) => fetch(`${API_BASE_URL}/surcharge-types/${id}`, {
|
||||
method: 'DELETE'
|
||||
}).then(res => res.json()),
|
||||
|
||||
// Surcharges
|
||||
getSurcharges: () => fetch(`${API_BASE_URL}/surcharges`).then(res => res.json()),
|
||||
createSurcharge: (data: any) => fetch(`${API_BASE_URL}/surcharges`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
}).then(res => res.json()),
|
||||
updateSurcharge: (id: string, data: any) => fetch(`${API_BASE_URL}/surcharges/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
}).then(res => res.json()),
|
||||
deleteSurcharge: (id: string) => fetch(`${API_BASE_URL}/surcharges/${id}`, {
|
||||
method: 'DELETE'
|
||||
}).then(res => res.json()),
|
||||
|
||||
// Weight Limit Rules
|
||||
getWeightLimitRules: () => fetch(`${API_BASE_URL}/weight-limit-rules`).then(res => res.json()),
|
||||
createWeightLimitRule: (data: any) => fetch(`${API_BASE_URL}/weight-limit-rules`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
}).then(res => res.json()),
|
||||
updateWeightLimitRule: (id: string, data: any) => fetch(`${API_BASE_URL}/weight-limit-rules/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
}).then(res => res.json()),
|
||||
deleteWeightLimitRule: (id: string) => fetch(`${API_BASE_URL}/weight-limit-rules/${id}`, {
|
||||
method: 'DELETE'
|
||||
}).then(res => res.json()),
|
||||
};
|
||||
|
||||
// ==================== Entity Table Component ====================
|
||||
const EntityTable = ({
|
||||
title,
|
||||
data,
|
||||
columns,
|
||||
onAdd,
|
||||
onEdit,
|
||||
onDelete,
|
||||
isLoading
|
||||
}: any) => {
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const filteredData = 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 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
{expanded && (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-4 border-green-500 border-t-transparent"></div>
|
||||
<p className="mt-2">Loading...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-sm mb-6 overflow-hidden transition-all duration-300">
|
||||
<div
|
||||
className="flex items-center justify-between px-6 py-4 bg-gradient-to-r from-gray-50 to-white border-b border-gray-200 cursor-pointer hover:bg-gray-50 transition-colors group"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-bold text-green-600 transition-transform group-hover:scale-110">
|
||||
{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 transition-all hover:bg-green-700 hover:shadow-md"
|
||||
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 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||||
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 0114 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 transition-colors">
|
||||
{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 font-medium transition-all hover:bg-yellow-600 mr-2"
|
||||
onClick={() => onEdit(item)}
|
||||
>
|
||||
✏️ Edit
|
||||
</button>
|
||||
<button
|
||||
className="bg-red-600 text-white px-3 py-1 rounded text-xs font-medium transition-all 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">
|
||||
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<p className="mt-2">No data found</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ==================== Main Component ====================
|
||||
const ContractTypePage = () => {
|
||||
const [activeTab, setActiveTab] = useState('cargo-types');
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingItem, setEditingItem] = useState<any>(null);
|
||||
const [currentEntity, setCurrentEntity] = useState('');
|
||||
const [formData, setFormData] = useState<any>({});
|
||||
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const showToast = (message: string, type: 'success' | 'error') => {
|
||||
setToast({ message, type });
|
||||
};
|
||||
|
||||
// Fetch all data
|
||||
const { data: cargoTypes = [], isLoading: cargoLoading } = useQuery({
|
||||
queryKey: ['cargo-types'],
|
||||
queryFn: apiService.getCargoTypes,
|
||||
});
|
||||
|
||||
const { data: containerTypes = [], isLoading: containerLoading } = useQuery({
|
||||
queryKey: ['container-types'],
|
||||
queryFn: apiService.getContainerTypes,
|
||||
});
|
||||
|
||||
const { data: priorityRules = [], isLoading: priorityLoading } = useQuery({
|
||||
queryKey: ['priority-rules'],
|
||||
queryFn: apiService.getPriorityRules,
|
||||
});
|
||||
|
||||
const { data: serviceTypes = [], isLoading: serviceLoading } = useQuery({
|
||||
queryKey: ['service-types'],
|
||||
queryFn: apiService.getServiceTypes,
|
||||
});
|
||||
|
||||
const { data: surchargeTypes = [], isLoading: surchargeTypeLoading } = useQuery({
|
||||
queryKey: ['surcharge-types'],
|
||||
queryFn: apiService.getSurchargeTypes,
|
||||
});
|
||||
|
||||
const { data: surcharges = [], isLoading: surchargeLoading } = useQuery({
|
||||
queryKey: ['surcharges'],
|
||||
queryFn: apiService.getSurcharges,
|
||||
});
|
||||
|
||||
const { data: weightLimitRules = [], isLoading: weightLimitLoading } = useQuery({
|
||||
queryKey: ['weight-limit-rules'],
|
||||
queryFn: apiService.getWeightLimitRules,
|
||||
});
|
||||
|
||||
// Mutations for Cargo Types
|
||||
const createCargoType = useMutation({
|
||||
mutationFn: apiService.createCargoType,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['cargo-types'] });
|
||||
showToast('Cargo type created successfully', 'success');
|
||||
setModalOpen(false);
|
||||
setFormData({});
|
||||
},
|
||||
onError: () => showToast('Failed to create cargo type', 'error'),
|
||||
});
|
||||
|
||||
const updateCargoType = useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: any }) => apiService.updateCargoType(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['cargo-types'] });
|
||||
showToast('Cargo type updated successfully', 'success');
|
||||
setModalOpen(false);
|
||||
setFormData({});
|
||||
setEditingItem(null);
|
||||
},
|
||||
onError: () => showToast('Failed to update cargo type', 'error'),
|
||||
});
|
||||
|
||||
const deleteCargoType = useMutation({
|
||||
mutationFn: apiService.deleteCargoType,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['cargo-types'] });
|
||||
showToast('Cargo type deleted successfully', 'success');
|
||||
},
|
||||
onError: () => showToast('Failed to delete cargo type', 'error'),
|
||||
});
|
||||
|
||||
const handleAdd = (entity: string) => {
|
||||
setCurrentEntity(entity);
|
||||
setEditingItem(null);
|
||||
setFormData(getDefaultFormData(entity));
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleEdit = (entity: string, item: any) => {
|
||||
setCurrentEntity(entity);
|
||||
setEditingItem(item);
|
||||
setFormData(item);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = (entity: string, item: any) => {
|
||||
if (window.confirm(`Are you sure you want to delete this ${entity}?`)) {
|
||||
if (entity === 'cargo-types') deleteCargoType.mutate(item.id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (currentEntity === 'cargo-types') {
|
||||
if (editingItem) {
|
||||
updateCargoType.mutate({ id: editingItem.id, data: formData });
|
||||
} else {
|
||||
createCargoType.mutate(formData);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getDefaultFormData = (entity: string) => {
|
||||
switch(entity) {
|
||||
case 'cargo-types':
|
||||
return { code: '', cargoTypeName: '', showFreeTextBox: false, requiresDirectorApproval: false, isActive: true, displayOrder: 1 };
|
||||
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 getEntityLoading = (entity: string) => {
|
||||
switch(entity) {
|
||||
case 'cargo-types': return cargoLoading;
|
||||
case 'container-types': return containerLoading;
|
||||
case 'priority-rules': return priorityLoading;
|
||||
case 'service-types': return serviceLoading;
|
||||
case 'surcharge-types': return surchargeTypeLoading;
|
||||
case 'surcharges': return surchargeLoading;
|
||||
case 'weight-limit-rules': return weightLimitLoading;
|
||||
default: return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getColumns = (entity: string) => {
|
||||
switch(entity) {
|
||||
case 'cargo-types':
|
||||
return [
|
||||
{ key: 'code', label: 'Code' },
|
||||
{ key: 'cargoTypeName', label: 'Name' },
|
||||
{ key: 'displayOrder', label: 'Order' },
|
||||
{
|
||||
key: 'isActive',
|
||||
label: 'Status',
|
||||
render: (val: boolean) => (
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${val ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
||||
{val ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
];
|
||||
case 'container-types':
|
||||
return [
|
||||
{ key: 'sizeCode', label: 'Size Code' },
|
||||
{ key: 'description', label: 'Description' },
|
||||
{ key: 'containersPerWagon', label: 'Containers/Wagon' },
|
||||
{
|
||||
key: 'isActive',
|
||||
label: 'Status',
|
||||
render: (val: boolean) => (
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${val ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
||||
{val ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
];
|
||||
case 'priority-rules':
|
||||
return [
|
||||
{ key: 'priorityType', label: 'Priority Type' },
|
||||
{ key: 'ruleName', label: 'Rule Name' },
|
||||
{ key: 'bonusPoints', label: 'Bonus Points' },
|
||||
{
|
||||
key: 'isActive',
|
||||
label: 'Status',
|
||||
render: (val: boolean) => (
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${val ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
||||
{val ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
];
|
||||
case 'service-types':
|
||||
return [
|
||||
{ key: 'code', label: 'Code' },
|
||||
{ key: 'serviceName', label: 'Service Name' },
|
||||
{ key: 'displayOrder', label: 'Order' },
|
||||
{
|
||||
key: 'isActive',
|
||||
label: 'Status',
|
||||
render: (val: boolean) => (
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${val ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
||||
{val ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
];
|
||||
case 'surcharge-types':
|
||||
return [
|
||||
{ key: 'code', label: 'Code' },
|
||||
{ key: 'name', label: 'Name' },
|
||||
{
|
||||
key: 'isActive',
|
||||
label: 'Status',
|
||||
render: (val: boolean) => (
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${val ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
||||
{val ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
];
|
||||
case 'surcharges':
|
||||
return [
|
||||
{ key: 'feeName', label: 'Fee Name' },
|
||||
{ key: 'calculationMethod', label: 'Method' },
|
||||
{
|
||||
key: 'rate',
|
||||
label: 'Rate',
|
||||
render: (val: number, item: any) => `${val} ${item.currency}`
|
||||
},
|
||||
{
|
||||
key: 'isActive',
|
||||
label: 'Status',
|
||||
render: (val: boolean) => (
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${val ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
||||
{val ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
];
|
||||
case 'weight-limit-rules':
|
||||
return [
|
||||
{ key: 'tradeDirection', label: 'Direction' },
|
||||
{
|
||||
key: 'maxWeightTons',
|
||||
label: 'Max Weight',
|
||||
render: (val: number) => `${val} tons`
|
||||
},
|
||||
{ key: 'exceededAction', label: 'Action' },
|
||||
{
|
||||
key: 'isActive',
|
||||
label: 'Status',
|
||||
render: (val: boolean) => (
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${val ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
||||
{val ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ id: 'cargo-types', label: 'Cargo Types' },
|
||||
{ id: 'container-types', label: 'Container Types' },
|
||||
{ id: 'priority-rules', label: 'Priority Rules' },
|
||||
{ id: 'service-types', label: 'Service Types' },
|
||||
{ id: 'surcharge-types', label: 'Surcharge Types' },
|
||||
{ id: 'surcharges', label: 'Surcharges' },
|
||||
{ id: 'weight-limit-rules', label: 'Weight Limit Rules' },
|
||||
];
|
||||
|
||||
const isLoading = cargoLoading || containerLoading || priorityLoading || serviceLoading || surchargeTypeLoading || surchargeLoading || weightLimitLoading;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-96">
|
||||
<div className="text-center">
|
||||
<div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-green-500 border-t-transparent"></div>
|
||||
<p className="mt-4 text-gray-600">Loading master data...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="contract-type-page">
|
||||
{toast && (
|
||||
<Toast
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={() => setToast(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<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={getEntityLoading(tab.id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{modalOpen && currentEntity === 'cargo-types' && (
|
||||
<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-md w-full mx-4">
|
||||
<div className="flex justify-between items-center px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{editingItem ? 'Edit Cargo Type' : 'Add Cargo Type'}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => {
|
||||
setModalOpen(false);
|
||||
setEditingItem(null);
|
||||
setFormData({});
|
||||
}}
|
||||
className="text-gray-400 hover:text-gray-600 text-2xl"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="px-6 py-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 focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||
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 focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||
value={formData.cargoTypeName || ''}
|
||||
onChange={(e) => setFormData({...formData, cargoTypeName: e.target.value})}
|
||||
required
|
||||
/>
|
||||
</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 focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||
value={formData.displayOrder || 1}
|
||||
onChange={(e) => setFormData({...formData, displayOrder: parseInt(e.target.value)})}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="showFreeTextBox"
|
||||
className="mr-2"
|
||||
checked={formData.showFreeTextBox || false}
|
||||
onChange={(e) => setFormData({...formData, showFreeTextBox: e.target.checked})}
|
||||
/>
|
||||
<label htmlFor="showFreeTextBox" className="text-sm text-gray-700">Show Free Text Box</label>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="requiresDirectorApproval"
|
||||
className="mr-2"
|
||||
checked={formData.requiresDirectorApproval || false}
|
||||
onChange={(e) => setFormData({...formData, requiresDirectorApproval: e.target.checked})}
|
||||
/>
|
||||
<label htmlFor="requiresDirectorApproval" className="text-sm text-gray-700">Requires Director Approval</label>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isActive"
|
||||
className="mr-2"
|
||||
checked={formData.isActive !== false}
|
||||
onChange={(e) => setFormData({...formData, isActive: e.target.checked})}
|
||||
/>
|
||||
<label htmlFor="isActive" className="text-sm text-gray-700">Active</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3 px-6 py-4 border-t border-gray-200 bg-gray-50">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setModalOpen(false);
|
||||
setEditingItem(null);
|
||||
setFormData({});
|
||||
}}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={createCargoType.isPending || updateCargoType.isPending}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-md hover:bg-green-700 disabled:opacity-50"
|
||||
>
|
||||
{createCargoType.isPending || updateCargoType.isPending ? 'Saving...' : editingItem ? 'Update' : 'Create'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContractTypePage;
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
// src/components/ruleEngine/ContractType.tsx
|
||||
import { useState } from 'react';
|
||||
|
||||
// ==================== MOCK DATA (Replace with your API calls later) ====================
|
||||
const mockCargoTypes = [
|
||||
{ id: '1', code: 'BULK', cargoTypeName: 'Bulk Cargo', displayOrder: 1, isActive: true },
|
||||
{ id: '2', code: 'BREAK_BULK', cargoTypeName: 'Break Bulk', displayOrder: 2, isActive: true },
|
||||
{ id: '3', code: 'CONTAINER', cargoTypeName: 'Containerized', displayOrder: 3, isActive: false },
|
||||
{ id: '4', code: 'LIQUID', cargoTypeName: 'Liquid Bulk', displayOrder: 4, isActive: true },
|
||||
];
|
||||
|
||||
const mockContainerTypes = [
|
||||
{ id: '1', sizeCode: '20FT', description: '20 Foot Standard Container', containersPerWagon: 2, isActive: true },
|
||||
{ id: '2', sizeCode: '40FT', description: '40 Foot Standard Container', containersPerWagon: 1, isActive: true },
|
||||
{ id: '3', sizeCode: '20RF', description: '20 Foot Refrigerated', containersPerWagon: 2, isActive: true },
|
||||
];
|
||||
|
||||
const mockPriorityRules = [
|
||||
{ id: '1', priorityType: 'HIGH', ruleName: 'High Priority Booking', bonusPoints: 100, isActive: true },
|
||||
{ id: '2', priorityType: 'URGENT', ruleName: 'Urgent Delivery', bonusPoints: 200, isActive: true },
|
||||
{ id: '3', priorityType: 'LOW', ruleName: 'Standard Booking', bonusPoints: 0, isActive: true },
|
||||
];
|
||||
|
||||
const mockServiceTypes = [
|
||||
{ id: '1', code: 'RAIL', serviceName: 'Rail Only', displayOrder: 1, isActive: true },
|
||||
{ id: '2', code: 'RAIL_FIRST', serviceName: 'Rail + First Mile', displayOrder: 2, isActive: true },
|
||||
{ id: '3', code: 'RAIL_LAST', serviceName: 'Rail + Last Mile', displayOrder: 3, isActive: false },
|
||||
];
|
||||
|
||||
const mockSurchargeTypes = [
|
||||
{ id: '1', code: 'HAZ', name: 'Hazardous Material', isActive: true },
|
||||
{ id: '2', code: 'REF', name: 'Refrigerated', isActive: true },
|
||||
{ id: '3', code: 'OVR', name: 'Overweight', isActive: true },
|
||||
];
|
||||
|
||||
const mockSurcharges = [
|
||||
{ id: '1', feeName: 'Hazardous Fee', calculationMethod: 'FLAT', rate: 150, currency: 'USD', isActive: true },
|
||||
{ id: '2', feeName: 'Refrigeration Fee', calculationMethod: 'PER_TON', rate: 25, currency: 'USD', isActive: true },
|
||||
];
|
||||
|
||||
const mockWeightLimitRules = [
|
||||
{ id: '1', tradeDirection: 'IMPORT', maxWeightTons: 20, exceededAction: 'WARNING_ONLY', isActive: true },
|
||||
{ id: '2', tradeDirection: 'EXPORT', maxWeightTons: 22, exceededAction: 'BLOCK', isActive: true },
|
||||
];
|
||||
|
||||
// ==================== Toast Component ====================
|
||||
const Toast = ({ message, type, onClose }: { message: string; type: string; onClose: () => void }) => {
|
||||
setTimeout(onClose, 3000);
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
// ==================== Entity Table Component ====================
|
||||
const EntityTable = ({ title, data, columns, onAdd, onEdit, onDelete }: 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())
|
||||
)
|
||||
) : [];
|
||||
|
||||
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 focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||
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 0114 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</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);
|
||||
|
||||
// State for each entity
|
||||
const [cargoTypes, setCargoTypes] = useState(mockCargoTypes);
|
||||
const [containerTypes, setContainerTypes] = useState(mockContainerTypes);
|
||||
const [priorityRules, setPriorityRules] = useState(mockPriorityRules);
|
||||
const [serviceTypes, setServiceTypes] = useState(mockServiceTypes);
|
||||
const [surchargeTypes, setSurchargeTypes] = useState(mockSurchargeTypes);
|
||||
const [surcharges, setSurcharges] = useState(mockSurcharges);
|
||||
const [weightLimitRules, setWeightLimitRules] = useState(mockWeightLimitRules);
|
||||
|
||||
const showToast = (message: string, type: 'success' | 'error') => {
|
||||
setToast({ message, type });
|
||||
setTimeout(() => setToast(null), 3000);
|
||||
};
|
||||
|
||||
const handleAdd = (entity: string) => {
|
||||
const newId = String(Date.now());
|
||||
let newItem;
|
||||
|
||||
switch(entity) {
|
||||
case 'cargo-types':
|
||||
newItem = { id: newId, code: 'NEW', cargoTypeName: 'New Type', displayOrder: cargoTypes.length + 1, isActive: true };
|
||||
setCargoTypes([...cargoTypes, newItem]);
|
||||
break;
|
||||
case 'container-types':
|
||||
newItem = { id: newId, sizeCode: 'NEW', description: 'New Container', containersPerWagon: 1, isActive: true };
|
||||
setContainerTypes([...containerTypes, newItem]);
|
||||
break;
|
||||
case 'priority-rules':
|
||||
newItem = { id: newId, priorityType: 'MEDIUM', ruleName: 'New Rule', bonusPoints: 0, isActive: true };
|
||||
setPriorityRules([...priorityRules, newItem]);
|
||||
break;
|
||||
case 'service-types':
|
||||
newItem = { id: newId, code: 'NEW', serviceName: 'New Service', displayOrder: serviceTypes.length + 1, isActive: true };
|
||||
setServiceTypes([...serviceTypes, newItem]);
|
||||
break;
|
||||
case 'surcharge-types':
|
||||
newItem = { id: newId, code: 'NEW', name: 'New Surcharge Type', isActive: true };
|
||||
setSurchargeTypes([...surchargeTypes, newItem]);
|
||||
break;
|
||||
case 'surcharges':
|
||||
newItem = { id: newId, feeName: 'New Fee', calculationMethod: 'FLAT', rate: 0, currency: 'USD', isActive: true };
|
||||
setSurcharges([...surcharges, newItem]);
|
||||
break;
|
||||
case 'weight-limit-rules':
|
||||
newItem = { id: newId, tradeDirection: 'IMPORT', maxWeightTons: 20, exceededAction: 'WARNING_ONLY', isActive: true };
|
||||
setWeightLimitRules([...weightLimitRules, newItem]);
|
||||
break;
|
||||
}
|
||||
showToast(`${entity} added successfully`, 'success');
|
||||
};
|
||||
|
||||
const handleEdit = (entity: string, item: any) => {
|
||||
showToast(`Edit ${item.code || item.sizeCode || item.ruleName || item.serviceName || item.name || item.feeName}`, 'success');
|
||||
};
|
||||
|
||||
const handleDelete = (entity: string, item: any) => {
|
||||
if (confirm('Are you sure you want to delete this item?')) {
|
||||
switch(entity) {
|
||||
case 'cargo-types':
|
||||
setCargoTypes(cargoTypes.filter(c => c.id !== item.id));
|
||||
break;
|
||||
case 'container-types':
|
||||
setContainerTypes(containerTypes.filter(c => c.id !== item.id));
|
||||
break;
|
||||
case 'priority-rules':
|
||||
setPriorityRules(priorityRules.filter(p => p.id !== item.id));
|
||||
break;
|
||||
case 'service-types':
|
||||
setServiceTypes(serviceTypes.filter(s => s.id !== item.id));
|
||||
break;
|
||||
case 'surcharge-types':
|
||||
setSurchargeTypes(surchargeTypes.filter(s => s.id !== item.id));
|
||||
break;
|
||||
case 'surcharges':
|
||||
setSurcharges(surcharges.filter(s => s.id !== item.id));
|
||||
break;
|
||||
case 'weight-limit-rules':
|
||||
setWeightLimitRules(weightLimitRules.filter(w => w.id !== item.id));
|
||||
break;
|
||||
}
|
||||
showToast(`${entity} deleted successfully`, 'success');
|
||||
}
|
||||
};
|
||||
|
||||
const getColumns = (entity: string) => {
|
||||
switch(entity) {
|
||||
case 'cargo-types':
|
||||
return [
|
||||
{ key: 'code', label: 'Code' },
|
||||
{ key: 'cargoTypeName', label: 'Name' },
|
||||
{ key: 'displayOrder', label: 'Order' },
|
||||
{ key: 'isActive', label: 'Status', render: (val: boolean) => val ? '✅ Active' : '❌ Inactive' }
|
||||
];
|
||||
case 'container-types':
|
||||
return [
|
||||
{ key: 'sizeCode', label: 'Size Code' },
|
||||
{ key: 'description', label: 'Description' },
|
||||
{ key: 'containersPerWagon', label: 'Containers/Wagon' },
|
||||
{ key: 'isActive', label: 'Status', render: (val: boolean) => val ? '✅ Active' : '❌ Inactive' }
|
||||
];
|
||||
case 'priority-rules':
|
||||
return [
|
||||
{ key: 'priorityType', label: 'Priority Type' },
|
||||
{ key: 'ruleName', label: 'Rule Name' },
|
||||
{ key: 'bonusPoints', label: 'Bonus Points' },
|
||||
{ key: 'isActive', label: 'Status', render: (val: boolean) => val ? '✅ Active' : '❌ Inactive' }
|
||||
];
|
||||
case 'service-types':
|
||||
return [
|
||||
{ key: 'code', label: 'Code' },
|
||||
{ key: 'serviceName', label: 'Service Name' },
|
||||
{ key: 'displayOrder', label: 'Order' },
|
||||
{ key: 'isActive', label: 'Status', render: (val: boolean) => val ? '✅ Active' : '❌ Inactive' }
|
||||
];
|
||||
case 'surcharge-types':
|
||||
return [
|
||||
{ key: 'code', label: 'Code' },
|
||||
{ key: 'name', label: 'Name' },
|
||||
{ key: 'isActive', label: 'Status', render: (val: boolean) => val ? '✅ Active' : '❌ Inactive' }
|
||||
];
|
||||
case 'surcharges':
|
||||
return [
|
||||
{ key: 'feeName', label: 'Fee Name' },
|
||||
{ key: 'calculationMethod', label: 'Method' },
|
||||
{ key: 'rate', label: 'Rate', render: (val: number, item: any) => `${val} ${item.currency}` },
|
||||
{ key: 'isActive', label: 'Status', render: (val: boolean) => val ? '✅ Active' : '❌ Inactive' }
|
||||
];
|
||||
case 'weight-limit-rules':
|
||||
return [
|
||||
{ key: 'tradeDirection', label: 'Direction' },
|
||||
{ key: 'maxWeightTons', label: 'Max Weight', render: (val: number) => `${val} tons` },
|
||||
{ key: 'exceededAction', label: 'Action' },
|
||||
{ key: 'isActive', label: 'Status', render: (val: boolean) => val ? '✅ Active' : '❌ Inactive' }
|
||||
];
|
||||
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' },
|
||||
{ id: 'container-types', label: 'Container Types' },
|
||||
{ id: 'priority-rules', label: 'Priority Rules' },
|
||||
{ id: 'service-types', label: 'Service Types' },
|
||||
{ id: 'surcharge-types', label: 'Surcharge Types' },
|
||||
{ id: 'surcharges', label: 'Surcharges' },
|
||||
{ id: 'weight-limit-rules', label: 'Weight Limit Rules' },
|
||||
];
|
||||
|
||||
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)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContractTypePage;
|
||||
@@ -0,0 +1,221 @@
|
||||
import { JSXElementConstructor, MouseEventHandler, ReactElement, ReactNode, SetStateAction, useState } from 'react';
|
||||
|
||||
export const ContractTypePage = () => {
|
||||
const [expandedSections, setExpandedSections] = useState({
|
||||
contractType: true,
|
||||
serviceType: false,
|
||||
cargoType: false
|
||||
});
|
||||
|
||||
const [contractTypes, setContractTypes] = useState([
|
||||
{ id: 1, name: 'Shipper', description: 'Company that sends the freight' },
|
||||
{ id: 2, name: 'Consignee', description: 'Company that receives the freight' },
|
||||
{ id: 3, name: 'Third Party', description: 'Company that is neither the shipper nor the consignee but is involved in the freight process' }
|
||||
]);
|
||||
|
||||
const [serviceTypes, setServiceTypes] = useState([
|
||||
{ id: 1, name: 'Standard', description: 'Regular shipping service' },
|
||||
{ id: 2, name: 'Express', description: 'Fast delivery service' },
|
||||
{ id: 3, name: 'Economy', description: 'Cost-effective shipping option' }
|
||||
]);
|
||||
|
||||
const [cargoTypes, setCargoTypes] = useState([
|
||||
{ id: 1, name: 'General Cargo', description: 'Standard packaged goods' },
|
||||
{ id: 2, name: 'Temperature Controlled', description: 'Goods requiring specific temperature' },
|
||||
{ id: 3, name: 'Hazardous Materials', description: 'Dangerous goods requiring special handling' }
|
||||
]);
|
||||
|
||||
const [newContractType, setNewContractType] = useState({ name: '', description: '' });
|
||||
const [newServiceType, setNewServiceType] = useState({ name: '', description: '' });
|
||||
const [newCargoType, setNewCargoType] = useState({ name: '', description: '' });
|
||||
const [showAddForms, setShowAddForms] = useState({
|
||||
contractType: false,
|
||||
serviceType: false,
|
||||
|
||||
cargoType: false
|
||||
});
|
||||
|
||||
type SectionKey = 'contractType' | 'serviceType' | 'cargoType';
|
||||
|
||||
const toggleSection = (section: SectionKey) => {
|
||||
setExpandedSections(prev => ({
|
||||
...prev,
|
||||
[section]: !prev[section]
|
||||
}));
|
||||
};
|
||||
|
||||
const toggleAddForm = (section: SectionKey) => {
|
||||
setShowAddForms(prev => ({
|
||||
...prev,
|
||||
[section]: !prev[section]
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAddContractType = () => {
|
||||
if (newContractType.name && newContractType.description) {
|
||||
setContractTypes([
|
||||
...contractTypes,
|
||||
{ id: Date.now(), ...newContractType }
|
||||
]);
|
||||
setNewContractType({ name: '', description: '' });
|
||||
toggleAddForm('contractType');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddServiceType = () => {
|
||||
if (newServiceType.name && newServiceType.description) {
|
||||
setServiceTypes([
|
||||
...serviceTypes,
|
||||
{ id: Date.now(), ...newServiceType }
|
||||
]);
|
||||
setNewServiceType({ name: '', description: '' });
|
||||
toggleAddForm('serviceType');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddCargoType = () => {
|
||||
if (newCargoType.name && newCargoType.description) {
|
||||
setCargoTypes([
|
||||
...cargoTypes,
|
||||
{ id: Date.now(), ...newCargoType }
|
||||
]);
|
||||
setNewCargoType({ name: '', description: '' });
|
||||
toggleAddForm('cargoType');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (type: string, id: number) => {
|
||||
if (type === 'contract') {
|
||||
setContractTypes(contractTypes.filter(item => item.id !== id));
|
||||
} else if (type === 'service') {
|
||||
setServiceTypes(serviceTypes.filter(item => item.id !== id));
|
||||
} else if (type === 'cargo') {
|
||||
setCargoTypes(cargoTypes.filter(item => item.id !== id));
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (type: any, id: any) => {
|
||||
// Implement edit functionality as needed
|
||||
alert(`Edit ${type} type with id: ${id}`);
|
||||
};
|
||||
|
||||
const renderTable = (title: string | number | boolean | ReactElement<any, string | JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined, types: any[], onAdd: { (): void; (): void; (): void; }, newItem: { name: any; description: any; }, setNewItem: { (value: SetStateAction<{ name: string; description: string; }>): void; (value: SetStateAction<{ name: string; description: string; }>): void; (value: SetStateAction<{ name: string; description: string; }>): void; (arg0: any): void; }, showAddForm: boolean, typeKey: string, addHandler: MouseEventHandler<HTMLButtonElement> | undefined) => (
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<div
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '10px',
|
||||
backgroundColor: '#f0f0f0',
|
||||
marginBottom: '10px'
|
||||
}}
|
||||
onClick={() => toggleSection(typeKey)}
|
||||
>
|
||||
<span style={{ marginRight: '10px', fontSize: '20px', color: '#138a49' }}>
|
||||
{expandedSections[typeKey] ? '▼' : '▶'}
|
||||
</span>
|
||||
<h3 style={{ margin: 0 }}>{title}</h3>
|
||||
</div>
|
||||
|
||||
{expandedSections[typeKey] && (
|
||||
<div style={{ marginLeft: '20px' }}>
|
||||
<button onClick={() => toggleAddForm(typeKey)}>
|
||||
Add {title.replace(' Types', ' type')}
|
||||
</button>
|
||||
|
||||
{showAddForm && (
|
||||
<div style={{
|
||||
marginTop: '10px',
|
||||
marginBottom: '10px',
|
||||
padding: '10px',
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px'
|
||||
}}>
|
||||
<h4>Add New {title.replace(' Types', '')}</h4>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
value={newItem.name}
|
||||
onChange={(e) => setNewItem({ ...newItem, name: e.target.value })}
|
||||
style={{ marginRight: '10px', padding: '5px' }}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Description"
|
||||
value={newItem.description}
|
||||
onChange={(e) => setNewItem({ ...newItem, description: e.target.value })}
|
||||
style={{ marginRight: '10px', padding: '5px' }}
|
||||
/>
|
||||
<button onClick={addHandler}>Save</button>
|
||||
<button onClick={() => toggleAddForm(typeKey)} style={{ marginLeft: '5px' }}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', marginTop: '10px' }}>
|
||||
<thead>
|
||||
<tr style={{ backgroundColor: '#f2f2f2' }}>
|
||||
<th style={{ border: '1px solid #ddd', padding: '8px', textAlign: 'left' }}>ID</th>
|
||||
<th style={{ border: '1px solid #ddd', padding: '8px', textAlign: 'left' }}>Name</th>
|
||||
<th style={{ border: '1px solid #ddd', padding: '8px', textAlign: 'left' }}>Description</th>
|
||||
<th style={{ border: '1px solid #ddd', padding: '8px', textAlign: 'left' }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{types.map((type) => (
|
||||
<tr key={type.id}>
|
||||
<td style={{ border: '1px solid #ddd', padding: '8px' }}>{type.id}</td>
|
||||
<td style={{ border: '1px solid #ddd', padding: '8px' }}>{type.name}</td>
|
||||
<td style={{ border: '1px solid #ddd', padding: '8px' }}>{type.description}</td>
|
||||
<td style={{ border: '1px solid #ddd', padding: '8px' }}>
|
||||
<button onClick={() => handleEdit(typeKey, type.id)} style={{ marginRight: '5px' }}>Edit</button>
|
||||
<button onClick={() => handleDelete(typeKey === 'contractType' ? 'contract' : typeKey === 'serviceType' ? 'service' : 'cargo', type.id)}>Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{renderTable(
|
||||
'Contract Types',
|
||||
contractTypes,
|
||||
handleAddContractType,
|
||||
newContractType,
|
||||
setNewContractType,
|
||||
showAddForms.contractType,
|
||||
'contractType',
|
||||
handleAddContractType
|
||||
)}
|
||||
|
||||
{renderTable(
|
||||
'Service Types',
|
||||
serviceTypes,
|
||||
handleAddServiceType,
|
||||
newServiceType,
|
||||
setNewServiceType,
|
||||
showAddForms.serviceType,
|
||||
'serviceType',
|
||||
handleAddServiceType
|
||||
)}
|
||||
|
||||
{renderTable(
|
||||
'Cargo Types',
|
||||
cargoTypes,
|
||||
handleAddCargoType,
|
||||
newCargoType,
|
||||
setNewCargoType,
|
||||
showAddForms.cargoType,
|
||||
'cargoType',
|
||||
handleAddCargoType
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,10 @@
|
||||
import { ContractTypePage, } from "@/components/ruleEngine/ContractType";
|
||||
// src/pages/ruleEngine/RuleEngine.tsx
|
||||
import ContractTypePage from "@/components/ruleEngine/ContractType";
|
||||
|
||||
export const RuleEnginePage = () => {
|
||||
return <div>
|
||||
<h3>
|
||||
Rule Engine Page
|
||||
</h3>
|
||||
|
||||
<div>
|
||||
<ContractTypePage />
|
||||
</div>
|
||||
|
||||
</div>;
|
||||
return (
|
||||
<div className="p-6">
|
||||
<ContractTypePage />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
// import { CargoType } from "@edr-freight-web/types";
|
||||
// import { api as client } from "@edr-freight-web/utils/api/";
|
||||
export const createCargoType = (data: any) => {
|
||||
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
// vite.config.ts
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { defineConfig } from "file:///C:/laragon/www/edr-platform/node_modules/.pnpm/vite@5.4.21_@types+node@20.19.41_lightningcss@1.32.0_terser@5.48.0/node_modules/vite/dist/node/index.js";
|
||||
import react from "file:///C:/laragon/www/edr-platform/node_modules/.pnpm/@vitejs+plugin-react@4.7.0_vite@5.4.21_@types+node@20.19.41_lightningcss@1.32.0_terser@5.48.0_/node_modules/@vitejs/plugin-react/dist/index.js";
|
||||
import tailwindcss from "file:///C:/laragon/www/edr-platform/node_modules/.pnpm/@tailwindcss+vite@4.3.0_vite@5.4.21_@types+node@20.19.41_lightningcss@1.32.0_terser@5.48.0_/node_modules/@tailwindcss/vite/dist/index.mjs";
|
||||
var __vite_injected_original_import_meta_url = "file:///C:/laragon/www/edr-platform/apps/edr-freight-web/backoffice/vite.config.ts";
|
||||
var __dirname = path.dirname(fileURLToPath(__vite_injected_original_import_meta_url));
|
||||
var vite_config_default = defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src")
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 5183,
|
||||
host: "0.0.0.0"
|
||||
}
|
||||
});
|
||||
export {
|
||||
vite_config_default as default
|
||||
};
|
||||
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJDOlxcXFxsYXJhZ29uXFxcXHd3d1xcXFxlZHItcGxhdGZvcm1cXFxcYXBwc1xcXFxlZHItZnJlaWdodC13ZWJcXFxcYmFja29mZmljZVwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiQzpcXFxcbGFyYWdvblxcXFx3d3dcXFxcZWRyLXBsYXRmb3JtXFxcXGFwcHNcXFxcZWRyLWZyZWlnaHQtd2ViXFxcXGJhY2tvZmZpY2VcXFxcdml0ZS5jb25maWcudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL0M6L2xhcmFnb24vd3d3L2Vkci1wbGF0Zm9ybS9hcHBzL2Vkci1mcmVpZ2h0LXdlYi9iYWNrb2ZmaWNlL3ZpdGUuY29uZmlnLnRzXCI7aW1wb3J0IHBhdGggZnJvbSBcIm5vZGU6cGF0aFwiO1xyXG5pbXBvcnQgeyBmaWxlVVJMVG9QYXRoIH0gZnJvbSBcIm5vZGU6dXJsXCI7XHJcblxyXG5pbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tIFwidml0ZVwiO1xyXG5pbXBvcnQgcmVhY3QgZnJvbSBcIkB2aXRlanMvcGx1Z2luLXJlYWN0XCI7XHJcbmltcG9ydCB0YWlsd2luZGNzcyBmcm9tIFwiQHRhaWx3aW5kY3NzL3ZpdGVcIjtcclxuXHJcbmNvbnN0IF9fZGlybmFtZSA9IHBhdGguZGlybmFtZShmaWxlVVJMVG9QYXRoKGltcG9ydC5tZXRhLnVybCkpO1xyXG5cclxuZXhwb3J0IGRlZmF1bHQgZGVmaW5lQ29uZmlnKHtcclxuICBwbHVnaW5zOiBbcmVhY3QoKSwgdGFpbHdpbmRjc3MoKV0sXHJcbiAgcmVzb2x2ZToge1xyXG4gICAgYWxpYXM6IHtcclxuICAgICAgXCJAXCI6IHBhdGgucmVzb2x2ZShfX2Rpcm5hbWUsIFwiLi9zcmNcIiksXHJcbiAgICB9LFxyXG4gIH0sXHJcbiAgc2VydmVyOiB7XHJcbiAgICBwb3J0OiA1MTgzLFxyXG4gICAgaG9zdDogXCIwLjAuMC4wXCIsXHJcbiAgfSxcclxufSk7XHJcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBaVgsT0FBTyxVQUFVO0FBQ2xZLFNBQVMscUJBQXFCO0FBRTlCLFNBQVMsb0JBQW9CO0FBQzdCLE9BQU8sV0FBVztBQUNsQixPQUFPLGlCQUFpQjtBQUxtTixJQUFNLDJDQUEyQztBQU81UixJQUFNLFlBQVksS0FBSyxRQUFRLGNBQWMsd0NBQWUsQ0FBQztBQUU3RCxJQUFPLHNCQUFRLGFBQWE7QUFBQSxFQUMxQixTQUFTLENBQUMsTUFBTSxHQUFHLFlBQVksQ0FBQztBQUFBLEVBQ2hDLFNBQVM7QUFBQSxJQUNQLE9BQU87QUFBQSxNQUNMLEtBQUssS0FBSyxRQUFRLFdBQVcsT0FBTztBQUFBLElBQ3RDO0FBQUEsRUFDRjtBQUFBLEVBQ0EsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFDRixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo=
|
||||
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
@@ -201,6 +201,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: 19.2.6
|
||||
version: 19.2.6(react@19.2.6)
|
||||
react-hot-toast:
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
react-router-dom:
|
||||
specifier: ^6.27.0
|
||||
version: 6.30.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
@@ -6281,6 +6284,11 @@ packages:
|
||||
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
goober@2.1.19:
|
||||
resolution: {integrity: sha512-U7veizMqxyKlM58+Z5j2ngJBH/r9siDmxpvNxSw0PylF6WQvrASJEZrxh1hidRBJc2jqoBVSyOban5u8m+6Rxg==}
|
||||
peerDependencies:
|
||||
csstype: ^3.0.10
|
||||
|
||||
gopd@1.2.0:
|
||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -8408,6 +8416,13 @@ packages:
|
||||
peerDependencies:
|
||||
react: 19.2.6
|
||||
|
||||
react-hot-toast@2.6.0:
|
||||
resolution: {integrity: sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
react: 19.2.6
|
||||
react-dom: 19.2.6
|
||||
|
||||
react-i18next@15.7.4:
|
||||
resolution: {integrity: sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==}
|
||||
peerDependencies:
|
||||
@@ -17011,6 +17026,10 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
gopd: 1.2.0
|
||||
|
||||
goober@2.1.19(csstype@3.2.3):
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
|
||||
gopd@1.2.0: {}
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
@@ -19373,6 +19392,13 @@ snapshots:
|
||||
dependencies:
|
||||
react: 19.2.6
|
||||
|
||||
react-hot-toast@2.6.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
goober: 2.1.19(csstype@3.2.3)
|
||||
react: 19.2.6
|
||||
react-dom: 19.2.6(react@19.2.6)
|
||||
|
||||
react-i18next@15.7.4(i18next@25.10.10(typescript@5.9.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
|
||||
Reference in New Issue
Block a user