Some checks failed
CI/CD Pipeline - Xpeditis PreProd / Backend - Build & Test (push) Failing after 5m51s
CI/CD Pipeline - Xpeditis PreProd / Backend - Docker Build & Push (push) Has been skipped
CI/CD Pipeline - Xpeditis PreProd / Frontend - Build & Test (push) Successful in 10m57s
CI/CD Pipeline - Xpeditis PreProd / Frontend - Docker Build & Push (push) Failing after 12m28s
CI/CD Pipeline - Xpeditis PreProd / Deploy to PreProd Server (push) Has been skipped
CI/CD Pipeline - Xpeditis PreProd / Run Smoke Tests (push) Has been skipped
255 lines
8.9 KiB
TypeScript
255 lines
8.9 KiB
TypeScript
/**
|
|
* Carrier Management Page
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { Carrier, CarrierStatus, CreateCarrierInput, UpdateCarrierInput } from '@/types/carrier';
|
|
import { CarrierForm } from '@/components/admin/CarrierForm';
|
|
|
|
export const CarrierManagement: React.FC = () => {
|
|
const [carriers, setCarriers] = useState<Carrier[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [showForm, setShowForm] = useState(false);
|
|
const [editingCarrier, setEditingCarrier] = useState<Carrier | null>(null);
|
|
|
|
useEffect(() => {
|
|
fetchCarriers();
|
|
}, []);
|
|
|
|
const fetchCarriers = async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const response = await fetch('/api/v1/carriers', {
|
|
headers: {
|
|
Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to fetch carriers');
|
|
|
|
const data = await response.json();
|
|
setCarriers(data);
|
|
} catch (err: any) {
|
|
setError(err.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCreate = async (data: CreateCarrierInput) => {
|
|
const response = await fetch('/api/v1/carriers', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
|
|
},
|
|
body: JSON.stringify(data),
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to create carrier');
|
|
|
|
await fetchCarriers();
|
|
setShowForm(false);
|
|
};
|
|
|
|
const handleUpdate = async (data: UpdateCarrierInput) => {
|
|
if (!editingCarrier) return;
|
|
|
|
const response = await fetch(`/api/v1/carriers/${editingCarrier.id}`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
|
|
},
|
|
body: JSON.stringify(data),
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to update carrier');
|
|
|
|
await fetchCarriers();
|
|
setEditingCarrier(null);
|
|
setShowForm(false);
|
|
};
|
|
|
|
const handleDelete = async (carrierId: string) => {
|
|
if (!confirm('Are you sure you want to delete this carrier?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/v1/carriers/${carrierId}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to delete carrier');
|
|
|
|
await fetchCarriers();
|
|
} catch (err: any) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
};
|
|
|
|
const handleToggleStatus = async (carrier: Carrier) => {
|
|
const newStatus =
|
|
carrier.status === CarrierStatus.ACTIVE ? CarrierStatus.INACTIVE : CarrierStatus.ACTIVE;
|
|
|
|
try {
|
|
await handleUpdate({ status: newStatus });
|
|
} catch (err: any) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 py-8">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
{/* Header */}
|
|
<div className="mb-8 flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-gray-900">Carrier Management</h1>
|
|
<p className="mt-2 text-sm text-gray-600">
|
|
Manage carrier integrations and configurations
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
setEditingCarrier(null);
|
|
setShowForm(true);
|
|
}}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700"
|
|
>
|
|
Add Carrier
|
|
</button>
|
|
</div>
|
|
|
|
{/* Error */}
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
|
|
<p className="text-sm text-red-800">{error}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Form Modal */}
|
|
{showForm && (
|
|
<div className="fixed inset-0 bg-gray-600 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="p-6">
|
|
<h2 className="text-xl font-bold text-gray-900 mb-6">
|
|
{editingCarrier ? 'Edit Carrier' : 'Add New Carrier'}
|
|
</h2>
|
|
<CarrierForm
|
|
carrier={editingCarrier || undefined}
|
|
onSubmit={editingCarrier ? handleUpdate : handleCreate}
|
|
onCancel={() => {
|
|
setShowForm(false);
|
|
setEditingCarrier(null);
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Carriers Grid */}
|
|
{loading ? (
|
|
<div className="text-center py-12">
|
|
<div className="text-gray-600">Loading carriers...</div>
|
|
</div>
|
|
) : carriers.length === 0 ? (
|
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-12 text-center">
|
|
<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="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
|
|
/>
|
|
</svg>
|
|
<h3 className="mt-2 text-sm font-medium text-gray-900">No carriers configured</h3>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
Get started by adding your first carrier integration
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{carriers.map(carrier => (
|
|
<div
|
|
key={carrier.id}
|
|
className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-gray-900">{carrier.name}</h3>
|
|
<p className="text-sm text-gray-500">SCAC: {carrier.scac}</p>
|
|
</div>
|
|
<span
|
|
className={`px-2 py-1 rounded-full text-xs font-medium ${
|
|
carrier.status === CarrierStatus.ACTIVE
|
|
? 'bg-green-100 text-green-800'
|
|
: carrier.status === CarrierStatus.MAINTENANCE
|
|
? 'bg-yellow-100 text-yellow-800'
|
|
: 'bg-gray-100 text-gray-800'
|
|
}`}
|
|
>
|
|
{carrier.status.toUpperCase()}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Details */}
|
|
<div className="space-y-2 mb-4 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-500">Priority:</span>
|
|
<span className="font-medium">{carrier.priority}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-500">Rate Limit:</span>
|
|
<span className="font-medium">{carrier.rateLimit || 'N/A'} req/min</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-500">Timeout:</span>
|
|
<span className="font-medium">{carrier.timeout || 'N/A'} ms</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => {
|
|
setEditingCarrier(carrier);
|
|
setShowForm(true);
|
|
}}
|
|
className="flex-1 px-3 py-2 bg-gray-100 text-gray-700 rounded-md text-sm font-medium hover:bg-gray-200"
|
|
>
|
|
Edit
|
|
</button>
|
|
<button
|
|
onClick={() => handleToggleStatus(carrier)}
|
|
className="flex-1 px-3 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700"
|
|
>
|
|
{carrier.status === CarrierStatus.ACTIVE ? 'Deactivate' : 'Activate'}
|
|
</button>
|
|
<button
|
|
onClick={() => handleDelete(carrier.id)}
|
|
className="px-3 py-2 bg-red-600 text-white rounded-md text-sm font-medium hover:bg-red-700"
|
|
>
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|