fix: rework org admin form with full business-rule validation
This commit is contained in:
parent
aa301eb447
commit
295874e11f
@ -30,6 +30,49 @@ interface Organization {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FormData {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
scac: string;
|
||||||
|
siren: string;
|
||||||
|
siret: string;
|
||||||
|
eori: string;
|
||||||
|
contact_phone: string;
|
||||||
|
contact_email: string;
|
||||||
|
address: {
|
||||||
|
street: string;
|
||||||
|
city: string;
|
||||||
|
state: string;
|
||||||
|
postalCode: string;
|
||||||
|
country: string;
|
||||||
|
};
|
||||||
|
logoUrl: string;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_FORM: FormData = {
|
||||||
|
name: '',
|
||||||
|
type: 'FREIGHT_FORWARDER',
|
||||||
|
scac: '',
|
||||||
|
siren: '',
|
||||||
|
siret: '',
|
||||||
|
eori: '',
|
||||||
|
contact_phone: '',
|
||||||
|
contact_email: '',
|
||||||
|
address: { street: '', city: '', state: '', postalCode: '', country: 'FR' },
|
||||||
|
logoUrl: '',
|
||||||
|
isActive: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
function FieldHint({ children }: { children: React.ReactNode }) {
|
||||||
|
return <p className="mt-1 text-xs text-gray-400">{children}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldError({ message }: { message?: string }) {
|
||||||
|
if (!message) return null;
|
||||||
|
return <p className="mt-1 text-xs text-red-600">{message}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
export default function AdminOrganizationsPage() {
|
export default function AdminOrganizationsPage() {
|
||||||
const t = useTranslations('dashboard.admin.organizations');
|
const t = useTranslations('dashboard.admin.organizations');
|
||||||
|
|
||||||
@ -40,43 +83,10 @@ export default function AdminOrganizationsPage() {
|
|||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
const [verifyingId, setVerifyingId] = useState<string | null>(null);
|
const [verifyingId, setVerifyingId] = useState<string | null>(null);
|
||||||
|
const [formData, setFormData] = useState<FormData>(EMPTY_FORM);
|
||||||
// Form state
|
const [formError, setFormError] = useState<string | null>(null);
|
||||||
const [formData, setFormData] = useState<{
|
const [fieldErrors, setFieldErrors] = useState<Partial<Record<string, string>>>({});
|
||||||
name: string;
|
const [submitting, setSubmitting] = useState(false);
|
||||||
type: string;
|
|
||||||
scac: string;
|
|
||||||
siren: string;
|
|
||||||
siret: string;
|
|
||||||
eori: string;
|
|
||||||
contact_phone: string;
|
|
||||||
contact_email: string;
|
|
||||||
address: {
|
|
||||||
street: string;
|
|
||||||
city: string;
|
|
||||||
state?: string;
|
|
||||||
postalCode: string;
|
|
||||||
country: string;
|
|
||||||
};
|
|
||||||
logoUrl: string;
|
|
||||||
}>({
|
|
||||||
name: '',
|
|
||||||
type: 'FREIGHT_FORWARDER',
|
|
||||||
scac: '',
|
|
||||||
siren: '',
|
|
||||||
siret: '',
|
|
||||||
eori: '',
|
|
||||||
contact_phone: '',
|
|
||||||
contact_email: '',
|
|
||||||
address: {
|
|
||||||
street: '',
|
|
||||||
city: '',
|
|
||||||
state: '',
|
|
||||||
postalCode: '',
|
|
||||||
country: 'FR',
|
|
||||||
},
|
|
||||||
logoUrl: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchOrganizations();
|
fetchOrganizations();
|
||||||
@ -95,70 +105,120 @@ export default function AdminOrganizationsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateForm = (isCreate: boolean): boolean => {
|
||||||
|
const errors: Partial<Record<string, string>> = {};
|
||||||
|
|
||||||
|
if (!formData.name.trim() || formData.name.trim().length < 2) {
|
||||||
|
errors.name = t('validation.nameTooShort');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCreate && formData.type === 'CARRIER') {
|
||||||
|
if (!formData.scac) {
|
||||||
|
errors.scac = t('validation.scacRequired');
|
||||||
|
} else if (!/^[A-Z]{4}$/.test(formData.scac)) {
|
||||||
|
errors.scac = t('validation.scacFormat');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.siren && !/^[0-9]{9}$/.test(formData.siren)) {
|
||||||
|
errors.siren = t('validation.sirenFormat');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.siret && !/^[0-9]{14}$/.test(formData.siret)) {
|
||||||
|
errors.siret = t('validation.siretFormat');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.address.street.trim()) errors.street = t('validation.required');
|
||||||
|
if (!formData.address.city.trim()) errors.city = t('validation.required');
|
||||||
|
if (!formData.address.postalCode.trim()) errors.postalCode = t('validation.required');
|
||||||
|
if (!/^[A-Z]{2}$/.test(formData.address.country)) {
|
||||||
|
errors.country = t('validation.countryFormat');
|
||||||
|
}
|
||||||
|
|
||||||
|
setFieldErrors(errors);
|
||||||
|
return Object.keys(errors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildApiAddress = () => ({
|
||||||
|
street: formData.address.street,
|
||||||
|
city: formData.address.city,
|
||||||
|
postalCode: formData.address.postalCode,
|
||||||
|
country: formData.address.country,
|
||||||
|
...(formData.address.state ? { state: formData.address.state } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
const handleCreate = async (e: React.FormEvent) => {
|
const handleCreate = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
setFormError(null);
|
||||||
|
if (!validateForm(true)) return;
|
||||||
|
|
||||||
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const apiData = {
|
await createOrganization({
|
||||||
name: formData.name,
|
name: formData.name.trim(),
|
||||||
type: formData.type as any,
|
type: formData.type as any,
|
||||||
address: {
|
address: buildApiAddress(),
|
||||||
street: formData.address.street,
|
|
||||||
city: formData.address.city,
|
|
||||||
postalCode: formData.address.postalCode,
|
|
||||||
country: formData.address.country,
|
|
||||||
...(formData.address.state ? { state: formData.address.state } : {}),
|
|
||||||
},
|
|
||||||
contact_email: formData.contact_email || undefined,
|
contact_email: formData.contact_email || undefined,
|
||||||
contact_phone: formData.contact_phone || undefined,
|
contact_phone: formData.contact_phone || undefined,
|
||||||
logo_url: formData.logoUrl || undefined,
|
logoUrl: formData.logoUrl || undefined,
|
||||||
...(formData.siren ? { siren: formData.siren } : {}),
|
...(formData.siren ? { siren: formData.siren } : {}),
|
||||||
...(formData.siret ? { siret: formData.siret } : {}),
|
...(formData.siret ? { siret: formData.siret } : {}),
|
||||||
...(formData.eori ? { eori: formData.eori } : {}),
|
...(formData.eori ? { eori: formData.eori } : {}),
|
||||||
...(formData.scac ? { scac: formData.scac } : {}),
|
...(formData.scac ? { scac: formData.scac } : {}),
|
||||||
};
|
});
|
||||||
await createOrganization(apiData);
|
|
||||||
await fetchOrganizations();
|
await fetchOrganizations();
|
||||||
setShowCreateModal(false);
|
setShowCreateModal(false);
|
||||||
resetForm();
|
resetForm();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert(err.message || t('createError'));
|
setFormError(err.message || t('createError'));
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdate = async (e: React.FormEvent) => {
|
const handleUpdate = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!selectedOrg) return;
|
if (!selectedOrg) return;
|
||||||
|
setFormError(null);
|
||||||
|
if (!validateForm(false)) return;
|
||||||
|
|
||||||
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await updateOrganization(selectedOrg.id, formData);
|
await updateOrganization(selectedOrg.id, {
|
||||||
|
name: formData.name.trim(),
|
||||||
|
address: buildApiAddress(),
|
||||||
|
contact_email: formData.contact_email || undefined,
|
||||||
|
contact_phone: formData.contact_phone || undefined,
|
||||||
|
logoUrl: formData.logoUrl || undefined,
|
||||||
|
isActive: formData.isActive,
|
||||||
|
...(formData.siren ? { siren: formData.siren } : {}),
|
||||||
|
...(formData.siret ? { siret: formData.siret } : {}),
|
||||||
|
...(formData.eori ? { eori: formData.eori } : {}),
|
||||||
|
});
|
||||||
await fetchOrganizations();
|
await fetchOrganizations();
|
||||||
setShowEditModal(false);
|
setShowEditModal(false);
|
||||||
setSelectedOrg(null);
|
setSelectedOrg(null);
|
||||||
resetForm();
|
resetForm();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert(err.message || t('updateError'));
|
setFormError(err.message || t('updateError'));
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
setFormData({
|
setFormData(EMPTY_FORM);
|
||||||
name: '',
|
setFormError(null);
|
||||||
type: 'FREIGHT_FORWARDER',
|
setFieldErrors({});
|
||||||
scac: '',
|
};
|
||||||
siren: '',
|
|
||||||
siret: '',
|
const handleTypeChange = (newType: string) => {
|
||||||
eori: '',
|
setFormData(prev => ({
|
||||||
contact_phone: '',
|
...prev,
|
||||||
contact_email: '',
|
type: newType,
|
||||||
address: {
|
scac: newType !== 'CARRIER' ? '' : prev.scac,
|
||||||
street: '',
|
}));
|
||||||
city: '',
|
setFieldErrors(prev => ({ ...prev, scac: undefined }));
|
||||||
state: '',
|
|
||||||
postalCode: '',
|
|
||||||
country: 'FR',
|
|
||||||
},
|
|
||||||
logoUrl: '',
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVerifySiret = async (orgId: string) => {
|
const handleVerifySiret = async (orgId: string) => {
|
||||||
@ -222,20 +282,41 @@ export default function AdminOrganizationsPage() {
|
|||||||
eori: org.eori || '',
|
eori: org.eori || '',
|
||||||
contact_phone: org.contact_phone || '',
|
contact_phone: org.contact_phone || '',
|
||||||
contact_email: org.contact_email || '',
|
contact_email: org.contact_email || '',
|
||||||
address: org.address,
|
address: {
|
||||||
|
street: org.address.street,
|
||||||
|
city: org.address.city,
|
||||||
|
state: org.address.state || '',
|
||||||
|
postalCode: org.address.postalCode,
|
||||||
|
country: org.address.country,
|
||||||
|
},
|
||||||
logoUrl: org.logoUrl || '',
|
logoUrl: org.logoUrl || '',
|
||||||
|
isActive: org.isActive,
|
||||||
});
|
});
|
||||||
|
setFormError(null);
|
||||||
|
setFieldErrors({});
|
||||||
setShowEditModal(true);
|
setShowEditModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setShowCreateModal(false);
|
||||||
|
setShowEditModal(false);
|
||||||
|
setSelectedOrg(null);
|
||||||
|
resetForm();
|
||||||
|
};
|
||||||
|
|
||||||
const getTypeLabel = (type: string) => {
|
const getTypeLabel = (type: string) => {
|
||||||
const allowed = ['FREIGHT_FORWARDER', 'CARRIER', 'SHIPPER'];
|
const allowed = ['FREIGHT_FORWARDER', 'CARRIER', 'SHIPPER'];
|
||||||
if (allowed.includes(type)) {
|
if (allowed.includes(type)) return t(`types.${type}` as any);
|
||||||
return t(`types.${type}` as any);
|
|
||||||
}
|
|
||||||
return type.replace('_', ' ');
|
return type.replace('_', ' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const inputClass = (hasError?: string) =>
|
||||||
|
`mt-1 block w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none text-sm ${
|
||||||
|
hasError
|
||||||
|
? 'border-red-400 focus:border-red-500 focus:ring-red-500'
|
||||||
|
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||||
|
}`;
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-96">
|
<div className="flex items-center justify-center h-96">
|
||||||
@ -262,7 +343,6 @@ export default function AdminOrganizationsPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Error Message */}
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg">
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg">
|
||||||
{error}
|
{error}
|
||||||
@ -382,247 +462,365 @@ export default function AdminOrganizationsPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Create/Edit Modal */}
|
{/* Create / Edit Modal */}
|
||||||
{(showCreateModal || showEditModal) && (
|
{(showCreateModal || showEditModal) && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 overflow-y-auto">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 overflow-y-auto py-4">
|
||||||
<div className="bg-white rounded-lg p-6 max-w-2xl w-full m-4 max-h-[90vh] overflow-y-auto">
|
<div className="bg-white rounded-lg p-6 max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||||
<h2 className="text-xl font-bold mb-4">
|
<h2 className="text-xl font-bold mb-6">
|
||||||
{showCreateModal ? t('modal.createTitle') : t('modal.editTitle')}
|
{showCreateModal ? t('modal.createTitle') : t('modal.editTitle')}
|
||||||
</h2>
|
</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">
|
|
||||||
{t('modal.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>
|
<form
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
onSubmit={showCreateModal ? handleCreate : handleUpdate}
|
||||||
{t('modal.type')}
|
noValidate
|
||||||
</label>
|
className="space-y-5"
|
||||||
<select
|
>
|
||||||
value={formData.type}
|
{/* ── Informations générales ── */}
|
||||||
onChange={e => setFormData({ ...formData, type: e.target.value })}
|
<section>
|
||||||
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"
|
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
|
||||||
>
|
{t('modal.sectionGeneral')}
|
||||||
<option value="FREIGHT_FORWARDER">{t('types.FREIGHT_FORWARDER')}</option>
|
</h3>
|
||||||
<option value="CARRIER">{t('types.CARRIER')}</option>
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<option value="SHIPPER">{t('types.SHIPPER')}</option>
|
<div className="col-span-2">
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{formData.type === 'CARRIER' && (
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
{t('modal.scacLabel')}
|
{t('modal.name')} <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
required={formData.type === 'CARRIER'}
|
value={formData.name}
|
||||||
maxLength={4}
|
onChange={e => {
|
||||||
value={formData.scac}
|
setFormData({ ...formData, name: e.target.value });
|
||||||
onChange={e =>
|
setFieldErrors(prev => ({ ...prev, name: undefined }));
|
||||||
setFormData({ ...formData, scac: e.target.value.toUpperCase() })
|
}}
|
||||||
}
|
minLength={2}
|
||||||
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"
|
maxLength={200}
|
||||||
|
className={inputClass(fieldErrors.name)}
|
||||||
|
placeholder="Acme Freight Forwarding"
|
||||||
|
/>
|
||||||
|
<FieldError message={fieldErrors.name} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
{t('modal.type')} <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
{showCreateModal ? (
|
||||||
|
<select
|
||||||
|
value={formData.type}
|
||||||
|
onChange={e => handleTypeChange(e.target.value)}
|
||||||
|
className={inputClass()}
|
||||||
|
>
|
||||||
|
<option value="FREIGHT_FORWARDER">{t('types.FREIGHT_FORWARDER')}</option>
|
||||||
|
<option value="CARRIER">{t('types.CARRIER')}</option>
|
||||||
|
<option value="SHIPPER">{t('types.SHIPPER')}</option>
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<div className="mt-1 flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={`px-2 py-1 text-xs font-semibold rounded-full ${
|
||||||
|
formData.type === 'FREIGHT_FORWARDER'
|
||||||
|
? 'bg-blue-100 text-blue-800'
|
||||||
|
: formData.type === 'CARRIER'
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: 'bg-purple-100 text-purple-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{getTypeLabel(formData.type)}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-400">{t('modal.typeReadOnly')}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* SCAC — création uniquement, visible en lecture seule en édition si CARRIER */}
|
||||||
|
{showCreateModal && formData.type === 'CARRIER' && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
{t('modal.scacLabel')} <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.scac}
|
||||||
|
onChange={e => {
|
||||||
|
const v = e.target.value.toUpperCase().replace(/[^A-Z]/g, '');
|
||||||
|
setFormData({ ...formData, scac: v });
|
||||||
|
setFieldErrors(prev => ({ ...prev, scac: undefined }));
|
||||||
|
}}
|
||||||
|
maxLength={4}
|
||||||
|
minLength={4}
|
||||||
|
className={inputClass(fieldErrors.scac)}
|
||||||
|
placeholder="MAEU"
|
||||||
|
/>
|
||||||
|
<FieldHint>{t('modal.scacHint')}</FieldHint>
|
||||||
|
<FieldError message={fieldErrors.scac} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{showEditModal && formData.type === 'CARRIER' && formData.scac && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
{t('modal.scacLabel')}
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 px-3 py-2 bg-gray-50 border border-gray-200 rounded-md text-sm text-gray-700 font-mono">
|
||||||
|
{formData.scac}
|
||||||
|
</div>
|
||||||
|
<FieldHint>{t('modal.typeReadOnly')}</FieldHint>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ── Identifiants légaux ── */}
|
||||||
|
<section>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
|
||||||
|
{t('modal.sectionLegal')}
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
{t('modal.sirenLabel')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.siren}
|
||||||
|
onChange={e => {
|
||||||
|
const v = e.target.value.replace(/\D/g, '').slice(0, 9);
|
||||||
|
setFormData({ ...formData, siren: v });
|
||||||
|
setFieldErrors(prev => ({ ...prev, siren: undefined }));
|
||||||
|
}}
|
||||||
|
maxLength={9}
|
||||||
|
className={inputClass(fieldErrors.siren)}
|
||||||
|
placeholder="123456789"
|
||||||
|
/>
|
||||||
|
<FieldHint>{t('modal.sirenHint')}</FieldHint>
|
||||||
|
<FieldError message={fieldErrors.siren} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
{t('modal.siretLabel')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.siret}
|
||||||
|
onChange={e => {
|
||||||
|
const v = e.target.value.replace(/\D/g, '').slice(0, 14);
|
||||||
|
setFormData({ ...formData, siret: v });
|
||||||
|
setFieldErrors(prev => ({ ...prev, siret: undefined }));
|
||||||
|
}}
|
||||||
|
maxLength={14}
|
||||||
|
className={inputClass(fieldErrors.siret)}
|
||||||
|
placeholder="12345678901234"
|
||||||
|
/>
|
||||||
|
<FieldHint>{t('modal.siretHint')}</FieldHint>
|
||||||
|
<FieldError message={fieldErrors.siret} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
{t('modal.eoriLabel')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.eori}
|
||||||
|
onChange={e => setFormData({ ...formData, eori: e.target.value })}
|
||||||
|
className={inputClass()}
|
||||||
|
placeholder="FR123456789"
|
||||||
|
/>
|
||||||
|
<FieldHint>{t('modal.eoriHint')}</FieldHint>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ── Contact ── */}
|
||||||
|
<section>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
|
||||||
|
{t('modal.sectionContact')}
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
{t('modal.contactEmail')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={formData.contact_email}
|
||||||
|
onChange={e => setFormData({ ...formData, contact_email: e.target.value })}
|
||||||
|
className={inputClass()}
|
||||||
|
placeholder="contact@exemple.com"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
{t('modal.sirenLabel')}
|
{t('modal.contactPhone')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="tel"
|
||||||
maxLength={9}
|
value={formData.contact_phone}
|
||||||
value={formData.siren}
|
onChange={e => setFormData({ ...formData, contact_phone: e.target.value })}
|
||||||
onChange={e => setFormData({ ...formData, siren: e.target.value })}
|
className={inputClass()}
|
||||||
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"
|
placeholder="+33 6 00 00 00 00"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div>
|
{/* ── Adresse ── */}
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<section>
|
||||||
{t('modal.siretLabel')}
|
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
|
||||||
</label>
|
{t('modal.sectionAddress')}
|
||||||
<input
|
</h3>
|
||||||
type="text"
|
<div className="grid grid-cols-2 gap-4">
|
||||||
maxLength={14}
|
<div className="col-span-2">
|
||||||
value={formData.siret}
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
onChange={e =>
|
{t('modal.street')} <span className="text-red-500">*</span>
|
||||||
setFormData({ ...formData, siret: e.target.value.replace(/\D/g, '') })
|
</label>
|
||||||
}
|
<input
|
||||||
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"
|
type="text"
|
||||||
placeholder={t('modal.siretPlaceholder')}
|
value={formData.address.street}
|
||||||
/>
|
onChange={e => {
|
||||||
|
setFormData({ ...formData, address: { ...formData.address, street: e.target.value } });
|
||||||
|
setFieldErrors(prev => ({ ...prev, street: undefined }));
|
||||||
|
}}
|
||||||
|
className={inputClass(fieldErrors.street)}
|
||||||
|
placeholder="123 Rue de la République"
|
||||||
|
/>
|
||||||
|
<FieldError message={fieldErrors.street} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
{t('modal.city')} <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.address.city}
|
||||||
|
onChange={e => {
|
||||||
|
setFormData({ ...formData, address: { ...formData.address, city: e.target.value } });
|
||||||
|
setFieldErrors(prev => ({ ...prev, city: undefined }));
|
||||||
|
}}
|
||||||
|
className={inputClass(fieldErrors.city)}
|
||||||
|
placeholder="Paris"
|
||||||
|
/>
|
||||||
|
<FieldError message={fieldErrors.city} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
{t('modal.postalCode')} <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.address.postalCode}
|
||||||
|
onChange={e => {
|
||||||
|
setFormData({ ...formData, address: { ...formData.address, postalCode: e.target.value } });
|
||||||
|
setFieldErrors(prev => ({ ...prev, postalCode: undefined }));
|
||||||
|
}}
|
||||||
|
className={inputClass(fieldErrors.postalCode)}
|
||||||
|
placeholder="75001"
|
||||||
|
/>
|
||||||
|
<FieldError message={fieldErrors.postalCode} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
{t('modal.state')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.address.state}
|
||||||
|
onChange={e =>
|
||||||
|
setFormData({ ...formData, address: { ...formData.address, state: e.target.value } })
|
||||||
|
}
|
||||||
|
className={inputClass()}
|
||||||
|
placeholder="Île-de-France"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
{t('modal.country')} <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.address.country}
|
||||||
|
onChange={e => {
|
||||||
|
const v = e.target.value.toUpperCase().replace(/[^A-Z]/g, '').slice(0, 2);
|
||||||
|
setFormData({ ...formData, address: { ...formData.address, country: v } });
|
||||||
|
setFieldErrors(prev => ({ ...prev, country: undefined }));
|
||||||
|
}}
|
||||||
|
maxLength={2}
|
||||||
|
minLength={2}
|
||||||
|
className={inputClass(fieldErrors.country)}
|
||||||
|
placeholder="FR"
|
||||||
|
/>
|
||||||
|
<FieldHint>{t('modal.countryHint')}</FieldHint>
|
||||||
|
<FieldError message={fieldErrors.country} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div>
|
{/* ── Autres ── */}
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<section>
|
||||||
{t('modal.eoriLabel')}
|
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
|
||||||
</label>
|
{t('modal.sectionOther')}
|
||||||
<input
|
</h3>
|
||||||
type="text"
|
<div className="space-y-4">
|
||||||
value={formData.eori}
|
<div>
|
||||||
onChange={e => setFormData({ ...formData, eori: e.target.value })}
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
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"
|
{t('modal.logoUrl')}
|
||||||
/>
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={formData.logoUrl}
|
||||||
|
onChange={e => setFormData({ ...formData, logoUrl: e.target.value })}
|
||||||
|
className={inputClass()}
|
||||||
|
placeholder="https://exemple.com/logo.png"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showEditModal && (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<input
|
||||||
|
id="isActive"
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.isActive}
|
||||||
|
onChange={e => setFormData({ ...formData, isActive: e.target.checked })}
|
||||||
|
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<label htmlFor="isActive" className="text-sm font-medium text-gray-700">
|
||||||
|
{t('modal.isActive')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div>
|
{/* Erreur globale */}
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
{formError && (
|
||||||
{t('modal.contactPhone')}
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md text-sm">
|
||||||
</label>
|
{formError}
|
||||||
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div className="flex justify-end space-x-3 pt-2 border-t border-gray-100">
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
|
||||||
{t('modal.contactEmail')}
|
|
||||||
</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">
|
|
||||||
{t('modal.street')}
|
|
||||||
</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">
|
|
||||||
{t('modal.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">
|
|
||||||
{t('modal.postalCode')}
|
|
||||||
</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">
|
|
||||||
{t('modal.state')}
|
|
||||||
</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">
|
|
||||||
{t('modal.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">
|
|
||||||
{t('modal.logoUrl')}
|
|
||||||
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={closeModal}
|
||||||
setShowCreateModal(false);
|
className="px-4 py-2 border border-gray-300 rounded-md text-sm text-gray-700 hover:bg-gray-50 transition-colors"
|
||||||
setShowEditModal(false);
|
|
||||||
setSelectedOrg(null);
|
|
||||||
resetForm();
|
|
||||||
}}
|
|
||||||
className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
|
|
||||||
>
|
>
|
||||||
{t('modal.cancel')}
|
{t('modal.cancel')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
disabled={submitting}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 disabled:opacity-60 disabled:cursor-not-allowed transition-colors"
|
||||||
>
|
>
|
||||||
{showCreateModal ? t('modal.create') : t('modal.update')}
|
{submitting
|
||||||
|
? t('modal.saving')
|
||||||
|
: showCreateModal
|
||||||
|
? t('modal.create')
|
||||||
|
: t('modal.update')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -1200,24 +1200,46 @@
|
|||||||
"modal": {
|
"modal": {
|
||||||
"createTitle": "Create New Organization",
|
"createTitle": "Create New Organization",
|
||||||
"editTitle": "Edit Organization",
|
"editTitle": "Edit Organization",
|
||||||
"name": "Organization Name *",
|
"sectionGeneral": "General information",
|
||||||
"type": "Type *",
|
"sectionLegal": "Legal identifiers",
|
||||||
"scacLabel": "SCAC Code *",
|
"sectionContact": "Contact",
|
||||||
|
"sectionAddress": "Address",
|
||||||
|
"sectionOther": "Other",
|
||||||
|
"name": "Organization name",
|
||||||
|
"type": "Type",
|
||||||
|
"typeReadOnly": "Cannot be changed after creation",
|
||||||
|
"scacLabel": "SCAC code",
|
||||||
|
"scacHint": "4 uppercase letters (e.g. MAEU). Required for carriers.",
|
||||||
"sirenLabel": "SIREN",
|
"sirenLabel": "SIREN",
|
||||||
"siretLabel": "SIRET (14 digits)",
|
"sirenHint": "9 digits — optional",
|
||||||
|
"siretLabel": "SIRET",
|
||||||
|
"siretHint": "14 digits — optional",
|
||||||
"siretPlaceholder": "12345678901234",
|
"siretPlaceholder": "12345678901234",
|
||||||
"eoriLabel": "EORI",
|
"eoriLabel": "EORI",
|
||||||
"contactPhone": "Contact Phone",
|
"eoriHint": "EU EORI number (e.g. FR123456789) — optional",
|
||||||
"contactEmail": "Contact Email",
|
"contactEmail": "Contact email",
|
||||||
"street": "Street Address *",
|
"contactPhone": "Contact phone",
|
||||||
"city": "City *",
|
"street": "Street address",
|
||||||
"postalCode": "Postal Code *",
|
"city": "City",
|
||||||
|
"postalCode": "Postal code",
|
||||||
"state": "State / Region",
|
"state": "State / Region",
|
||||||
"country": "Country *",
|
"country": "Country",
|
||||||
|
"countryHint": "2-letter ISO code (e.g. FR, DE, US)",
|
||||||
"logoUrl": "Logo URL",
|
"logoUrl": "Logo URL",
|
||||||
|
"isActive": "Active organization",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"update": "Update"
|
"update": "Update",
|
||||||
|
"saving": "Saving..."
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"required": "This field is required",
|
||||||
|
"nameTooShort": "Name must be at least 2 characters",
|
||||||
|
"scacRequired": "SCAC code is required for carriers",
|
||||||
|
"scacFormat": "SCAC must be exactly 4 uppercase letters (e.g. MAEU)",
|
||||||
|
"sirenFormat": "SIREN must be exactly 9 digits",
|
||||||
|
"siretFormat": "SIRET must be exactly 14 digits",
|
||||||
|
"countryFormat": "Country must be a 2-letter ISO code (e.g. FR)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
|
|||||||
@ -1200,24 +1200,46 @@
|
|||||||
"modal": {
|
"modal": {
|
||||||
"createTitle": "Créer une nouvelle organisation",
|
"createTitle": "Créer une nouvelle organisation",
|
||||||
"editTitle": "Modifier l'organisation",
|
"editTitle": "Modifier l'organisation",
|
||||||
"name": "Nom de l'organisation *",
|
"sectionGeneral": "Informations générales",
|
||||||
"type": "Type *",
|
"sectionLegal": "Identifiants légaux",
|
||||||
"scacLabel": "Code SCAC *",
|
"sectionContact": "Contact",
|
||||||
|
"sectionAddress": "Adresse",
|
||||||
|
"sectionOther": "Autres",
|
||||||
|
"name": "Nom de l'organisation",
|
||||||
|
"type": "Type",
|
||||||
|
"typeReadOnly": "Non modifiable après création",
|
||||||
|
"scacLabel": "Code SCAC",
|
||||||
|
"scacHint": "4 lettres majuscules (ex. MAEU). Obligatoire pour les transporteurs.",
|
||||||
"sirenLabel": "SIREN",
|
"sirenLabel": "SIREN",
|
||||||
"siretLabel": "SIRET (14 chiffres)",
|
"sirenHint": "9 chiffres — facultatif",
|
||||||
|
"siretLabel": "SIRET",
|
||||||
|
"siretHint": "14 chiffres — facultatif",
|
||||||
"siretPlaceholder": "12345678901234",
|
"siretPlaceholder": "12345678901234",
|
||||||
"eoriLabel": "EORI",
|
"eoriLabel": "EORI",
|
||||||
"contactPhone": "Téléphone de contact",
|
"eoriHint": "Numéro EORI européen (ex. FR123456789) — facultatif",
|
||||||
"contactEmail": "Email de contact",
|
"contactEmail": "Email de contact",
|
||||||
"street": "Rue *",
|
"contactPhone": "Téléphone de contact",
|
||||||
"city": "Ville *",
|
"street": "Rue",
|
||||||
"postalCode": "Code postal *",
|
"city": "Ville",
|
||||||
|
"postalCode": "Code postal",
|
||||||
"state": "État / Région",
|
"state": "État / Région",
|
||||||
"country": "Pays *",
|
"country": "Pays",
|
||||||
|
"countryHint": "Code ISO 2 lettres majuscules (ex. FR, DE, US)",
|
||||||
"logoUrl": "URL du logo",
|
"logoUrl": "URL du logo",
|
||||||
|
"isActive": "Organisation active",
|
||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
"create": "Créer",
|
"create": "Créer",
|
||||||
"update": "Mettre à jour"
|
"update": "Mettre à jour",
|
||||||
|
"saving": "Enregistrement..."
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"required": "Ce champ est obligatoire",
|
||||||
|
"nameTooShort": "Le nom doit comporter au moins 2 caractères",
|
||||||
|
"scacRequired": "Le code SCAC est obligatoire pour les transporteurs",
|
||||||
|
"scacFormat": "Le code SCAC doit comporter exactement 4 lettres majuscules (ex. MAEU)",
|
||||||
|
"sirenFormat": "Le SIREN doit comporter exactement 9 chiffres",
|
||||||
|
"siretFormat": "Le SIRET doit comporter exactement 14 chiffres",
|
||||||
|
"countryFormat": "Le pays doit être un code ISO à 2 lettres majuscules (ex. FR)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
|
|||||||
@ -124,7 +124,7 @@ export interface CreateOrganizationRequest {
|
|||||||
scac?: string;
|
scac?: string;
|
||||||
contact_email?: string;
|
contact_email?: string;
|
||||||
contact_phone?: string;
|
contact_phone?: string;
|
||||||
logo_url?: string;
|
logoUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateOrganizationRequest {
|
export interface UpdateOrganizationRequest {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user