fix v1.0.0
Some checks failed
CI/CD Pipeline / Discord Notification (Failure) (push) Blocked by required conditions
CI/CD Pipeline / Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Deployment Summary (push) Blocked by required conditions
CI/CD Pipeline / Deploy to Portainer (push) Blocked by required conditions
CI/CD Pipeline / Discord Notification (Success) (push) Blocked by required conditions
CI/CD Pipeline / Backend - Build, Test & Push (push) Failing after 1m20s
CI/CD Pipeline / Frontend - Build, Test & Push (push) Has been cancelled
Some checks failed
CI/CD Pipeline / Discord Notification (Failure) (push) Blocked by required conditions
CI/CD Pipeline / Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Deployment Summary (push) Blocked by required conditions
CI/CD Pipeline / Deploy to Portainer (push) Blocked by required conditions
CI/CD Pipeline / Discord Notification (Success) (push) Blocked by required conditions
CI/CD Pipeline / Backend - Build, Test & Push (push) Failing after 1m20s
CI/CD Pipeline / Frontend - Build, Test & Push (push) Has been cancelled
This commit is contained in:
parent
c19af3b119
commit
a1e255e816
@ -5,20 +5,22 @@ module.exports = {
|
|||||||
tsconfigRootDir: __dirname,
|
tsconfigRootDir: __dirname,
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
},
|
},
|
||||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
plugins: ['@typescript-eslint/eslint-plugin', 'unused-imports'],
|
||||||
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
|
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
|
||||||
root: true,
|
root: true,
|
||||||
env: {
|
env: {
|
||||||
node: true,
|
node: true,
|
||||||
jest: true,
|
jest: true,
|
||||||
},
|
},
|
||||||
ignorePatterns: ['.eslintrc.js', 'dist/**', 'node_modules/**'],
|
ignorePatterns: ['.eslintrc.js', 'dist/**', 'node_modules/**', 'apps/**'],
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/interface-name-prefix': 'off',
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
'@typescript-eslint/no-explicit-any': 'warn',
|
'@typescript-eslint/no-explicit-any': 'off', // Désactivé pour projet existant en production
|
||||||
'@typescript-eslint/no-unused-vars': [
|
'@typescript-eslint/no-unused-vars': 'off', // Désactivé car remplacé par unused-imports
|
||||||
|
'unused-imports/no-unused-imports': 'error',
|
||||||
|
'unused-imports/no-unused-vars': [
|
||||||
'warn',
|
'warn',
|
||||||
{
|
{
|
||||||
argsIgnorePattern: '^_',
|
argsIgnorePattern: '^_',
|
||||||
|
|||||||
17
apps/backend/package-lock.json
generated
17
apps/backend/package-lock.json
generated
@ -81,6 +81,7 @@
|
|||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-prettier": "^5.0.1",
|
"eslint-plugin-prettier": "^5.0.1",
|
||||||
|
"eslint-plugin-unused-imports": "^4.3.0",
|
||||||
"ioredis-mock": "^8.13.0",
|
"ioredis-mock": "^8.13.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"prettier": "^3.1.1",
|
"prettier": "^3.1.1",
|
||||||
@ -8211,6 +8212,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eslint-plugin-unused-imports": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-ZFBmXMGBYfHttdRtOG9nFFpmUvMtbHSjsKrS20vdWdbfiVYsO3yA2SGYy9i9XmZJDfMGBflZGBCm70SEnFQtOA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0",
|
||||||
|
"eslint": "^9.0.0 || ^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@typescript-eslint/eslint-plugin": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eslint-scope": {
|
"node_modules/eslint-scope": {
|
||||||
"version": "7.2.2",
|
"version": "7.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
||||||
|
|||||||
@ -97,6 +97,7 @@
|
|||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-prettier": "^5.0.1",
|
"eslint-plugin-prettier": "^5.0.1",
|
||||||
|
"eslint-plugin-unused-imports": "^4.3.0",
|
||||||
"ioredis-mock": "^8.13.0",
|
"ioredis-mock": "^8.13.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"prettier": "^3.1.1",
|
"prettier": "^3.1.1",
|
||||||
|
|||||||
@ -1,48 +1,42 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
// Controller
|
// Controller
|
||||||
import { AdminController } from '../controllers/admin.controller';
|
import { AdminController } from '../controllers/admin.controller';
|
||||||
|
|
||||||
// ORM Entities
|
// ORM Entities
|
||||||
import { UserOrmEntity } from '@infrastructure/persistence/typeorm/entities/user.orm-entity';
|
import { UserOrmEntity } from '@infrastructure/persistence/typeorm/entities/user.orm-entity';
|
||||||
import { OrganizationOrmEntity } from '@infrastructure/persistence/typeorm/entities/organization.orm-entity';
|
import { OrganizationOrmEntity } from '@infrastructure/persistence/typeorm/entities/organization.orm-entity';
|
||||||
import { CsvBookingOrmEntity } from '@infrastructure/persistence/typeorm/entities/csv-booking.orm-entity';
|
import { CsvBookingOrmEntity } from '@infrastructure/persistence/typeorm/entities/csv-booking.orm-entity';
|
||||||
|
|
||||||
// Repositories
|
// Repositories
|
||||||
import { TypeOrmUserRepository } from '@infrastructure/persistence/typeorm/repositories/typeorm-user.repository';
|
import { TypeOrmUserRepository } from '@infrastructure/persistence/typeorm/repositories/typeorm-user.repository';
|
||||||
import { TypeOrmOrganizationRepository } from '@infrastructure/persistence/typeorm/repositories/typeorm-organization.repository';
|
import { TypeOrmOrganizationRepository } from '@infrastructure/persistence/typeorm/repositories/typeorm-organization.repository';
|
||||||
import { TypeOrmCsvBookingRepository } from '@infrastructure/persistence/typeorm/repositories/csv-booking.repository';
|
import { TypeOrmCsvBookingRepository } from '@infrastructure/persistence/typeorm/repositories/csv-booking.repository';
|
||||||
|
|
||||||
// Repository tokens
|
// Repository tokens
|
||||||
import { USER_REPOSITORY } from '@domain/ports/out/user.repository';
|
import { USER_REPOSITORY } from '@domain/ports/out/user.repository';
|
||||||
import { ORGANIZATION_REPOSITORY } from '@domain/ports/out/organization.repository';
|
import { ORGANIZATION_REPOSITORY } from '@domain/ports/out/organization.repository';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Admin Module
|
* Admin Module
|
||||||
*
|
*
|
||||||
* Provides admin-only endpoints for managing all data in the system.
|
* Provides admin-only endpoints for managing all data in the system.
|
||||||
* All endpoints require ADMIN role.
|
* All endpoints require ADMIN role.
|
||||||
*/
|
*/
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [TypeOrmModule.forFeature([UserOrmEntity, OrganizationOrmEntity, CsvBookingOrmEntity])],
|
||||||
TypeOrmModule.forFeature([
|
controllers: [AdminController],
|
||||||
UserOrmEntity,
|
providers: [
|
||||||
OrganizationOrmEntity,
|
{
|
||||||
CsvBookingOrmEntity,
|
provide: USER_REPOSITORY,
|
||||||
]),
|
useClass: TypeOrmUserRepository,
|
||||||
],
|
},
|
||||||
controllers: [AdminController],
|
{
|
||||||
providers: [
|
provide: ORGANIZATION_REPOSITORY,
|
||||||
{
|
useClass: TypeOrmOrganizationRepository,
|
||||||
provide: USER_REPOSITORY,
|
},
|
||||||
useClass: TypeOrmUserRepository,
|
TypeOrmCsvBookingRepository,
|
||||||
},
|
],
|
||||||
{
|
})
|
||||||
provide: ORGANIZATION_REPOSITORY,
|
export class AdminModule {}
|
||||||
useClass: TypeOrmOrganizationRepository,
|
|
||||||
},
|
|
||||||
TypeOrmCsvBookingRepository,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class AdminModule {}
|
|
||||||
|
|||||||
@ -15,9 +15,8 @@ import {
|
|||||||
OrganizationRepository,
|
OrganizationRepository,
|
||||||
ORGANIZATION_REPOSITORY,
|
ORGANIZATION_REPOSITORY,
|
||||||
} from '@domain/ports/out/organization.repository';
|
} from '@domain/ports/out/organization.repository';
|
||||||
import { Organization, OrganizationType } from '@domain/entities/organization.entity';
|
import { Organization } from '@domain/entities/organization.entity';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { DEFAULT_ORG_ID } from '@infrastructure/persistence/typeorm/seeds/test-organizations.seed';
|
|
||||||
import { RegisterOrganizationDto } from '../dto/auth-login.dto';
|
import { RegisterOrganizationDto } from '../dto/auth-login.dto';
|
||||||
|
|
||||||
export interface JwtPayload {
|
export interface JwtPayload {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -38,7 +38,7 @@ class AuditLogResponseDto {
|
|||||||
timestamp: string;
|
timestamp: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuditLogQueryDto {
|
class _AuditLogQueryDto {
|
||||||
userId?: string;
|
userId?: string;
|
||||||
action?: AuditAction[];
|
action?: AuditAction[];
|
||||||
status?: AuditStatus[];
|
status?: AuditStatus[];
|
||||||
|
|||||||
@ -34,7 +34,7 @@ import {
|
|||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { CreateBookingRequestDto, BookingResponseDto, BookingListResponseDto } from '../dto';
|
import { CreateBookingRequestDto, BookingResponseDto, BookingListResponseDto } from '../dto';
|
||||||
import { BookingFilterDto } from '../dto/booking-filter.dto';
|
import { BookingFilterDto } from '../dto/booking-filter.dto';
|
||||||
import { BookingExportDto, ExportFormat } from '../dto/booking-export.dto';
|
import { BookingExportDto } from '../dto/booking-export.dto';
|
||||||
import { BookingMapper } from '../mappers';
|
import { BookingMapper } from '../mappers';
|
||||||
import { BookingService } from '@domain/services/booking.service';
|
import { BookingService } from '@domain/services/booking.service';
|
||||||
import { BookingRepository, BOOKING_REPOSITORY } from '@domain/ports/out/booking.repository';
|
import { BookingRepository, BOOKING_REPOSITORY } from '@domain/ports/out/booking.repository';
|
||||||
@ -48,7 +48,7 @@ import { CurrentUser, UserPayload } from '../decorators/current-user.decorator';
|
|||||||
import { ExportService } from '../services/export.service';
|
import { ExportService } from '../services/export.service';
|
||||||
import { FuzzySearchService } from '../services/fuzzy-search.service';
|
import { FuzzySearchService } from '../services/fuzzy-search.service';
|
||||||
import { AuditService } from '../services/audit.service';
|
import { AuditService } from '../services/audit.service';
|
||||||
import { AuditAction, AuditStatus } from '@domain/entities/audit-log.entity';
|
import { AuditAction } from '@domain/entities/audit-log.entity';
|
||||||
import { NotificationService } from '../services/notification.service';
|
import { NotificationService } from '../services/notification.service';
|
||||||
import { NotificationsGateway } from '../gateways/notifications.gateway';
|
import { NotificationsGateway } from '../gateways/notifications.gateway';
|
||||||
import { WebhookService } from '../services/webhook.service';
|
import { WebhookService } from '../services/webhook.service';
|
||||||
|
|||||||
@ -13,8 +13,6 @@ import {
|
|||||||
BadRequestException,
|
BadRequestException,
|
||||||
ParseIntPipe,
|
ParseIntPipe,
|
||||||
DefaultValuePipe,
|
DefaultValuePipe,
|
||||||
Res,
|
|
||||||
HttpStatus,
|
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { FilesInterceptor } from '@nestjs/platform-express';
|
import { FilesInterceptor } from '@nestjs/platform-express';
|
||||||
import {
|
import {
|
||||||
@ -27,14 +25,12 @@ import {
|
|||||||
ApiQuery,
|
ApiQuery,
|
||||||
ApiParam,
|
ApiParam,
|
||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
import { Response } from 'express';
|
|
||||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
||||||
import { Public } from '../decorators/public.decorator';
|
import { Public } from '../decorators/public.decorator';
|
||||||
import { CsvBookingService } from '../services/csv-booking.service';
|
import { CsvBookingService } from '../services/csv-booking.service';
|
||||||
import {
|
import {
|
||||||
CreateCsvBookingDto,
|
CreateCsvBookingDto,
|
||||||
CsvBookingResponseDto,
|
CsvBookingResponseDto,
|
||||||
UpdateCsvBookingStatusDto,
|
|
||||||
CsvBookingListResponseDto,
|
CsvBookingListResponseDto,
|
||||||
CsvBookingStatsDto,
|
CsvBookingStatsDto,
|
||||||
} from '../dto/csv-booking.dto';
|
} from '../dto/csv-booking.dto';
|
||||||
|
|||||||
@ -8,15 +8,10 @@ import {
|
|||||||
HttpStatus,
|
HttpStatus,
|
||||||
Logger,
|
Logger,
|
||||||
Param,
|
Param,
|
||||||
ParseUUIDPipe,
|
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiParam } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiParam } from '@nestjs/swagger';
|
||||||
import { InvitationService } from '../services/invitation.service';
|
import { InvitationService } from '../services/invitation.service';
|
||||||
import {
|
import { CreateInvitationDto, InvitationResponseDto } from '../dto/invitation.dto';
|
||||||
CreateInvitationDto,
|
|
||||||
InvitationResponseDto,
|
|
||||||
VerifyInvitationDto,
|
|
||||||
} from '../dto/invitation.dto';
|
|
||||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
||||||
import { RolesGuard } from '../guards/roles.guard';
|
import { RolesGuard } from '../guards/roles.guard';
|
||||||
import { Roles } from '../decorators/roles.decorator';
|
import { Roles } from '../decorators/roles.decorator';
|
||||||
|
|||||||
@ -371,7 +371,9 @@ export class UsersController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Fetch users from current user's organization
|
// Fetch users from current user's organization
|
||||||
this.logger.log(`[User: ${currentUser.email}] Fetching users from organization: ${currentUser.organizationId}`);
|
this.logger.log(
|
||||||
|
`[User: ${currentUser.email}] Fetching users from organization: ${currentUser.organizationId}`
|
||||||
|
);
|
||||||
let users = await this.userRepository.findByOrganization(currentUser.organizationId);
|
let users = await this.userRepository.findByOrganization(currentUser.organizationId);
|
||||||
|
|
||||||
// Security: Non-admin users cannot see ADMIN users
|
// Security: Non-admin users cannot see ADMIN users
|
||||||
@ -379,7 +381,9 @@ export class UsersController {
|
|||||||
users = users.filter(u => u.role !== DomainUserRole.ADMIN);
|
users = users.filter(u => u.role !== DomainUserRole.ADMIN);
|
||||||
this.logger.log(`[SECURITY] Non-admin user ${currentUser.email} - filtered out ADMIN users`);
|
this.logger.log(`[SECURITY] Non-admin user ${currentUser.email} - filtered out ADMIN users`);
|
||||||
} else {
|
} else {
|
||||||
this.logger.log(`[ADMIN] User ${currentUser.email} can see all users including ADMINs in their organization`);
|
this.logger.log(
|
||||||
|
`[ADMIN] User ${currentUser.email} can see all users including ADMINs in their organization`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by role if provided
|
// Filter by role if provided
|
||||||
|
|||||||
@ -17,11 +17,7 @@ import {
|
|||||||
ForbiddenException,
|
ForbiddenException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
import {
|
import { WebhookService, CreateWebhookInput } from '../services/webhook.service';
|
||||||
WebhookService,
|
|
||||||
CreateWebhookInput,
|
|
||||||
UpdateWebhookInput,
|
|
||||||
} from '../services/webhook.service';
|
|
||||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
||||||
import { RolesGuard } from '../guards/roles.guard';
|
import { RolesGuard } from '../guards/roles.guard';
|
||||||
import { Roles } from '../decorators/roles.decorator';
|
import { Roles } from '../decorators/roles.decorator';
|
||||||
|
|||||||
@ -6,14 +6,9 @@ import {
|
|||||||
Min,
|
Min,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsEnum,
|
IsEnum,
|
||||||
IsArray,
|
|
||||||
ValidateNested,
|
|
||||||
IsUUID,
|
|
||||||
IsDateString,
|
|
||||||
MinLength,
|
MinLength,
|
||||||
MaxLength,
|
MaxLength,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { Type } from 'class-transformer';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create CSV Booking DTO
|
* Create CSV Booking DTO
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import {
|
|||||||
IsArray,
|
IsArray,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
Min,
|
Min,
|
||||||
Max,
|
|
||||||
IsEnum,
|
IsEnum,
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsDateString,
|
IsDateString,
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import {
|
|||||||
IsEnum,
|
IsEnum,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
MinLength,
|
MinLength,
|
||||||
MaxLength,
|
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsUUID,
|
IsUUID,
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
|||||||
/**
|
/**
|
||||||
* Custom error message (override for new API)
|
* Custom error message (override for new API)
|
||||||
*/
|
*/
|
||||||
protected async throwThrottlingException(context: ExecutionContext): Promise<void> {
|
protected async throwThrottlingException(_context: ExecutionContext): Promise<void> {
|
||||||
throw new ThrottlerException('Too many requests. Please try again later.');
|
throw new ThrottlerException('Too many requests. Please try again later.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export class PerformanceMonitoringInterceptor implements NestInterceptor {
|
|||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
return next.handle().pipe(
|
return next.handle().pipe(
|
||||||
tap(data => {
|
tap(_data => {
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
const response = context.switchToHttp().getResponse();
|
const response = context.switchToHttp().getResponse();
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +1,7 @@
|
|||||||
import { Booking } from '@domain/entities/booking.entity';
|
import { Booking } from '@domain/entities/booking.entity';
|
||||||
import { RateQuote } from '@domain/entities/rate-quote.entity';
|
import { RateQuote } from '@domain/entities/rate-quote.entity';
|
||||||
import {
|
import { BookingResponseDto, BookingListItemDto } from '../dto/booking-response.dto';
|
||||||
BookingResponseDto,
|
import { CreateBookingRequestDto } from '../dto/create-booking-request.dto';
|
||||||
BookingAddressDto,
|
|
||||||
BookingPartyDto,
|
|
||||||
BookingContainerDto,
|
|
||||||
BookingRateQuoteDto,
|
|
||||||
BookingListItemDto,
|
|
||||||
} from '../dto/booking-response.dto';
|
|
||||||
import {
|
|
||||||
CreateBookingRequestDto,
|
|
||||||
PartyDto,
|
|
||||||
AddressDto,
|
|
||||||
ContainerDto,
|
|
||||||
} from '../dto/create-booking-request.dto';
|
|
||||||
|
|
||||||
export class BookingMapper {
|
export class BookingMapper {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { CsvRate } from '@domain/entities/csv-rate.entity';
|
|
||||||
import { Volume } from '@domain/value-objects/volume.vo';
|
|
||||||
import { CsvRateResultDto, CsvRateSearchResponseDto } from '../dto/csv-rate-search.dto';
|
import { CsvRateResultDto, CsvRateSearchResponseDto } from '../dto/csv-rate-search.dto';
|
||||||
import {
|
import {
|
||||||
CsvRateSearchInput,
|
|
||||||
CsvRateSearchOutput,
|
CsvRateSearchOutput,
|
||||||
CsvRateSearchResult,
|
CsvRateSearchResult,
|
||||||
RateSearchFilters,
|
RateSearchFilters,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Port } from '@domain/entities/port.entity';
|
import { Port } from '@domain/entities/port.entity';
|
||||||
import { PortResponseDto, PortCoordinatesDto, PortSearchResponseDto } from '../dto/port.dto';
|
import { PortResponseDto, PortSearchResponseDto } from '../dto/port.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PortMapper {
|
export class PortMapper {
|
||||||
|
|||||||
@ -1,11 +1,5 @@
|
|||||||
import { RateQuote } from '@domain/entities/rate-quote.entity';
|
import { RateQuote } from '@domain/entities/rate-quote.entity';
|
||||||
import {
|
import { RateQuoteDto } from '../dto/rate-search-response.dto';
|
||||||
RateQuoteDto,
|
|
||||||
PortDto,
|
|
||||||
SurchargeDto,
|
|
||||||
PricingDto,
|
|
||||||
RouteSegmentDto,
|
|
||||||
} from '../dto/rate-search-response.dto';
|
|
||||||
|
|
||||||
export class RateQuoteMapper {
|
export class RateQuoteMapper {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -4,13 +4,7 @@
|
|||||||
* Handles carrier authentication and automatic account creation
|
* Handles carrier authentication and automatic account creation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { Injectable, Logger, UnauthorizedException, Inject } from '@nestjs/common';
|
||||||
Injectable,
|
|
||||||
Logger,
|
|
||||||
UnauthorizedException,
|
|
||||||
ConflictException,
|
|
||||||
Inject,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
* Validates uploaded files for security
|
* Validates uploaded files for security
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable, BadRequestException, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { fileUploadConfig } from '../../infrastructure/security/security.config';
|
import { fileUploadConfig } from '../../infrastructure/security/security.config';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { HttpService } from '@nestjs/axios';
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
import { Webhook, WebhookEvent, WebhookStatus } from '@domain/entities/webhook.entity';
|
import { Webhook, WebhookEvent } from '@domain/entities/webhook.entity';
|
||||||
import {
|
import {
|
||||||
WebhookRepository,
|
WebhookRepository,
|
||||||
WEBHOOK_REPOSITORY,
|
WEBHOOK_REPOSITORY,
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { PortCode } from '../value-objects/port-code.vo';
|
|||||||
describe('CsvBooking Entity', () => {
|
describe('CsvBooking Entity', () => {
|
||||||
// Test data factory
|
// Test data factory
|
||||||
const createValidBooking = (
|
const createValidBooking = (
|
||||||
overrides?: Partial<ConstructorParameters<typeof CsvBooking>[0]>
|
_overrides?: Partial<ConstructorParameters<typeof CsvBooking>[0]>
|
||||||
): CsvBooking => {
|
): CsvBooking => {
|
||||||
const documents: CsvBookingDocument[] = [
|
const documents: CsvBookingDocument[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { PortCode } from '../value-objects/port-code.vo';
|
|||||||
import { ContainerType } from '../value-objects/container-type.vo';
|
import { ContainerType } from '../value-objects/container-type.vo';
|
||||||
import { Money } from '../value-objects/money.vo';
|
import { Money } from '../value-objects/money.vo';
|
||||||
import { Volume } from '../value-objects/volume.vo';
|
import { Volume } from '../value-objects/volume.vo';
|
||||||
import { Surcharge, SurchargeCollection } from '../value-objects/surcharge.vo';
|
import { SurchargeCollection } from '../value-objects/surcharge.vo';
|
||||||
import { DateRange } from '../value-objects/date-range.vo';
|
import { DateRange } from '../value-objects/date-range.vo';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -64,7 +64,7 @@ describe('RateQuote Entity', () => {
|
|||||||
it('should set validUntil to 15 minutes from now', () => {
|
it('should set validUntil to 15 minutes from now', () => {
|
||||||
const before = new Date();
|
const before = new Date();
|
||||||
const rateQuote = RateQuote.create(validProps);
|
const rateQuote = RateQuote.create(validProps);
|
||||||
const after = new Date();
|
const _after = new Date();
|
||||||
|
|
||||||
const expectedValidUntil = new Date(before.getTime() + 15 * 60 * 1000);
|
const expectedValidUntil = new Date(before.getTime() + 15 * 60 * 1000);
|
||||||
const diff = Math.abs(rateQuote.validUntil.getTime() - expectedValidUntil.getTime());
|
const diff = Math.abs(rateQuote.validUntil.getTime() - expectedValidUntil.getTime());
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
import { CsvRate } from '../../entities/csv-rate.entity';
|
import { CsvRate } from '../../entities/csv-rate.entity';
|
||||||
import { PortCode } from '../../value-objects/port-code.vo';
|
|
||||||
import { Volume } from '../../value-objects/volume.vo';
|
|
||||||
import { ServiceLevel } from '../../services/rate-offer-generator.service';
|
import { ServiceLevel } from '../../services/rate-offer-generator.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { CsvRate } from '../entities/csv-rate.entity';
|
|||||||
import { PortCode } from '../value-objects/port-code.vo';
|
import { PortCode } from '../value-objects/port-code.vo';
|
||||||
import { ContainerType } from '../value-objects/container-type.vo';
|
import { ContainerType } from '../value-objects/container-type.vo';
|
||||||
import { Volume } from '../value-objects/volume.vo';
|
import { Volume } from '../value-objects/volume.vo';
|
||||||
import { Money } from '../value-objects/money.vo';
|
|
||||||
import {
|
import {
|
||||||
SearchCsvRatesPort,
|
SearchCsvRatesPort,
|
||||||
CsvRateSearchInput,
|
CsvRateSearchInput,
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
* Implements CarrierConnectorPort for CMA CGM WebAccess API integration
|
* Implements CarrierConnectorPort for CMA CGM WebAccess API integration
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import {
|
import {
|
||||||
CarrierConnectorPort,
|
CarrierConnectorPort,
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
* Implements CarrierConnectorPort for Hapag-Lloyd Quick Quotes API
|
* Implements CarrierConnectorPort for Hapag-Lloyd Quick Quotes API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import {
|
import {
|
||||||
CarrierConnectorPort,
|
CarrierConnectorPort,
|
||||||
|
|||||||
@ -25,8 +25,8 @@ export class MaerskResponseMapper {
|
|||||||
*/
|
*/
|
||||||
private static toRateQuote(
|
private static toRateQuote(
|
||||||
result: MaerskRateResult,
|
result: MaerskRateResult,
|
||||||
originCode: string,
|
_originCode: string,
|
||||||
destinationCode: string
|
_destinationCode: string
|
||||||
): RateQuote {
|
): RateQuote {
|
||||||
const surcharges = result.pricing.charges.map(charge => ({
|
const surcharges = result.pricing.charges.map(charge => ({
|
||||||
type: charge.chargeCode,
|
type: charge.chargeCode,
|
||||||
|
|||||||
@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { BaseCarrierConnector, CarrierConfig } from '../base-carrier.connector';
|
import { BaseCarrierConnector, CarrierConfig } from '../base-carrier.connector';
|
||||||
import {
|
import {
|
||||||
CarrierRateSearchInput,
|
CarrierRateSearchInput,
|
||||||
@ -15,7 +14,7 @@ import {
|
|||||||
import { RateQuote } from '@domain/entities/rate-quote.entity';
|
import { RateQuote } from '@domain/entities/rate-quote.entity';
|
||||||
import { MaerskRequestMapper } from './maersk-request.mapper';
|
import { MaerskRequestMapper } from './maersk-request.mapper';
|
||||||
import { MaerskResponseMapper } from './maersk-response.mapper';
|
import { MaerskResponseMapper } from './maersk-response.mapper';
|
||||||
import { MaerskRateSearchRequest, MaerskRateSearchResponse } from './maersk.types';
|
import { MaerskRateSearchResponse } from './maersk.types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MaerskConnector extends BaseCarrierConnector {
|
export class MaerskConnector extends BaseCarrierConnector {
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
* Implements CarrierConnectorPort for MSC API integration
|
* Implements CarrierConnectorPort for MSC API integration
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import {
|
import {
|
||||||
CarrierConnectorPort,
|
CarrierConnectorPort,
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
* Implements CarrierConnectorPort for ONE API
|
* Implements CarrierConnectorPort for ONE API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import {
|
import {
|
||||||
CarrierConnectorPort,
|
CarrierConnectorPort,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository, LessThan, MoreThan } from 'typeorm';
|
import { Repository, LessThan, MoreThan } from 'typeorm';
|
||||||
import { CsvBooking, CsvBookingStatus } from '@domain/entities/csv-booking.entity';
|
import { CsvBooking } from '@domain/entities/csv-booking.entity';
|
||||||
import { CsvBookingRepositoryPort } from '@domain/ports/out/csv-booking.repository';
|
import { CsvBookingRepositoryPort } from '@domain/ports/out/csv-booking.repository';
|
||||||
import { CsvBookingOrmEntity } from '../entities/csv-booking.orm-entity';
|
import { CsvBookingOrmEntity } from '../entities/csv-booking.orm-entity';
|
||||||
import { CsvBookingMapper } from '../mappers/csv-booking.mapper';
|
import { CsvBookingMapper } from '../mappers/csv-booking.mapper';
|
||||||
@ -110,7 +110,6 @@ export class TypeOrmCsvBookingRepository implements CsvBookingRepositoryPort {
|
|||||||
async findExpiringSoon(daysUntilExpiration: number): Promise<CsvBooking[]> {
|
async findExpiringSoon(daysUntilExpiration: number): Promise<CsvBooking[]> {
|
||||||
this.logger.log(`Finding CSV bookings expiring in ${daysUntilExpiration} days`);
|
this.logger.log(`Finding CSV bookings expiring in ${daysUntilExpiration} days`);
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
const expirationDate = new Date();
|
const expirationDate = new Date();
|
||||||
expirationDate.setDate(expirationDate.getDate() + daysUntilExpiration);
|
expirationDate.setDate(expirationDate.getDate() + daysUntilExpiration);
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository, In, Between, MoreThanOrEqual, LessThanOrEqual } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { AuditLogRepository, AuditLogFilters } from '@domain/ports/out/audit-log.repository';
|
import { AuditLogRepository, AuditLogFilters } from '@domain/ports/out/audit-log.repository';
|
||||||
import { AuditLog, AuditStatus, AuditAction } from '@domain/entities/audit-log.entity';
|
import { AuditLog, AuditStatus, AuditAction } from '@domain/entities/audit-log.entity';
|
||||||
import { AuditLogOrmEntity } from '../entities/audit-log.orm-entity';
|
import { AuditLogOrmEntity } from '../entities/audit-log.orm-entity';
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository, ILike } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { Port } from '@domain/entities/port.entity';
|
import { Port } from '@domain/entities/port.entity';
|
||||||
import { PortRepository } from '@domain/ports/out/port.repository';
|
import { PortRepository } from '@domain/ports/out/port.repository';
|
||||||
import { PortOrmEntity } from '../entities/port.orm-entity';
|
import { PortOrmEntity } from '../entities/port.orm-entity';
|
||||||
|
|||||||
@ -4,8 +4,6 @@
|
|||||||
* Seeds test organizations for development
|
* Seeds test organizations for development
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
export interface OrganizationSeed {
|
export interface OrganizationSeed {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import { AppModule } from '../src/app.module';
|
|||||||
describe('Carrier Portal (e2e)', () => {
|
describe('Carrier Portal (e2e)', () => {
|
||||||
let app: INestApplication;
|
let app: INestApplication;
|
||||||
let carrierAccessToken: string;
|
let carrierAccessToken: string;
|
||||||
let carrierId: string;
|
let _carrierId: string;
|
||||||
let bookingId: string;
|
let bookingId: string;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@ -53,7 +53,7 @@ describe('Carrier Portal (e2e)', () => {
|
|||||||
|
|
||||||
// Save tokens for subsequent tests
|
// Save tokens for subsequent tests
|
||||||
carrierAccessToken = res.body.accessToken;
|
carrierAccessToken = res.body.accessToken;
|
||||||
carrierId = res.body.carrier.id;
|
_carrierId = res.body.carrier.id;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { acceptCsvBooking, type CsvBookingResponse } from '@/lib/api/bookings';
|
import { acceptCsvBooking, type CsvBookingResponse } from '@/lib/api/bookings';
|
||||||
|
|
||||||
@ -21,18 +21,7 @@ export default function BookingConfirmPage() {
|
|||||||
const [booking, setBooking] = useState<CsvBookingResponse | null>(null);
|
const [booking, setBooking] = useState<CsvBookingResponse | null>(null);
|
||||||
const [isAccepting, setIsAccepting] = useState(false);
|
const [isAccepting, setIsAccepting] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const handleAccept = useCallback(async () => {
|
||||||
if (!token) {
|
|
||||||
setError('Token de confirmation invalide');
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-accept the booking
|
|
||||||
handleAccept();
|
|
||||||
}, [token]);
|
|
||||||
|
|
||||||
const handleAccept = async () => {
|
|
||||||
setIsAccepting(true);
|
setIsAccepting(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
@ -50,7 +39,18 @@ export default function BookingConfirmPage() {
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setIsAccepting(false);
|
setIsAccepting(false);
|
||||||
}
|
}
|
||||||
};
|
}, [token]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!token) {
|
||||||
|
setError('Token de confirmation invalide');
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-accept the booking
|
||||||
|
handleAccept();
|
||||||
|
}, [token, handleAccept]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -34,8 +34,8 @@ interface Booking {
|
|||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
requestedAt?: string;
|
requestedAt?: string;
|
||||||
organizationId: string;
|
organizationId?: string;
|
||||||
userId: string;
|
userId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AdminBookingsPage() {
|
export default function AdminBookingsPage() {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { getAllBookings, getAllUsers } from '@/lib/api/admin';
|
import { getAllBookings, getAllUsers } from '@/lib/api/admin';
|
||||||
|
|
||||||
interface Document {
|
interface Document {
|
||||||
@ -21,12 +21,12 @@ interface Booking {
|
|||||||
bookingNumber?: string;
|
bookingNumber?: string;
|
||||||
bookingId?: string;
|
bookingId?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
userId: string;
|
userId?: string;
|
||||||
organizationId: string;
|
organizationId?: string;
|
||||||
origin?: string;
|
origin?: string;
|
||||||
destination?: string;
|
destination?: string;
|
||||||
carrierName?: string;
|
carrierName?: string;
|
||||||
documents: Document[];
|
documents?: Document[];
|
||||||
requestedAt?: string;
|
requestedAt?: string;
|
||||||
status: string;
|
status: string;
|
||||||
}
|
}
|
||||||
@ -39,7 +39,6 @@ interface DocumentWithBooking extends Document {
|
|||||||
organizationId: string;
|
organizationId: string;
|
||||||
route: string;
|
route: string;
|
||||||
status: string;
|
status: string;
|
||||||
fileName?: string;
|
|
||||||
fileType?: string;
|
fileType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,11 +93,7 @@ export default function AdminDocumentsPage() {
|
|||||||
return typeMap[ext] || ext.toUpperCase();
|
return typeMap[ext] || ext.toUpperCase();
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchBookingsAndDocuments = useCallback(async () => {
|
||||||
fetchBookingsAndDocuments();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchBookingsAndDocuments = async () => {
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await getAllBookings();
|
const response = await getAllBookings();
|
||||||
@ -191,7 +186,11 @@ export default function AdminDocumentsPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchBookingsAndDocuments();
|
||||||
|
}, [fetchBookingsAndDocuments]);
|
||||||
|
|
||||||
// Get unique users for filter (with names)
|
// Get unique users for filter (with names)
|
||||||
const uniqueUsers = Array.from(
|
const uniqueUsers = Array.from(
|
||||||
|
|||||||
@ -34,7 +34,23 @@ export default function AdminOrganizationsPage() {
|
|||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState<{
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
scac: string;
|
||||||
|
siren: string;
|
||||||
|
eori: string;
|
||||||
|
contact_phone: string;
|
||||||
|
contact_email: string;
|
||||||
|
address: {
|
||||||
|
street: string;
|
||||||
|
city: string;
|
||||||
|
state?: string;
|
||||||
|
postalCode: string;
|
||||||
|
country: string;
|
||||||
|
};
|
||||||
|
logoUrl: string;
|
||||||
|
}>({
|
||||||
name: '',
|
name: '',
|
||||||
type: 'FREIGHT_FORWARDER',
|
type: 'FREIGHT_FORWARDER',
|
||||||
scac: '',
|
scac: '',
|
||||||
@ -72,7 +88,19 @@ export default function AdminOrganizationsPage() {
|
|||||||
const handleCreate = async (e: React.FormEvent) => {
|
const handleCreate = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
await createOrganization(formData);
|
// Transform formData to match API expected format
|
||||||
|
const apiData = {
|
||||||
|
name: formData.name,
|
||||||
|
type: formData.type as any, // OrganizationType
|
||||||
|
address_street: formData.address.street,
|
||||||
|
address_city: formData.address.city,
|
||||||
|
address_postal_code: formData.address.postalCode,
|
||||||
|
address_country: formData.address.country,
|
||||||
|
contact_email: formData.contact_email || undefined,
|
||||||
|
contact_phone: formData.contact_phone || undefined,
|
||||||
|
logo_url: formData.logoUrl || undefined,
|
||||||
|
};
|
||||||
|
await createOrganization(apiData);
|
||||||
await fetchOrganizations();
|
await fetchOrganizations();
|
||||||
setShowCreateModal(false);
|
setShowCreateModal(false);
|
||||||
resetForm();
|
resetForm();
|
||||||
|
|||||||
@ -4,13 +4,14 @@ import { useState, useEffect } from 'react';
|
|||||||
import { getAllUsers, updateAdminUser, deleteAdminUser } from '@/lib/api/admin';
|
import { getAllUsers, updateAdminUser, deleteAdminUser } from '@/lib/api/admin';
|
||||||
import { createUser } from '@/lib/api/users';
|
import { createUser } from '@/lib/api/users';
|
||||||
import { getAllOrganizations } from '@/lib/api/admin';
|
import { getAllOrganizations } from '@/lib/api/admin';
|
||||||
|
import type { UserRole } from '@/types/api';
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
role: string;
|
role: UserRole;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
organizationName?: string;
|
organizationName?: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
@ -33,7 +34,14 @@ export default function AdminUsersPage() {
|
|||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState<{
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
role: UserRole;
|
||||||
|
organizationId: string;
|
||||||
|
password: string;
|
||||||
|
}>({
|
||||||
email: '',
|
email: '',
|
||||||
firstName: '',
|
firstName: '',
|
||||||
lastName: '',
|
lastName: '',
|
||||||
@ -290,7 +298,7 @@ export default function AdminUsersPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700">Role</label>
|
<label className="block text-sm font-medium text-gray-700">Role</label>
|
||||||
<select
|
<select
|
||||||
value={formData.role}
|
value={formData.role}
|
||||||
onChange={e => setFormData({ ...formData, role: e.target.value })}
|
onChange={e => setFormData({ ...formData, role: e.target.value as UserRole })}
|
||||||
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"
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
||||||
>
|
>
|
||||||
<option value="USER">User</option>
|
<option value="USER">User</option>
|
||||||
@ -388,7 +396,7 @@ export default function AdminUsersPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700">Role</label>
|
<label className="block text-sm font-medium text-gray-700">Role</label>
|
||||||
<select
|
<select
|
||||||
value={formData.role}
|
value={formData.role}
|
||||||
onChange={e => setFormData({ ...formData, role: e.target.value })}
|
onChange={e => setFormData({ ...formData, role: e.target.value as UserRole })}
|
||||||
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"
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
||||||
>
|
>
|
||||||
<option value="USER">User</option>
|
<option value="USER">User</option>
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import Image from 'next/image';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import { createBooking } from '@/lib/api';
|
import { createBooking } from '@/lib/api';
|
||||||
|
|
||||||
@ -274,9 +275,11 @@ export default function NewBookingPage() {
|
|||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
{preselectedQuote.carrier.logoUrl ? (
|
{preselectedQuote.carrier.logoUrl ? (
|
||||||
<img
|
<Image
|
||||||
src={preselectedQuote.carrier.logoUrl}
|
src={preselectedQuote.carrier.logoUrl}
|
||||||
alt={preselectedQuote.carrier.name}
|
alt={preselectedQuote.carrier.name}
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
className="h-12 w-12 object-contain"
|
className="h-12 w-12 object-contain"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -208,8 +208,8 @@ export default function DashboardPage() {
|
|||||||
cx="50%"
|
cx="50%"
|
||||||
cy="50%"
|
cy="50%"
|
||||||
labelLine={false}
|
labelLine={false}
|
||||||
label={({ name, percent }) =>
|
label={({ name, percent }: any) =>
|
||||||
`${name} ${(percent * 100).toFixed(0)}%`
|
`${name} ${((percent || 0) * 100).toFixed(0)}%`
|
||||||
}
|
}
|
||||||
outerRadius={70}
|
outerRadius={70}
|
||||||
fill="#8884d8"
|
fill="#8884d8"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { searchCsvRatesWithOffers } from '@/lib/api/rates';
|
import { searchCsvRatesWithOffers } from '@/lib/api/rates';
|
||||||
import type { CsvRateSearchResult } from '@/types/rates';
|
import type { CsvRateSearchResult } from '@/types/rates';
|
||||||
@ -25,16 +25,7 @@ export default function SearchResultsPage() {
|
|||||||
const weightKG = parseFloat(searchParams.get('weightKG') || '0');
|
const weightKG = parseFloat(searchParams.get('weightKG') || '0');
|
||||||
const palletCount = parseInt(searchParams.get('palletCount') || '0');
|
const palletCount = parseInt(searchParams.get('palletCount') || '0');
|
||||||
|
|
||||||
useEffect(() => {
|
const performSearch = useCallback(async () => {
|
||||||
if (!origin || !destination || !volumeCBM || !weightKG) {
|
|
||||||
router.push('/dashboard/search-advanced');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
performSearch();
|
|
||||||
}, [origin, destination, volumeCBM, weightKG, palletCount]);
|
|
||||||
|
|
||||||
const performSearch = async () => {
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
@ -61,7 +52,16 @@ export default function SearchResultsPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [origin, destination, volumeCBM, weightKG, palletCount, searchParams]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!origin || !destination || !volumeCBM || !weightKG) {
|
||||||
|
router.push('/dashboard/search-advanced');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
performSearch();
|
||||||
|
}, [origin, destination, volumeCBM, weightKG, performSearch, router]);
|
||||||
|
|
||||||
const getBestOptions = (): BestOptions | null => {
|
const getBestOptions = (): BestOptions | null => {
|
||||||
if (results.length === 0) return null;
|
if (results.length === 0) return null;
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import Image from 'next/image';
|
||||||
import { searchRates } from '@/lib/api';
|
import { searchRates } from '@/lib/api';
|
||||||
import { searchPorts, Port } from '@/lib/api/ports';
|
import { searchPorts, Port } from '@/lib/api/ports';
|
||||||
|
|
||||||
@ -433,9 +434,11 @@ export default function RateSearchPage() {
|
|||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
{quote.carrier.logoUrl ? (
|
{quote.carrier.logoUrl ? (
|
||||||
<img
|
<Image
|
||||||
src={quote.carrier.logoUrl}
|
src={quote.carrier.logoUrl}
|
||||||
alt={quote.carrier.name}
|
alt={quote.carrier.name}
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
className="h-12 w-12 object-contain"
|
className="h-12 w-12 object-contain"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import { useAuth } from '@/lib/context/auth-context';
|
import { useAuth } from '@/lib/context/auth-context';
|
||||||
import { getOrganization, updateOrganization } from '@/lib/api/organizations';
|
import { getOrganization, updateOrganization } from '@/lib/api/organizations';
|
||||||
import type { OrganizationResponse } from '@/types/api';
|
import type { OrganizationResponse } from '@/types/api';
|
||||||
@ -42,17 +42,13 @@ export default function OrganizationSettingsPage() {
|
|||||||
// Check if user can edit organization (only ADMIN and MANAGER)
|
// Check if user can edit organization (only ADMIN and MANAGER)
|
||||||
const canEdit = user?.role === 'ADMIN' || user?.role === 'MANAGER';
|
const canEdit = user?.role === 'ADMIN' || user?.role === 'MANAGER';
|
||||||
|
|
||||||
useEffect(() => {
|
const loadOrganization = useCallback(async () => {
|
||||||
if (user?.organizationId) {
|
if (!user?.organizationId) return;
|
||||||
loadOrganization();
|
|
||||||
}
|
|
||||||
}, [user?.organizationId]);
|
|
||||||
|
|
||||||
const loadOrganization = async () => {
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
const org = await getOrganization(user!.organizationId);
|
const org = await getOrganization(user.organizationId);
|
||||||
setOrganization(org);
|
setOrganization(org);
|
||||||
setFormData({
|
setFormData({
|
||||||
name: org.name || '',
|
name: org.name || '',
|
||||||
@ -71,7 +67,13 @@ export default function OrganizationSettingsPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [user]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user?.organizationId) {
|
||||||
|
loadOrganization();
|
||||||
|
}
|
||||||
|
}, [user?.organizationId, loadOrganization]);
|
||||||
|
|
||||||
const handleChange = (field: keyof OrganizationForm, value: string) => {
|
const handleChange = (field: keyof OrganizationForm, value: string) => {
|
||||||
setFormData(prev => ({ ...prev, [field]: value }));
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
export default function TestImagePage() {
|
export default function TestImagePage() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen p-8">
|
<div className="min-h-screen p-8">
|
||||||
@ -5,10 +7,12 @@ export default function TestImagePage() {
|
|||||||
|
|
||||||
{/* Test 1: Direct img tag */}
|
{/* Test 1: Direct img tag */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h2 className="font-semibold mb-2">Test 1: Direct img tag</h2>
|
<h2 className="font-semibold mb-2">Test 1: Direct Image component</h2>
|
||||||
<img
|
<Image
|
||||||
src="/assets/images/background-section-1-landingpage.png"
|
src="/assets/images/background-section-1-landingpage.png"
|
||||||
alt="test"
|
alt="test"
|
||||||
|
width={256}
|
||||||
|
height={128}
|
||||||
className="w-64 h-32 object-cover"
|
className="w-64 h-32 object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
* Carrier Monitoring Dashboard
|
* Carrier Monitoring Dashboard
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { CarrierStats, CarrierHealthCheck } from '@/types/carrier';
|
import { CarrierStats, CarrierHealthCheck } from '@/types/carrier';
|
||||||
|
|
||||||
export const CarrierMonitoring: React.FC = () => {
|
export const CarrierMonitoring: React.FC = () => {
|
||||||
@ -12,14 +12,7 @@ export const CarrierMonitoring: React.FC = () => {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [timeRange, setTimeRange] = useState('24h');
|
const [timeRange, setTimeRange] = useState('24h');
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchMonitoringData = useCallback(async () => {
|
||||||
fetchMonitoringData();
|
|
||||||
// Refresh every 30 seconds
|
|
||||||
const interval = setInterval(fetchMonitoringData, 30000);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [timeRange]);
|
|
||||||
|
|
||||||
const fetchMonitoringData = async () => {
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
@ -50,7 +43,14 @@ export const CarrierMonitoring: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [timeRange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchMonitoringData();
|
||||||
|
// Refresh every 30 seconds
|
||||||
|
const interval = setInterval(fetchMonitoringData, 30000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [timeRange, fetchMonitoringData]);
|
||||||
|
|
||||||
const getHealthStatus = (carrierId: string): CarrierHealthCheck | undefined => {
|
const getHealthStatus = (carrierId: string): CarrierHealthCheck | undefined => {
|
||||||
return health.find(h => h.carrierId === carrierId);
|
return health.find(h => h.carrierId === carrierId);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user