422 lines
16 KiB
TypeScript
422 lines
16 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { getAllOrganizations } from '@/lib/api/admin';
|
|
import { createOrganization, updateOrganization } from '@/lib/api/organizations';
|
|
|
|
interface Organization {
|
|
id: string;
|
|
name: string;
|
|
type: string;
|
|
scac?: string;
|
|
siren?: string;
|
|
eori?: string;
|
|
contact_phone?: string;
|
|
contact_email?: string;
|
|
address: {
|
|
street: string;
|
|
city: string;
|
|
state?: string;
|
|
postalCode: string;
|
|
country: string;
|
|
};
|
|
logoUrl?: string;
|
|
isActive: boolean;
|
|
createdAt: string;
|
|
}
|
|
|
|
export default function AdminOrganizationsPage() {
|
|
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [selectedOrg, setSelectedOrg] = useState<Organization | null>(null);
|
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
const [showEditModal, setShowEditModal] = useState(false);
|
|
|
|
// Form state
|
|
const [formData, setFormData] = useState({
|
|
name: '',
|
|
type: 'FREIGHT_FORWARDER',
|
|
scac: '',
|
|
siren: '',
|
|
eori: '',
|
|
contact_phone: '',
|
|
contact_email: '',
|
|
address: {
|
|
street: '',
|
|
city: '',
|
|
state: '',
|
|
postalCode: '',
|
|
country: 'FR',
|
|
},
|
|
logoUrl: '',
|
|
});
|
|
|
|
useEffect(() => {
|
|
fetchOrganizations();
|
|
}, []);
|
|
|
|
const fetchOrganizations = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await getAllOrganizations();
|
|
setOrganizations(response.organizations || []);
|
|
setError(null);
|
|
} catch (err: any) {
|
|
setError(err.message || 'Failed to load organizations');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCreate = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
try {
|
|
await createOrganization(formData);
|
|
await fetchOrganizations();
|
|
setShowCreateModal(false);
|
|
resetForm();
|
|
} catch (err: any) {
|
|
alert(err.message || 'Failed to create organization');
|
|
}
|
|
};
|
|
|
|
const handleUpdate = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!selectedOrg) return;
|
|
|
|
try {
|
|
await updateOrganization(selectedOrg.id, formData);
|
|
await fetchOrganizations();
|
|
setShowEditModal(false);
|
|
setSelectedOrg(null);
|
|
resetForm();
|
|
} catch (err: any) {
|
|
alert(err.message || 'Failed to update organization');
|
|
}
|
|
};
|
|
|
|
const resetForm = () => {
|
|
setFormData({
|
|
name: '',
|
|
type: 'FREIGHT_FORWARDER',
|
|
scac: '',
|
|
siren: '',
|
|
eori: '',
|
|
contact_phone: '',
|
|
contact_email: '',
|
|
address: {
|
|
street: '',
|
|
city: '',
|
|
state: '',
|
|
postalCode: '',
|
|
country: 'FR',
|
|
},
|
|
logoUrl: '',
|
|
});
|
|
};
|
|
|
|
const openEditModal = (org: Organization) => {
|
|
setSelectedOrg(org);
|
|
setFormData({
|
|
name: org.name,
|
|
type: org.type,
|
|
scac: org.scac || '',
|
|
siren: org.siren || '',
|
|
eori: org.eori || '',
|
|
contact_phone: org.contact_phone || '',
|
|
contact_email: org.contact_email || '',
|
|
address: org.address,
|
|
logoUrl: org.logoUrl || '',
|
|
});
|
|
setShowEditModal(true);
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-96">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
|
<p className="mt-4 text-gray-600">Loading organizations...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">Organization Management</h1>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
Manage all organizations in the system
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setShowCreateModal(true)}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
|
>
|
|
+ Create Organization
|
|
</button>
|
|
</div>
|
|
|
|
{/* Error Message */}
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Organizations Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{organizations.map(org => (
|
|
<div key={org.id} className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div className="flex-1">
|
|
<h3 className="text-lg font-semibold text-gray-900">{org.name}</h3>
|
|
<span className={`inline-block mt-2 px-2 py-1 text-xs font-semibold rounded-full ${
|
|
org.type === 'FREIGHT_FORWARDER' ? 'bg-blue-100 text-blue-800' :
|
|
org.type === 'CARRIER' ? 'bg-green-100 text-green-800' :
|
|
'bg-purple-100 text-purple-800'
|
|
}`}>
|
|
{org.type.replace('_', ' ')}
|
|
</span>
|
|
</div>
|
|
<span className={`px-2 py-1 text-xs font-semibold rounded-full ${
|
|
org.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
|
}`}>
|
|
{org.isActive ? 'Active' : 'Inactive'}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="space-y-2 text-sm text-gray-600 mb-4">
|
|
{org.scac && (
|
|
<div>
|
|
<span className="font-medium">SCAC:</span> {org.scac}
|
|
</div>
|
|
)}
|
|
{org.siren && (
|
|
<div>
|
|
<span className="font-medium">SIREN:</span> {org.siren}
|
|
</div>
|
|
)}
|
|
{org.contact_email && (
|
|
<div>
|
|
<span className="font-medium">Email:</span> {org.contact_email}
|
|
</div>
|
|
)}
|
|
<div>
|
|
<span className="font-medium">Location:</span> {org.address.city}, {org.address.country}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex space-x-2">
|
|
<button
|
|
onClick={() => openEditModal(org)}
|
|
className="flex-1 px-3 py-2 bg-blue-50 text-blue-700 rounded-md hover:bg-blue-100 transition-colors text-sm font-medium"
|
|
>
|
|
Edit
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Create/Edit Modal */}
|
|
{(showCreateModal || showEditModal) && (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 overflow-y-auto">
|
|
<div className="bg-white rounded-lg p-6 max-w-2xl w-full m-4 max-h-[90vh] overflow-y-auto">
|
|
<h2 className="text-xl font-bold mb-4">
|
|
{showCreateModal ? 'Create New Organization' : 'Edit Organization'}
|
|
</h2>
|
|
<form onSubmit={showCreateModal ? handleCreate : handleUpdate} className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700">Organization Name *</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formData.name}
|
|
onChange={e => setFormData({ ...formData, name: e.target.value })}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">Type *</label>
|
|
<select
|
|
value={formData.type}
|
|
onChange={e => setFormData({ ...formData, type: e.target.value })}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
|
>
|
|
<option value="FREIGHT_FORWARDER">Freight Forwarder</option>
|
|
<option value="CARRIER">Carrier</option>
|
|
<option value="SHIPPER">Shipper</option>
|
|
</select>
|
|
</div>
|
|
|
|
{formData.type === 'CARRIER' && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">SCAC Code *</label>
|
|
<input
|
|
type="text"
|
|
required={formData.type === 'CARRIER'}
|
|
maxLength={4}
|
|
value={formData.scac}
|
|
onChange={e => setFormData({ ...formData, scac: e.target.value.toUpperCase() })}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">SIREN</label>
|
|
<input
|
|
type="text"
|
|
maxLength={9}
|
|
value={formData.siren}
|
|
onChange={e => setFormData({ ...formData, siren: e.target.value })}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">EORI</label>
|
|
<input
|
|
type="text"
|
|
value={formData.eori}
|
|
onChange={e => setFormData({ ...formData, eori: e.target.value })}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">Contact Phone</label>
|
|
<input
|
|
type="tel"
|
|
value={formData.contact_phone}
|
|
onChange={e => setFormData({ ...formData, contact_phone: e.target.value })}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">Contact Email</label>
|
|
<input
|
|
type="email"
|
|
value={formData.contact_email}
|
|
onChange={e => setFormData({ ...formData, contact_email: e.target.value })}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700">Street Address *</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formData.address.street}
|
|
onChange={e => setFormData({
|
|
...formData,
|
|
address: { ...formData.address, street: e.target.value }
|
|
})}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">City *</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formData.address.city}
|
|
onChange={e => setFormData({
|
|
...formData,
|
|
address: { ...formData.address, city: e.target.value }
|
|
})}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">Postal Code *</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formData.address.postalCode}
|
|
onChange={e => setFormData({
|
|
...formData,
|
|
address: { ...formData.address, postalCode: e.target.value }
|
|
})}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">State/Region</label>
|
|
<input
|
|
type="text"
|
|
value={formData.address.state}
|
|
onChange={e => setFormData({
|
|
...formData,
|
|
address: { ...formData.address, state: e.target.value }
|
|
})}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">Country *</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
maxLength={2}
|
|
value={formData.address.country}
|
|
onChange={e => setFormData({
|
|
...formData,
|
|
address: { ...formData.address, country: e.target.value.toUpperCase() }
|
|
})}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700">Logo URL</label>
|
|
<input
|
|
type="url"
|
|
value={formData.logoUrl}
|
|
onChange={e => setFormData({ ...formData, logoUrl: e.target.value })}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end space-x-2 pt-4">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setShowCreateModal(false);
|
|
setShowEditModal(false);
|
|
setSelectedOrg(null);
|
|
resetForm();
|
|
}}
|
|
className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
|
>
|
|
{showCreateModal ? 'Create' : 'Update'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|