From c76f908d5c6df54a124e969a62d879a199d80c19 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 30 Nov 2025 18:39:08 +0100 Subject: [PATCH] fix error get organisation --- .../controllers/organizations.controller.ts | 16 + .../src/application/dto/organization.dto.ts | 98 +++++ .../mappers/organization.mapper.ts | 4 + .../domain/entities/organization.entity.ts | 40 ++ .../entities/organization.orm-entity.ts | 12 + .../mappers/organization-orm.mapper.ts | 8 + ...3000000000-AddOrganizationContactFields.ts | 56 +++ .../dashboard/settings/organization/page.tsx | 376 +++++++++--------- apps/frontend/lib/api/index.ts | 3 +- apps/frontend/lib/api/organizations.ts | 3 + apps/frontend/src/types/api.ts | 48 ++- 11 files changed, 469 insertions(+), 195 deletions(-) create mode 100644 apps/backend/src/infrastructure/persistence/typeorm/migrations/1733000000000-AddOrganizationContactFields.ts diff --git a/apps/backend/src/application/controllers/organizations.controller.ts b/apps/backend/src/application/controllers/organizations.controller.ts index 9f1893e..49359aa 100644 --- a/apps/backend/src/application/controllers/organizations.controller.ts +++ b/apps/backend/src/application/controllers/organizations.controller.ts @@ -248,6 +248,22 @@ export class OrganizationsController { organization.updateName(dto.name); } + if (dto.siren) { + organization.updateSiren(dto.siren); + } + + if (dto.eori) { + organization.updateEori(dto.eori); + } + + if (dto.contact_phone) { + organization.updateContactPhone(dto.contact_phone); + } + + if (dto.contact_email) { + organization.updateContactEmail(dto.contact_email); + } + if (dto.address) { organization.updateAddress(OrganizationMapper.mapDtoToAddress(dto.address)); } diff --git a/apps/backend/src/application/dto/organization.dto.ts b/apps/backend/src/application/dto/organization.dto.ts index af426d3..881322c 100644 --- a/apps/backend/src/application/dto/organization.dto.ts +++ b/apps/backend/src/application/dto/organization.dto.ts @@ -101,6 +101,43 @@ export class CreateOrganizationDto { @Matches(/^[A-Z]{4}$/, { message: 'SCAC must be 4 uppercase letters (e.g., MAEU, MSCU)' }) scac?: string; + @ApiPropertyOptional({ + example: '123456789', + description: 'French SIREN number (9 digits)', + minLength: 9, + maxLength: 9, + }) + @IsString() + @IsOptional() + @MinLength(9) + @MaxLength(9) + @Matches(/^[0-9]{9}$/, { message: 'SIREN must be 9 digits' }) + siren?: string; + + @ApiPropertyOptional({ + example: 'FR123456789', + description: 'EU EORI number', + }) + @IsString() + @IsOptional() + eori?: string; + + @ApiPropertyOptional({ + example: '+33 6 80 18 28 12', + description: 'Contact phone number', + }) + @IsString() + @IsOptional() + contact_phone?: string; + + @ApiPropertyOptional({ + example: 'contact@xpeditis.com', + description: 'Contact email address', + }) + @IsString() + @IsOptional() + contact_email?: string; + @ApiProperty({ description: 'Organization address', type: AddressDto, @@ -134,6 +171,43 @@ export class UpdateOrganizationDto { @MaxLength(200) name?: string; + @ApiPropertyOptional({ + example: '123456789', + description: 'French SIREN number (9 digits)', + minLength: 9, + maxLength: 9, + }) + @IsString() + @IsOptional() + @MinLength(9) + @MaxLength(9) + @Matches(/^[0-9]{9}$/, { message: 'SIREN must be 9 digits' }) + siren?: string; + + @ApiPropertyOptional({ + example: 'FR123456789', + description: 'EU EORI number', + }) + @IsString() + @IsOptional() + eori?: string; + + @ApiPropertyOptional({ + example: '+33 6 80 18 28 12', + description: 'Contact phone number', + }) + @IsString() + @IsOptional() + contact_phone?: string; + + @ApiPropertyOptional({ + example: 'contact@xpeditis.com', + description: 'Contact email address', + }) + @IsString() + @IsOptional() + contact_email?: string; + @ApiPropertyOptional({ description: 'Organization address', type: AddressDto, @@ -228,6 +302,30 @@ export class OrganizationResponseDto { }) scac?: string; + @ApiPropertyOptional({ + example: '123456789', + description: 'French SIREN number (9 digits)', + }) + siren?: string; + + @ApiPropertyOptional({ + example: 'FR123456789', + description: 'EU EORI number', + }) + eori?: string; + + @ApiPropertyOptional({ + example: '+33 6 80 18 28 12', + description: 'Contact phone number', + }) + contact_phone?: string; + + @ApiPropertyOptional({ + example: 'contact@xpeditis.com', + description: 'Contact email address', + }) + contact_email?: string; + @ApiProperty({ description: 'Organization address', type: AddressDto, diff --git a/apps/backend/src/application/mappers/organization.mapper.ts b/apps/backend/src/application/mappers/organization.mapper.ts index af53172..8405e33 100644 --- a/apps/backend/src/application/mappers/organization.mapper.ts +++ b/apps/backend/src/application/mappers/organization.mapper.ts @@ -24,6 +24,10 @@ export class OrganizationMapper { name: organization.name, type: organization.type, scac: organization.scac, + siren: organization.siren, + eori: organization.eori, + contact_phone: organization.contactPhone, + contact_email: organization.contactEmail, address: this.mapAddressToDto(organization.address), logoUrl: organization.logoUrl, documents: organization.documents.map(doc => this.mapDocumentToDto(doc)), diff --git a/apps/backend/src/domain/entities/organization.entity.ts b/apps/backend/src/domain/entities/organization.entity.ts index 5907e35..32baac5 100644 --- a/apps/backend/src/domain/entities/organization.entity.ts +++ b/apps/backend/src/domain/entities/organization.entity.ts @@ -37,6 +37,10 @@ export interface OrganizationProps { name: string; type: OrganizationType; scac?: string; // Standard Carrier Alpha Code (for carriers only) + siren?: string; // French SIREN number (9 digits) + eori?: string; // EU EORI number + contact_phone?: string; // Contact phone number + contact_email?: string; // Contact email address address: OrganizationAddress; logoUrl?: string; documents: OrganizationDocument[]; @@ -113,6 +117,22 @@ export class Organization { return this.props.scac; } + get siren(): string | undefined { + return this.props.siren; + } + + get eori(): string | undefined { + return this.props.eori; + } + + get contactPhone(): string | undefined { + return this.props.contact_phone; + } + + get contactEmail(): string | undefined { + return this.props.contact_email; + } + get address(): OrganizationAddress { return { ...this.props.address }; } @@ -163,6 +183,26 @@ export class Organization { this.props.updatedAt = new Date(); } + updateSiren(siren: string): void { + this.props.siren = siren; + this.props.updatedAt = new Date(); + } + + updateEori(eori: string): void { + this.props.eori = eori; + this.props.updatedAt = new Date(); + } + + updateContactPhone(phone: string): void { + this.props.contact_phone = phone; + this.props.updatedAt = new Date(); + } + + updateContactEmail(email: string): void { + this.props.contact_email = email; + this.props.updatedAt = new Date(); + } + updateLogoUrl(logoUrl: string): void { this.props.logoUrl = logoUrl; this.props.updatedAt = new Date(); diff --git a/apps/backend/src/infrastructure/persistence/typeorm/entities/organization.orm-entity.ts b/apps/backend/src/infrastructure/persistence/typeorm/entities/organization.orm-entity.ts index 2a75d7e..392a2a0 100644 --- a/apps/backend/src/infrastructure/persistence/typeorm/entities/organization.orm-entity.ts +++ b/apps/backend/src/infrastructure/persistence/typeorm/entities/organization.orm-entity.ts @@ -23,6 +23,18 @@ export class OrganizationOrmEntity { @Column({ type: 'char', length: 4, nullable: true, unique: true }) scac: string | null; + @Column({ type: 'char', length: 9, nullable: true }) + siren: string | null; + + @Column({ type: 'varchar', length: 17, nullable: true }) + eori: string | null; + + @Column({ name: 'contact_phone', type: 'varchar', length: 50, nullable: true }) + contactPhone: string | null; + + @Column({ name: 'contact_email', type: 'varchar', length: 255, nullable: true }) + contactEmail: string | null; + @Column({ name: 'address_street', type: 'varchar', length: 255 }) addressStreet: string; diff --git a/apps/backend/src/infrastructure/persistence/typeorm/mappers/organization-orm.mapper.ts b/apps/backend/src/infrastructure/persistence/typeorm/mappers/organization-orm.mapper.ts index 306ec29..78f6660 100644 --- a/apps/backend/src/infrastructure/persistence/typeorm/mappers/organization-orm.mapper.ts +++ b/apps/backend/src/infrastructure/persistence/typeorm/mappers/organization-orm.mapper.ts @@ -19,6 +19,10 @@ export class OrganizationOrmMapper { orm.name = props.name; orm.type = props.type; orm.scac = props.scac || null; + orm.siren = props.siren || null; + orm.eori = props.eori || null; + orm.contactPhone = props.contact_phone || null; + orm.contactEmail = props.contact_email || null; orm.addressStreet = props.address.street; orm.addressCity = props.address.city; orm.addressState = props.address.state || null; @@ -42,6 +46,10 @@ export class OrganizationOrmMapper { name: orm.name, type: orm.type as any, scac: orm.scac || undefined, + siren: orm.siren || undefined, + eori: orm.eori || undefined, + contact_phone: orm.contactPhone || undefined, + contact_email: orm.contactEmail || undefined, address: { street: orm.addressStreet, city: orm.addressCity, diff --git a/apps/backend/src/infrastructure/persistence/typeorm/migrations/1733000000000-AddOrganizationContactFields.ts b/apps/backend/src/infrastructure/persistence/typeorm/migrations/1733000000000-AddOrganizationContactFields.ts new file mode 100644 index 0000000..25ab242 --- /dev/null +++ b/apps/backend/src/infrastructure/persistence/typeorm/migrations/1733000000000-AddOrganizationContactFields.ts @@ -0,0 +1,56 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class AddOrganizationContactFields1733000000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // Add siren column + await queryRunner.addColumn( + 'organizations', + new TableColumn({ + name: 'siren', + type: 'char', + length: '9', + isNullable: true, + }) + ); + + // Add eori column + await queryRunner.addColumn( + 'organizations', + new TableColumn({ + name: 'eori', + type: 'varchar', + length: '17', + isNullable: true, + }) + ); + + // Add contact_phone column + await queryRunner.addColumn( + 'organizations', + new TableColumn({ + name: 'contact_phone', + type: 'varchar', + length: '50', + isNullable: true, + }) + ); + + // Add contact_email column + await queryRunner.addColumn( + 'organizations', + new TableColumn({ + name: 'contact_email', + type: 'varchar', + length: '255', + isNullable: true, + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn('organizations', 'contact_email'); + await queryRunner.dropColumn('organizations', 'contact_phone'); + await queryRunner.dropColumn('organizations', 'eori'); + await queryRunner.dropColumn('organizations', 'siren'); + } +} diff --git a/apps/frontend/app/dashboard/settings/organization/page.tsx b/apps/frontend/app/dashboard/settings/organization/page.tsx index 79cd6bb..eb31743 100644 --- a/apps/frontend/app/dashboard/settings/organization/page.tsx +++ b/apps/frontend/app/dashboard/settings/organization/page.tsx @@ -7,8 +7,8 @@ import type { OrganizationResponse } from '@/types/api'; interface OrganizationForm { name: string; - siren: string; // TODO: Add to backend - eori: string; // TODO: Add to backend + siren: string; + eori: string; contact_phone: string; contact_email: string; address_street: string; @@ -17,8 +17,11 @@ interface OrganizationForm { address_country: string; } +type TabType = 'information' | 'address'; + export default function OrganizationSettingsPage() { const { user } = useAuth(); + const [activeTab, setActiveTab] = useState('information'); const [organization, setOrganization] = useState(null); const [formData, setFormData] = useState({ name: '', @@ -49,15 +52,15 @@ export default function OrganizationSettingsPage() { const org = await getOrganization(user!.organizationId); setOrganization(org); setFormData({ - name: org.name, - siren: '', // TODO: Get from backend when available - eori: '', // TODO: Get from backend when available + name: org.name || '', + siren: org.siren || '', + eori: org.eori || '', contact_phone: org.contact_phone || '', contact_email: org.contact_email || '', - address_street: org.address_street, - address_city: org.address_city, - address_postal_code: org.address_postal_code, - address_country: org.address_country, + address_street: org.address?.street || '', + address_city: org.address?.city || '', + address_postal_code: org.address?.postalCode || '', + address_country: org.address?.country || 'FR', }); } catch (err) { console.error('Failed to load organization:', err); @@ -75,15 +78,15 @@ export default function OrganizationSettingsPage() { const handleCancel = () => { if (organization) { setFormData({ - name: organization.name, - siren: '', - eori: '', + name: organization.name || '', + siren: organization.siren || '', + eori: organization.eori || '', contact_phone: organization.contact_phone || '', contact_email: organization.contact_email || '', - address_street: organization.address_street, - address_city: organization.address_city, - address_postal_code: organization.address_postal_code, - address_country: organization.address_country, + address_street: organization.address?.street || '', + address_city: organization.address?.city || '', + address_postal_code: organization.address?.postalCode || '', + address_country: organization.address?.country || 'FR', }); setSuccessMessage(null); setError(null); @@ -98,27 +101,22 @@ export default function OrganizationSettingsPage() { setError(null); setSuccessMessage(null); - // Update organization (excluding SIREN and EORI for now) const updatedOrg = await updateOrganization(user.organizationId, { name: formData.name, + siren: formData.siren, + eori: formData.eori, contact_phone: formData.contact_phone, contact_email: formData.contact_email, - address_street: formData.address_street, - address_city: formData.address_city, - address_postal_code: formData.address_postal_code, - address_country: formData.address_country, + address: { + street: formData.address_street, + city: formData.address_city, + postalCode: formData.address_postal_code, + country: formData.address_country, + }, }); setOrganization(updatedOrg); setSuccessMessage('Informations sauvegardées avec succès'); - - // TODO: Save SIREN and EORI when backend supports them - if (formData.siren || formData.eori) { - console.log('SIREN/EORI will be saved when backend is updated:', { - siren: formData.siren, - eori: formData.eori, - }); - } } catch (err) { console.error('Failed to update organization:', err); setError(err instanceof Error ? err.message : 'Erreur lors de la sauvegarde'); @@ -161,7 +159,9 @@ export default function OrganizationSettingsPage() { {successMessage && (
- + + +

{successMessage}

@@ -171,164 +171,202 @@ export default function OrganizationSettingsPage() { {error && (
- + + +

{error}

)} - {/* Form */} + {/* Tabs */}
+
+ +
+ + {/* Tab Content */}
-

Informations

- -
- {/* Nom de la société */} -
- - handleChange('name', e.target.value)} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="Xpeditis" - required - /> -
- - {/* SIREN */} -
- - handleChange('siren', e.target.value.replace(/\D/g, '').slice(0, 9))} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="123 456 789" - maxLength={9} - /> -

9 chiffres

-
- - {/* Numéro EORI */} -
- - handleChange('eori', e.target.value.toUpperCase())} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="FR123456789" - maxLength={17} - /> -

Code pays (2 lettres) + numéro unique (max 15 caractères)

-
- - {/* Téléphone */} -
- - handleChange('contact_phone', e.target.value)} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="06 80 18 28 12" - /> -
- - {/* Email */} -
- - handleChange('contact_email', e.target.value)} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="contact@xpeditis.com" - /> -
- - {/* Divider */} -
-

Adresse

-
- - {/* Rue */} -
- - handleChange('address_street', e.target.value)} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="123 Rue de la Paix" - required - /> -
- - {/* Ville et Code postal */} -
+ {activeTab === 'information' && ( +
+ {/* Nom de la société */}
handleChange('address_postal_code', e.target.value)} + value={formData.name} + onChange={e => handleChange('name', e.target.value)} className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="75001" + placeholder="Xpeditis" required />
+ + {/* SIREN */}
handleChange('address_city', e.target.value)} + value={formData.siren} + onChange={e => handleChange('siren', e.target.value.replace(/\D/g, '').slice(0, 9))} className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="Paris" - required + placeholder="123 456 789" + maxLength={9} + /> +

9 chiffres

+
+ + {/* Numéro EORI */} +
+ + handleChange('eori', e.target.value.toUpperCase())} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="FR123456789" + maxLength={17} + /> +

Code pays (2 lettres) + numéro unique (max 15 caractères)

+
+ + {/* Téléphone */} +
+ + handleChange('contact_phone', e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="+33 6 80 18 28 12" + /> +
+ + {/* Email */} +
+ + handleChange('contact_email', e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="contact@xpeditis.com" />
+ )} - {/* Pays */} -
- - + {activeTab === 'address' && ( +
+ {/* Rue */} +
+ + handleChange('address_street', e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="123 Rue de la Paix" + required + /> +
+ + {/* Ville et Code postal */} +
+
+ + handleChange('address_postal_code', e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="75001" + required + /> +
+
+ + handleChange('address_city', e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="Paris" + required + /> +
+
+ + {/* Pays */} +
+ + +
-
+ )}
{/* Actions */} @@ -358,22 +396,6 @@ export default function OrganizationSettingsPage() {
- - {/* Info Note */} - {(formData.siren || formData.eori) && ( -
-
- ℹ️ -
-

Note importante

-

- Les champs SIREN et EORI seront sauvegardés une fois que le backend sera mis à jour pour les - supporter. Pour l'instant, seules les autres informations seront enregistrées. -

-
-
-
- )}
); } diff --git a/apps/frontend/lib/api/index.ts b/apps/frontend/lib/api/index.ts index 9d1c35a..ec84dee 100644 --- a/apps/frontend/lib/api/index.ts +++ b/apps/frontend/lib/api/index.ts @@ -7,7 +7,8 @@ export * from './client'; export * from './auth'; export * from './bookings'; -export * from './organizations'; +export type { Organization, CreateOrganizationRequest, UpdateOrganizationRequest } from './organizations'; +export { organizationsApi } from './organizations'; // Export users module - rename User type to avoid conflict with auth.User export type { User as UserModel, CreateUserRequest, UpdateUserRequest, ChangePasswordRequest } from './users'; export { usersApi } from './users'; diff --git a/apps/frontend/lib/api/organizations.ts b/apps/frontend/lib/api/organizations.ts index d064561..16f5a25 100644 --- a/apps/frontend/lib/api/organizations.ts +++ b/apps/frontend/lib/api/organizations.ts @@ -107,3 +107,6 @@ export const organizationsApi = { return apiClient.get('/api/v1/organizations'); }, }; + +// Export for use in components +export { organizationsApi }; diff --git a/apps/frontend/src/types/api.ts b/apps/frontend/src/types/api.ts index 104694d..19e843a 100644 --- a/apps/frontend/src/types/api.ts +++ b/apps/frontend/src/types/api.ts @@ -119,30 +119,44 @@ export interface CreateOrganizationRequest { export interface UpdateOrganizationRequest { name?: string; - address_street?: string; - address_city?: string; - address_postal_code?: string; - address_country?: string; - contact_email?: string; + siren?: string; + eori?: string; contact_phone?: string; - logo_url?: string; - is_active?: boolean; + contact_email?: string; + address?: { + street: string; + city: string; + state?: string; + postalCode: string; + country: string; + }; + logoUrl?: string; + isActive?: boolean; +} + +export interface OrganizationAddress { + street: string; + city: string; + state?: string; + postalCode: string; + country: string; } export interface OrganizationResponse { id: string; name: string; type: OrganizationType; - address_street: string; - address_city: string; - address_postal_code: string; - address_country: string; - contact_email: string | null; - contact_phone: string | null; - logo_url: string | null; - is_active: boolean; - created_at: string; - updated_at: string; + scac?: string | null; + siren?: string | null; + eori?: string | null; + contact_phone?: string | null; + contact_email?: string | null; + address: OrganizationAddress; + logoUrl?: string | null; + documents: any[]; + isActive: boolean; + createdAt: string; + updatedAt: string; } export interface OrganizationListResponse {