fix error get organisation

This commit is contained in:
David 2025-11-30 18:39:08 +01:00
parent 1a92228af5
commit c76f908d5c
11 changed files with 469 additions and 195 deletions

View File

@ -248,6 +248,22 @@ export class OrganizationsController {
organization.updateName(dto.name); 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) { if (dto.address) {
organization.updateAddress(OrganizationMapper.mapDtoToAddress(dto.address)); organization.updateAddress(OrganizationMapper.mapDtoToAddress(dto.address));
} }

View File

@ -101,6 +101,43 @@ export class CreateOrganizationDto {
@Matches(/^[A-Z]{4}$/, { message: 'SCAC must be 4 uppercase letters (e.g., MAEU, MSCU)' }) @Matches(/^[A-Z]{4}$/, { message: 'SCAC must be 4 uppercase letters (e.g., MAEU, MSCU)' })
scac?: string; 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({ @ApiProperty({
description: 'Organization address', description: 'Organization address',
type: AddressDto, type: AddressDto,
@ -134,6 +171,43 @@ export class UpdateOrganizationDto {
@MaxLength(200) @MaxLength(200)
name?: string; 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({ @ApiPropertyOptional({
description: 'Organization address', description: 'Organization address',
type: AddressDto, type: AddressDto,
@ -228,6 +302,30 @@ export class OrganizationResponseDto {
}) })
scac?: string; 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({ @ApiProperty({
description: 'Organization address', description: 'Organization address',
type: AddressDto, type: AddressDto,

View File

@ -24,6 +24,10 @@ export class OrganizationMapper {
name: organization.name, name: organization.name,
type: organization.type, type: organization.type,
scac: organization.scac, scac: organization.scac,
siren: organization.siren,
eori: organization.eori,
contact_phone: organization.contactPhone,
contact_email: organization.contactEmail,
address: this.mapAddressToDto(organization.address), address: this.mapAddressToDto(organization.address),
logoUrl: organization.logoUrl, logoUrl: organization.logoUrl,
documents: organization.documents.map(doc => this.mapDocumentToDto(doc)), documents: organization.documents.map(doc => this.mapDocumentToDto(doc)),

View File

@ -37,6 +37,10 @@ export interface OrganizationProps {
name: string; name: string;
type: OrganizationType; type: OrganizationType;
scac?: string; // Standard Carrier Alpha Code (for carriers only) 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; address: OrganizationAddress;
logoUrl?: string; logoUrl?: string;
documents: OrganizationDocument[]; documents: OrganizationDocument[];
@ -113,6 +117,22 @@ export class Organization {
return this.props.scac; 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 { get address(): OrganizationAddress {
return { ...this.props.address }; return { ...this.props.address };
} }
@ -163,6 +183,26 @@ export class Organization {
this.props.updatedAt = new Date(); 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 { updateLogoUrl(logoUrl: string): void {
this.props.logoUrl = logoUrl; this.props.logoUrl = logoUrl;
this.props.updatedAt = new Date(); this.props.updatedAt = new Date();

View File

@ -23,6 +23,18 @@ export class OrganizationOrmEntity {
@Column({ type: 'char', length: 4, nullable: true, unique: true }) @Column({ type: 'char', length: 4, nullable: true, unique: true })
scac: string | null; 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 }) @Column({ name: 'address_street', type: 'varchar', length: 255 })
addressStreet: string; addressStreet: string;

View File

@ -19,6 +19,10 @@ export class OrganizationOrmMapper {
orm.name = props.name; orm.name = props.name;
orm.type = props.type; orm.type = props.type;
orm.scac = props.scac || null; 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.addressStreet = props.address.street;
orm.addressCity = props.address.city; orm.addressCity = props.address.city;
orm.addressState = props.address.state || null; orm.addressState = props.address.state || null;
@ -42,6 +46,10 @@ export class OrganizationOrmMapper {
name: orm.name, name: orm.name,
type: orm.type as any, type: orm.type as any,
scac: orm.scac || undefined, scac: orm.scac || undefined,
siren: orm.siren || undefined,
eori: orm.eori || undefined,
contact_phone: orm.contactPhone || undefined,
contact_email: orm.contactEmail || undefined,
address: { address: {
street: orm.addressStreet, street: orm.addressStreet,
city: orm.addressCity, city: orm.addressCity,

View File

@ -0,0 +1,56 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AddOrganizationContactFields1733000000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// 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<void> {
await queryRunner.dropColumn('organizations', 'contact_email');
await queryRunner.dropColumn('organizations', 'contact_phone');
await queryRunner.dropColumn('organizations', 'eori');
await queryRunner.dropColumn('organizations', 'siren');
}
}

View File

@ -7,8 +7,8 @@ import type { OrganizationResponse } from '@/types/api';
interface OrganizationForm { interface OrganizationForm {
name: string; name: string;
siren: string; // TODO: Add to backend siren: string;
eori: string; // TODO: Add to backend eori: string;
contact_phone: string; contact_phone: string;
contact_email: string; contact_email: string;
address_street: string; address_street: string;
@ -17,8 +17,11 @@ interface OrganizationForm {
address_country: string; address_country: string;
} }
type TabType = 'information' | 'address';
export default function OrganizationSettingsPage() { export default function OrganizationSettingsPage() {
const { user } = useAuth(); const { user } = useAuth();
const [activeTab, setActiveTab] = useState<TabType>('information');
const [organization, setOrganization] = useState<OrganizationResponse | null>(null); const [organization, setOrganization] = useState<OrganizationResponse | null>(null);
const [formData, setFormData] = useState<OrganizationForm>({ const [formData, setFormData] = useState<OrganizationForm>({
name: '', name: '',
@ -49,15 +52,15 @@ export default function OrganizationSettingsPage() {
const org = await getOrganization(user!.organizationId); const org = await getOrganization(user!.organizationId);
setOrganization(org); setOrganization(org);
setFormData({ setFormData({
name: org.name, name: org.name || '',
siren: '', // TODO: Get from backend when available siren: org.siren || '',
eori: '', // TODO: Get from backend when available eori: org.eori || '',
contact_phone: org.contact_phone || '', contact_phone: org.contact_phone || '',
contact_email: org.contact_email || '', contact_email: org.contact_email || '',
address_street: org.address_street, address_street: org.address?.street || '',
address_city: org.address_city, address_city: org.address?.city || '',
address_postal_code: org.address_postal_code, address_postal_code: org.address?.postalCode || '',
address_country: org.address_country, address_country: org.address?.country || 'FR',
}); });
} catch (err) { } catch (err) {
console.error('Failed to load organization:', err); console.error('Failed to load organization:', err);
@ -75,15 +78,15 @@ export default function OrganizationSettingsPage() {
const handleCancel = () => { const handleCancel = () => {
if (organization) { if (organization) {
setFormData({ setFormData({
name: organization.name, name: organization.name || '',
siren: '', siren: organization.siren || '',
eori: '', eori: organization.eori || '',
contact_phone: organization.contact_phone || '', contact_phone: organization.contact_phone || '',
contact_email: organization.contact_email || '', contact_email: organization.contact_email || '',
address_street: organization.address_street, address_street: organization.address?.street || '',
address_city: organization.address_city, address_city: organization.address?.city || '',
address_postal_code: organization.address_postal_code, address_postal_code: organization.address?.postalCode || '',
address_country: organization.address_country, address_country: organization.address?.country || 'FR',
}); });
setSuccessMessage(null); setSuccessMessage(null);
setError(null); setError(null);
@ -98,27 +101,22 @@ export default function OrganizationSettingsPage() {
setError(null); setError(null);
setSuccessMessage(null); setSuccessMessage(null);
// Update organization (excluding SIREN and EORI for now)
const updatedOrg = await updateOrganization(user.organizationId, { const updatedOrg = await updateOrganization(user.organizationId, {
name: formData.name, name: formData.name,
siren: formData.siren,
eori: formData.eori,
contact_phone: formData.contact_phone, contact_phone: formData.contact_phone,
contact_email: formData.contact_email, contact_email: formData.contact_email,
address_street: formData.address_street, address: {
address_city: formData.address_city, street: formData.address_street,
address_postal_code: formData.address_postal_code, city: formData.address_city,
address_country: formData.address_country, postalCode: formData.address_postal_code,
country: formData.address_country,
},
}); });
setOrganization(updatedOrg); setOrganization(updatedOrg);
setSuccessMessage('Informations sauvegardées avec succès'); 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) { } catch (err) {
console.error('Failed to update organization:', err); console.error('Failed to update organization:', err);
setError(err instanceof Error ? err.message : 'Erreur lors de la sauvegarde'); setError(err instanceof Error ? err.message : 'Erreur lors de la sauvegarde');
@ -161,7 +159,9 @@ export default function OrganizationSettingsPage() {
{successMessage && ( {successMessage && (
<div className="mb-6 bg-green-50 border border-green-200 rounded-lg p-4"> <div className="mb-6 bg-green-50 border border-green-200 rounded-lg p-4">
<div className="flex items-center"> <div className="flex items-center">
<span className="text-green-600 text-xl mr-3"></span> <svg className="w-5 h-5 text-green-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<p className="text-green-800 font-medium">{successMessage}</p> <p className="text-green-800 font-medium">{successMessage}</p>
</div> </div>
</div> </div>
@ -171,17 +171,55 @@ export default function OrganizationSettingsPage() {
{error && ( {error && (
<div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4"> <div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center"> <div className="flex items-center">
<span className="text-red-600 text-xl mr-3"></span> <svg className="w-5 h-5 text-red-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
<p className="text-red-800 font-medium">{error}</p> <p className="text-red-800 font-medium">{error}</p>
</div> </div>
</div> </div>
)} )}
{/* Form */} {/* Tabs */}
<div className="bg-white rounded-lg shadow-md"> <div className="bg-white rounded-lg shadow-md">
<div className="p-8"> <div className="border-b border-gray-200">
<h2 className="text-xl font-semibold text-gray-900 mb-6">Informations</h2> <nav className="flex -mb-px">
<button
onClick={() => setActiveTab('information')}
className={`px-6 py-4 text-sm font-medium border-b-2 transition-colors ${
activeTab === 'information'
? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<div className="flex items-center space-x-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Informations</span>
</div>
</button>
<button
onClick={() => setActiveTab('address')}
className={`px-6 py-4 text-sm font-medium border-b-2 transition-colors ${
activeTab === 'address'
? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<div className="flex items-center space-x-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span>Adresse</span>
</div>
</button>
</nav>
</div>
{/* Tab Content */}
<div className="p-8">
{activeTab === 'information' && (
<div className="space-y-6"> <div className="space-y-6">
{/* Nom de la société */} {/* Nom de la société */}
<div> <div>
@ -240,7 +278,7 @@ export default function OrganizationSettingsPage() {
value={formData.contact_phone} value={formData.contact_phone}
onChange={e => handleChange('contact_phone', e.target.value)} onChange={e => 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" 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" placeholder="+33 6 80 18 28 12"
/> />
</div> </div>
@ -255,12 +293,11 @@ export default function OrganizationSettingsPage() {
placeholder="contact@xpeditis.com" placeholder="contact@xpeditis.com"
/> />
</div> </div>
{/* Divider */}
<div className="border-t border-gray-200 pt-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Adresse</h3>
</div> </div>
)}
{activeTab === 'address' && (
<div className="space-y-6">
{/* Rue */} {/* Rue */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
@ -329,6 +366,7 @@ export default function OrganizationSettingsPage() {
</select> </select>
</div> </div>
</div> </div>
)}
</div> </div>
{/* Actions */} {/* Actions */}
@ -358,22 +396,6 @@ export default function OrganizationSettingsPage() {
</button> </button>
</div> </div>
</div> </div>
{/* Info Note */}
{(formData.siren || formData.eori) && (
<div className="mt-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-start">
<span className="text-blue-600 text-xl mr-3"></span>
<div>
<p className="text-blue-900 font-medium mb-1">Note importante</p>
<p className="text-blue-800 text-sm">
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.
</p>
</div>
</div>
</div>
)}
</div> </div>
); );
} }

View File

@ -7,7 +7,8 @@
export * from './client'; export * from './client';
export * from './auth'; export * from './auth';
export * from './bookings'; 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 users module - rename User type to avoid conflict with auth.User
export type { User as UserModel, CreateUserRequest, UpdateUserRequest, ChangePasswordRequest } from './users'; export type { User as UserModel, CreateUserRequest, UpdateUserRequest, ChangePasswordRequest } from './users';
export { usersApi } from './users'; export { usersApi } from './users';

View File

@ -107,3 +107,6 @@ export const organizationsApi = {
return apiClient.get<Organization[]>('/api/v1/organizations'); return apiClient.get<Organization[]>('/api/v1/organizations');
}, },
}; };
// Export for use in components
export { organizationsApi };

View File

@ -119,30 +119,44 @@ export interface CreateOrganizationRequest {
export interface UpdateOrganizationRequest { export interface UpdateOrganizationRequest {
name?: string; name?: string;
address_street?: string; siren?: string;
address_city?: string; eori?: string;
address_postal_code?: string;
address_country?: string;
contact_email?: string;
contact_phone?: string; contact_phone?: string;
logo_url?: string; contact_email?: string;
is_active?: boolean; 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 { export interface OrganizationResponse {
id: string; id: string;
name: string; name: string;
type: OrganizationType; type: OrganizationType;
address_street: string; scac?: string | null;
address_city: string; siren?: string | null;
address_postal_code: string; eori?: string | null;
address_country: string; contact_phone?: string | null;
contact_email: string | null; contact_email?: string | null;
contact_phone: string | null; address: OrganizationAddress;
logo_url: string | null; logoUrl?: string | null;
is_active: boolean; documents: any[];
created_at: string; isActive: boolean;
updated_at: string; createdAt: string;
updatedAt: string;
} }
export interface OrganizationListResponse { export interface OrganizationListResponse {