fix: rework org admin form with full business-rule validation

This commit is contained in:
David 2026-06-07 18:11:09 +02:00
parent aa301eb447
commit 295874e11f
4 changed files with 553 additions and 311 deletions

View File

@ -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>

View File

@ -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": {

View File

@ -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": {

View File

@ -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 {