From a1e255e81608e5fae6254b7a43cacfdb09b1ad8b Mon Sep 17 00:00:00 2001 From: David Date: Tue, 23 Dec 2025 11:49:57 +0100 Subject: [PATCH] fix v1.0.0 --- apps/backend/.eslintrc.js | 10 +- apps/backend/apps/backend/src/main.ts | 0 apps/backend/package-lock.json | 17 + apps/backend/package.json | 1 + .../src/application/admin/admin.module.ts | 90 +- .../src/application/auth/auth.service.ts | 3 +- .../controllers/admin.controller.ts | 1209 ++++++++--------- .../controllers/audit.controller.ts | 2 +- .../controllers/bookings.controller.ts | 4 +- .../controllers/csv-bookings.controller.ts | 4 - .../controllers/invitations.controller.ts | 7 +- .../controllers/users.controller.ts | 8 +- .../controllers/webhooks.controller.ts | 6 +- .../src/application/dto/csv-booking.dto.ts | 5 - .../dto/rate-search-filters.dto.ts | 1 - apps/backend/src/application/dto/user.dto.ts | 1 - .../src/application/guards/throttle.guard.ts | 2 +- .../performance-monitoring.interceptor.ts | 2 +- .../src/application/mappers/booking.mapper.ts | 16 +- .../application/mappers/csv-rate.mapper.ts | 3 - .../src/application/mappers/port.mapper.ts | 2 +- .../application/mappers/rate-quote.mapper.ts | 8 +- .../services/carrier-auth.service.ts | 8 +- .../services/file-validation.service.ts | 2 +- .../application/services/webhook.service.ts | 2 +- .../entities/csv-booking.entity.spec.ts | 2 +- .../src/domain/entities/csv-rate.entity.ts | 2 +- .../domain/entities/rate-quote.entity.spec.ts | 2 +- .../domain/ports/in/search-csv-rates.port.ts | 2 - .../services/csv-rate-search.service.ts | 1 - .../carriers/cma-cgm/cma-cgm.connector.ts | 2 +- .../hapag-lloyd/hapag-lloyd.connector.ts | 2 +- .../carriers/maersk/maersk-response.mapper.ts | 4 +- .../carriers/maersk/maersk.connector.ts | 3 +- .../carriers/msc/msc.connector.ts | 2 +- .../carriers/one/one.connector.ts | 2 +- .../repositories/csv-booking.repository.ts | 3 +- .../typeorm-audit-log.repository.ts | 2 +- .../repositories/typeorm-port.repository.ts | 2 +- .../typeorm/seeds/test-organizations.seed.ts | 2 - apps/backend/test/carrier-portal.e2e-spec.ts | 4 +- .../app/booking/confirm/[token]/page.tsx | 28 +- .../app/dashboard/admin/bookings/page.tsx | 4 +- .../app/dashboard/admin/documents/page.tsx | 21 +- .../dashboard/admin/organizations/page.tsx | 32 +- .../app/dashboard/admin/users/page.tsx | 16 +- .../app/dashboard/bookings/new/page.tsx | 5 +- apps/frontend/app/dashboard/page.tsx | 4 +- .../search-advanced/results/page.tsx | 24 +- apps/frontend/app/dashboard/search/page.tsx | 5 +- .../dashboard/settings/organization/page.tsx | 20 +- apps/frontend/app/test-image/page.tsx | 8 +- .../src/legacy-pages/CarrierMonitoring.tsx | 20 +- 53 files changed, 819 insertions(+), 818 deletions(-) delete mode 100644 apps/backend/apps/backend/src/main.ts diff --git a/apps/backend/.eslintrc.js b/apps/backend/.eslintrc.js index 79d268e..909c403 100644 --- a/apps/backend/.eslintrc.js +++ b/apps/backend/.eslintrc.js @@ -5,20 +5,22 @@ module.exports = { tsconfigRootDir: __dirname, sourceType: 'module', }, - plugins: ['@typescript-eslint/eslint-plugin'], + plugins: ['@typescript-eslint/eslint-plugin', 'unused-imports'], extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], root: true, env: { node: true, jest: true, }, - ignorePatterns: ['.eslintrc.js', 'dist/**', 'node_modules/**'], + ignorePatterns: ['.eslintrc.js', 'dist/**', 'node_modules/**', 'apps/**'], rules: { '@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-unused-vars': [ + '@typescript-eslint/no-explicit-any': 'off', // Désactivé pour projet existant en production + '@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', { argsIgnorePattern: '^_', diff --git a/apps/backend/apps/backend/src/main.ts b/apps/backend/apps/backend/src/main.ts deleted file mode 100644 index e69de29..0000000 diff --git a/apps/backend/package-lock.json b/apps/backend/package-lock.json index 62d7fb2..914fd4b 100644 --- a/apps/backend/package-lock.json +++ b/apps/backend/package-lock.json @@ -81,6 +81,7 @@ "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.0.1", + "eslint-plugin-unused-imports": "^4.3.0", "ioredis-mock": "^8.13.0", "jest": "^29.7.0", "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": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", diff --git a/apps/backend/package.json b/apps/backend/package.json index 41b911f..c5763d4 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -97,6 +97,7 @@ "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.0.1", + "eslint-plugin-unused-imports": "^4.3.0", "ioredis-mock": "^8.13.0", "jest": "^29.7.0", "prettier": "^3.1.1", diff --git a/apps/backend/src/application/admin/admin.module.ts b/apps/backend/src/application/admin/admin.module.ts index f354586..4fa96e8 100644 --- a/apps/backend/src/application/admin/admin.module.ts +++ b/apps/backend/src/application/admin/admin.module.ts @@ -1,48 +1,42 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; - -// Controller -import { AdminController } from '../controllers/admin.controller'; - -// ORM Entities -import { UserOrmEntity } from '@infrastructure/persistence/typeorm/entities/user.orm-entity'; -import { OrganizationOrmEntity } from '@infrastructure/persistence/typeorm/entities/organization.orm-entity'; -import { CsvBookingOrmEntity } from '@infrastructure/persistence/typeorm/entities/csv-booking.orm-entity'; - -// Repositories -import { TypeOrmUserRepository } from '@infrastructure/persistence/typeorm/repositories/typeorm-user.repository'; -import { TypeOrmOrganizationRepository } from '@infrastructure/persistence/typeorm/repositories/typeorm-organization.repository'; -import { TypeOrmCsvBookingRepository } from '@infrastructure/persistence/typeorm/repositories/csv-booking.repository'; - -// Repository tokens -import { USER_REPOSITORY } from '@domain/ports/out/user.repository'; -import { ORGANIZATION_REPOSITORY } from '@domain/ports/out/organization.repository'; - -/** - * Admin Module - * - * Provides admin-only endpoints for managing all data in the system. - * All endpoints require ADMIN role. - */ -@Module({ - imports: [ - TypeOrmModule.forFeature([ - UserOrmEntity, - OrganizationOrmEntity, - CsvBookingOrmEntity, - ]), - ], - controllers: [AdminController], - providers: [ - { - provide: USER_REPOSITORY, - useClass: TypeOrmUserRepository, - }, - { - provide: ORGANIZATION_REPOSITORY, - useClass: TypeOrmOrganizationRepository, - }, - TypeOrmCsvBookingRepository, - ], -}) -export class AdminModule {} +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +// Controller +import { AdminController } from '../controllers/admin.controller'; + +// ORM Entities +import { UserOrmEntity } from '@infrastructure/persistence/typeorm/entities/user.orm-entity'; +import { OrganizationOrmEntity } from '@infrastructure/persistence/typeorm/entities/organization.orm-entity'; +import { CsvBookingOrmEntity } from '@infrastructure/persistence/typeorm/entities/csv-booking.orm-entity'; + +// Repositories +import { TypeOrmUserRepository } from '@infrastructure/persistence/typeorm/repositories/typeorm-user.repository'; +import { TypeOrmOrganizationRepository } from '@infrastructure/persistence/typeorm/repositories/typeorm-organization.repository'; +import { TypeOrmCsvBookingRepository } from '@infrastructure/persistence/typeorm/repositories/csv-booking.repository'; + +// Repository tokens +import { USER_REPOSITORY } from '@domain/ports/out/user.repository'; +import { ORGANIZATION_REPOSITORY } from '@domain/ports/out/organization.repository'; + +/** + * Admin Module + * + * Provides admin-only endpoints for managing all data in the system. + * All endpoints require ADMIN role. + */ +@Module({ + imports: [TypeOrmModule.forFeature([UserOrmEntity, OrganizationOrmEntity, CsvBookingOrmEntity])], + controllers: [AdminController], + providers: [ + { + provide: USER_REPOSITORY, + useClass: TypeOrmUserRepository, + }, + { + provide: ORGANIZATION_REPOSITORY, + useClass: TypeOrmOrganizationRepository, + }, + TypeOrmCsvBookingRepository, + ], +}) +export class AdminModule {} diff --git a/apps/backend/src/application/auth/auth.service.ts b/apps/backend/src/application/auth/auth.service.ts index 9e20439..f01e9f9 100644 --- a/apps/backend/src/application/auth/auth.service.ts +++ b/apps/backend/src/application/auth/auth.service.ts @@ -15,9 +15,8 @@ import { OrganizationRepository, 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 { DEFAULT_ORG_ID } from '@infrastructure/persistence/typeorm/seeds/test-organizations.seed'; import { RegisterOrganizationDto } from '../dto/auth-login.dto'; export interface JwtPayload { diff --git a/apps/backend/src/application/controllers/admin.controller.ts b/apps/backend/src/application/controllers/admin.controller.ts index 8bde9fa..218aa9d 100644 --- a/apps/backend/src/application/controllers/admin.controller.ts +++ b/apps/backend/src/application/controllers/admin.controller.ts @@ -1,609 +1,600 @@ -import { - Controller, - Get, - Post, - Patch, - Delete, - Param, - Body, - HttpCode, - HttpStatus, - Logger, - UsePipes, - ValidationPipe, - NotFoundException, - BadRequestException, - ParseUUIDPipe, - UseGuards, - Inject, -} from '@nestjs/common'; -import { - ApiTags, - ApiOperation, - ApiResponse, - ApiBadRequestResponse, - ApiNotFoundResponse, - ApiParam, - ApiBearerAuth, -} from '@nestjs/swagger'; -import { JwtAuthGuard } from '../guards/jwt-auth.guard'; -import { RolesGuard } from '../guards/roles.guard'; -import { CurrentUser, UserPayload } from '../decorators/current-user.decorator'; -import { Roles } from '../decorators/roles.decorator'; - -// User imports -import { - UserRepository, - USER_REPOSITORY, -} from '@domain/ports/out/user.repository'; -import { UserMapper } from '../mappers/user.mapper'; -import { - CreateUserDto, - UpdateUserDto, - UserResponseDto, - UserListResponseDto, -} from '../dto/user.dto'; - -// Organization imports -import { - OrganizationRepository, - ORGANIZATION_REPOSITORY, -} from '@domain/ports/out/organization.repository'; -import { OrganizationMapper } from '../mappers/organization.mapper'; -import { - OrganizationResponseDto, - OrganizationListResponseDto, -} from '../dto/organization.dto'; - -// CSV Booking imports -import { TypeOrmCsvBookingRepository } from '@infrastructure/persistence/typeorm/repositories/csv-booking.repository'; -import { CsvBookingMapper } from '@infrastructure/persistence/typeorm/mappers/csv-booking.mapper'; - -/** - * Admin Controller - * - * Dedicated controller for admin-only endpoints that provide access to ALL data - * in the database without organization filtering. - * - * All endpoints require ADMIN role. - */ -@ApiTags('Admin') -@Controller('admin') -@UseGuards(JwtAuthGuard, RolesGuard) -@Roles('admin') -@ApiBearerAuth() -export class AdminController { - private readonly logger = new Logger(AdminController.name); - - constructor( - @Inject(USER_REPOSITORY) private readonly userRepository: UserRepository, - @Inject(ORGANIZATION_REPOSITORY) private readonly organizationRepository: OrganizationRepository, - private readonly csvBookingRepository: TypeOrmCsvBookingRepository, - ) {} - - // ==================== USERS ENDPOINTS ==================== - - /** - * Get ALL users from database (admin only) - * - * Returns all users regardless of status (active/inactive) or organization - */ - @Get('users') - @ApiOperation({ - summary: 'Get all users (Admin only)', - description: 'Retrieve ALL users from the database without any filters. Includes active and inactive users from all organizations.', - }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'All users retrieved successfully', - type: UserListResponseDto, - }) - @ApiResponse({ - status: 401, - description: 'Unauthorized - missing or invalid token', - }) - @ApiResponse({ - status: 403, - description: 'Forbidden - requires admin role', - }) - async getAllUsers(@CurrentUser() user: UserPayload): Promise { - this.logger.log(`[ADMIN: ${user.email}] Fetching ALL users from database`); - - let users = await this.userRepository.findAll(); - - // Security: Non-admin users (MANAGER and below) cannot see ADMIN users - if (user.role !== 'ADMIN') { - users = users.filter(u => u.role !== 'ADMIN'); - this.logger.log(`[SECURITY] Non-admin user ${user.email} - filtered out ADMIN users`); - } - - const userDtos = UserMapper.toDtoArray(users); - - this.logger.log(`[ADMIN] Retrieved ${users.length} users from database`); - - return { - users: userDtos, - total: users.length, - page: 1, - pageSize: users.length, - totalPages: 1, - }; - } - - /** - * Get user by ID (admin only) - */ - @Get('users/:id') - @ApiOperation({ - summary: 'Get user by ID (Admin only)', - description: 'Retrieve a specific user by ID', - }) - @ApiParam({ - name: 'id', - description: 'User ID (UUID)', - }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'User retrieved successfully', - type: UserResponseDto, - }) - @ApiNotFoundResponse({ - description: 'User not found', - }) - async getUserById( - @Param('id', ParseUUIDPipe) id: string, - @CurrentUser() user: UserPayload, - ): Promise { - this.logger.log(`[ADMIN: ${user.email}] Fetching user: ${id}`); - - const foundUser = await this.userRepository.findById(id); - if (!foundUser) { - throw new NotFoundException(`User ${id} not found`); - } - - return UserMapper.toDto(foundUser); - } - - /** - * Update user (admin only) - */ - @Patch('users/:id') - @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) - @ApiOperation({ - summary: 'Update user (Admin only)', - description: 'Update user details (any user, any organization)', - }) - @ApiParam({ - name: 'id', - description: 'User ID (UUID)', - }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'User updated successfully', - type: UserResponseDto, - }) - @ApiNotFoundResponse({ - description: 'User not found', - }) - async updateUser( - @Param('id', ParseUUIDPipe) id: string, - @Body() dto: UpdateUserDto, - @CurrentUser() user: UserPayload, - ): Promise { - this.logger.log(`[ADMIN: ${user.email}] Updating user: ${id}`); - - const foundUser = await this.userRepository.findById(id); - if (!foundUser) { - throw new NotFoundException(`User ${id} not found`); - } - - // Security: Prevent users from changing their own role - if (dto.role && id === user.id) { - this.logger.warn(`[SECURITY] User ${user.email} attempted to change their own role`); - throw new BadRequestException('You cannot change your own role'); - } - - // Apply updates - if (dto.firstName) { - foundUser.updateFirstName(dto.firstName); - } - if (dto.lastName) { - foundUser.updateLastName(dto.lastName); - } - if (dto.role) { - foundUser.updateRole(dto.role); - } - if (dto.isActive !== undefined) { - if (dto.isActive) { - foundUser.activate(); - } else { - foundUser.deactivate(); - } - } - - const updatedUser = await this.userRepository.update(foundUser); - this.logger.log(`[ADMIN] User updated successfully: ${updatedUser.id}`); - - return UserMapper.toDto(updatedUser); - } - - /** - * Delete user (admin only) - */ - @Delete('users/:id') - @HttpCode(HttpStatus.NO_CONTENT) - @ApiOperation({ - summary: 'Delete user (Admin only)', - description: 'Permanently delete a user from the database', - }) - @ApiParam({ - name: 'id', - description: 'User ID (UUID)', - }) - @ApiResponse({ - status: HttpStatus.NO_CONTENT, - description: 'User deleted successfully', - }) - @ApiNotFoundResponse({ - description: 'User not found', - }) - async deleteUser( - @Param('id', ParseUUIDPipe) id: string, - @CurrentUser() user: UserPayload, - ): Promise { - this.logger.log(`[ADMIN: ${user.email}] Deleting user: ${id}`); - - const foundUser = await this.userRepository.findById(id); - if (!foundUser) { - throw new NotFoundException(`User ${id} not found`); - } - - await this.userRepository.deleteById(id); - this.logger.log(`[ADMIN] User deleted successfully: ${id}`); - } - - // ==================== ORGANIZATIONS ENDPOINTS ==================== - - /** - * Get ALL organizations from database (admin only) - * - * Returns all organizations regardless of status (active/inactive) - */ - @Get('organizations') - @ApiOperation({ - summary: 'Get all organizations (Admin only)', - description: 'Retrieve ALL organizations from the database without any filters. Includes active and inactive organizations.', - }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'All organizations retrieved successfully', - type: OrganizationListResponseDto, - }) - @ApiResponse({ - status: 401, - description: 'Unauthorized - missing or invalid token', - }) - @ApiResponse({ - status: 403, - description: 'Forbidden - requires admin role', - }) - async getAllOrganizations(@CurrentUser() user: UserPayload): Promise { - this.logger.log(`[ADMIN: ${user.email}] Fetching ALL organizations from database`); - - const organizations = await this.organizationRepository.findAll(); - const orgDtos = OrganizationMapper.toDtoArray(organizations); - - this.logger.log(`[ADMIN] Retrieved ${organizations.length} organizations from database`); - - return { - organizations: orgDtos, - total: organizations.length, - page: 1, - pageSize: organizations.length, - totalPages: 1, - }; - } - - /** - * Get organization by ID (admin only) - */ - @Get('organizations/:id') - @ApiOperation({ - summary: 'Get organization by ID (Admin only)', - description: 'Retrieve a specific organization by ID', - }) - @ApiParam({ - name: 'id', - description: 'Organization ID (UUID)', - }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'Organization retrieved successfully', - type: OrganizationResponseDto, - }) - @ApiNotFoundResponse({ - description: 'Organization not found', - }) - async getOrganizationById( - @Param('id', ParseUUIDPipe) id: string, - @CurrentUser() user: UserPayload, - ): Promise { - this.logger.log(`[ADMIN: ${user.email}] Fetching organization: ${id}`); - - const organization = await this.organizationRepository.findById(id); - if (!organization) { - throw new NotFoundException(`Organization ${id} not found`); - } - - return OrganizationMapper.toDto(organization); - } - - // ==================== CSV BOOKINGS ENDPOINTS ==================== - - /** - * Get ALL csv bookings from database (admin only) - * - * Returns all csv bookings from all organizations - */ - @Get('bookings') - @ApiOperation({ - summary: 'Get all CSV bookings (Admin only)', - description: 'Retrieve ALL CSV bookings from the database without any filters. Includes bookings from all organizations and all statuses.', - }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'All CSV bookings retrieved successfully', - }) - @ApiResponse({ - status: 401, - description: 'Unauthorized - missing or invalid token', - }) - @ApiResponse({ - status: 403, - description: 'Forbidden - requires admin role', - }) - async getAllBookings(@CurrentUser() user: UserPayload) { - this.logger.log(`[ADMIN: ${user.email}] Fetching ALL csv bookings from database`); - - const csvBookings = await this.csvBookingRepository.findAll(); - const bookingDtos = csvBookings.map(booking => this.csvBookingToDto(booking)); - - this.logger.log(`[ADMIN] Retrieved ${csvBookings.length} csv bookings from database`); - - return { - bookings: bookingDtos, - total: csvBookings.length, - page: 1, - pageSize: csvBookings.length, - totalPages: csvBookings.length > 0 ? 1 : 0, - }; - } - - /** - * Get csv booking by ID (admin only) - */ - @Get('bookings/:id') - @ApiOperation({ - summary: 'Get CSV booking by ID (Admin only)', - description: 'Retrieve a specific CSV booking by ID', - }) - @ApiParam({ - name: 'id', - description: 'Booking ID (UUID)', - }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'CSV booking retrieved successfully', - }) - @ApiNotFoundResponse({ - description: 'CSV booking not found', - }) - async getBookingById( - @Param('id', ParseUUIDPipe) id: string, - @CurrentUser() user: UserPayload, - ) { - this.logger.log(`[ADMIN: ${user.email}] Fetching csv booking: ${id}`); - - const csvBooking = await this.csvBookingRepository.findById(id); - if (!csvBooking) { - throw new NotFoundException(`CSV booking ${id} not found`); - } - - return this.csvBookingToDto(csvBooking); - } - - /** - * Update csv booking (admin only) - */ - @Patch('bookings/:id') - @ApiOperation({ - summary: 'Update CSV booking (Admin only)', - description: 'Update CSV booking status or details', - }) - @ApiParam({ - name: 'id', - description: 'Booking ID (UUID)', - }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'CSV booking updated successfully', - }) - @ApiNotFoundResponse({ - description: 'CSV booking not found', - }) - async updateBooking( - @Param('id', ParseUUIDPipe) id: string, - @Body() updateDto: any, - @CurrentUser() user: UserPayload, - ) { - this.logger.log(`[ADMIN: ${user.email}] Updating csv booking: ${id}`); - - const csvBooking = await this.csvBookingRepository.findById(id); - if (!csvBooking) { - throw new NotFoundException(`CSV booking ${id} not found`); - } - - // Apply updates to the domain entity - // Note: This is a simplified version. You may want to add proper domain methods - const updatedBooking = await this.csvBookingRepository.update(csvBooking); - - this.logger.log(`[ADMIN] CSV booking updated successfully: ${id}`); - return this.csvBookingToDto(updatedBooking); - } - - /** - * Delete csv booking (admin only) - */ - @Delete('bookings/:id') - @HttpCode(HttpStatus.NO_CONTENT) - @ApiOperation({ - summary: 'Delete CSV booking (Admin only)', - description: 'Permanently delete a CSV booking from the database', - }) - @ApiParam({ - name: 'id', - description: 'Booking ID (UUID)', - }) - @ApiResponse({ - status: HttpStatus.NO_CONTENT, - description: 'CSV booking deleted successfully', - }) - @ApiNotFoundResponse({ - description: 'CSV booking not found', - }) - async deleteBooking( - @Param('id', ParseUUIDPipe) id: string, - @CurrentUser() user: UserPayload, - ): Promise { - this.logger.log(`[ADMIN: ${user.email}] Deleting csv booking: ${id}`); - - const csvBooking = await this.csvBookingRepository.findById(id); - if (!csvBooking) { - throw new NotFoundException(`CSV booking ${id} not found`); - } - - await this.csvBookingRepository.delete(id); - this.logger.log(`[ADMIN] CSV booking deleted successfully: ${id}`); - } - - /** - * Helper method to convert CSV booking domain entity to DTO - */ - private csvBookingToDto(booking: any) { - const primaryCurrency = booking.primaryCurrency as 'USD' | 'EUR'; - - return { - id: booking.id, - userId: booking.userId, - organizationId: booking.organizationId, - carrierName: booking.carrierName, - carrierEmail: booking.carrierEmail, - origin: booking.origin.getValue(), - destination: booking.destination.getValue(), - volumeCBM: booking.volumeCBM, - weightKG: booking.weightKG, - palletCount: booking.palletCount, - priceUSD: booking.priceUSD, - priceEUR: booking.priceEUR, - primaryCurrency: booking.primaryCurrency, - transitDays: booking.transitDays, - containerType: booking.containerType, - status: booking.status, - documents: booking.documents || [], - confirmationToken: booking.confirmationToken, - requestedAt: booking.requestedAt, - respondedAt: booking.respondedAt || null, - notes: booking.notes, - rejectionReason: booking.rejectionReason, - routeDescription: booking.getRouteDescription(), - isExpired: booking.isExpired(), - price: booking.getPriceInCurrency(primaryCurrency), - }; - } - - // ==================== DOCUMENTS ENDPOINTS ==================== - - /** - * Get ALL documents from all organizations (admin only) - * - * Returns documents grouped by organization - */ - @Get('documents') - @ApiOperation({ - summary: 'Get all documents (Admin only)', - description: 'Retrieve ALL documents from all organizations in the database.', - }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'All documents retrieved successfully', - }) - @ApiResponse({ - status: 401, - description: 'Unauthorized - missing or invalid token', - }) - @ApiResponse({ - status: 403, - description: 'Forbidden - requires admin role', - }) - async getAllDocuments(@CurrentUser() user: UserPayload): Promise { - this.logger.log(`[ADMIN: ${user.email}] Fetching ALL documents from database`); - - // Get all organizations - const organizations = await this.organizationRepository.findAll(); - - // Extract documents from all organizations - const allDocuments = organizations.flatMap(org => - org.documents.map(doc => ({ - ...doc, - organizationId: org.id, - organizationName: org.name, - })) - ); - - this.logger.log(`[ADMIN] Retrieved ${allDocuments.length} documents from ${organizations.length} organizations`); - - return { - documents: allDocuments, - total: allDocuments.length, - organizationCount: organizations.length, - }; - } - - /** - * Get documents for a specific organization (admin only) - */ - @Get('organizations/:id/documents') - @ApiOperation({ - summary: 'Get organization documents (Admin only)', - description: 'Retrieve all documents for a specific organization', - }) - @ApiParam({ - name: 'id', - description: 'Organization ID (UUID)', - }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'Organization documents retrieved successfully', - }) - @ApiNotFoundResponse({ - description: 'Organization not found', - }) - async getOrganizationDocuments( - @Param('id', ParseUUIDPipe) id: string, - @CurrentUser() user: UserPayload, - ): Promise { - this.logger.log(`[ADMIN: ${user.email}] Fetching documents for organization: ${id}`); - - const organization = await this.organizationRepository.findById(id); - if (!organization) { - throw new NotFoundException(`Organization ${id} not found`); - } - - return { - organizationId: organization.id, - organizationName: organization.name, - documents: organization.documents, - total: organization.documents.length, - }; - } -} +import { + Controller, + Get, + Patch, + Delete, + Param, + Body, + HttpCode, + HttpStatus, + Logger, + UsePipes, + ValidationPipe, + NotFoundException, + BadRequestException, + ParseUUIDPipe, + UseGuards, + Inject, +} from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiNotFoundResponse, + ApiParam, + ApiBearerAuth, +} from '@nestjs/swagger'; +import { JwtAuthGuard } from '../guards/jwt-auth.guard'; +import { RolesGuard } from '../guards/roles.guard'; +import { CurrentUser, UserPayload } from '../decorators/current-user.decorator'; +import { Roles } from '../decorators/roles.decorator'; + +// User imports +import { UserRepository, USER_REPOSITORY } from '@domain/ports/out/user.repository'; +import { UserMapper } from '../mappers/user.mapper'; +import { UpdateUserDto, UserResponseDto, UserListResponseDto } from '../dto/user.dto'; + +// Organization imports +import { + OrganizationRepository, + ORGANIZATION_REPOSITORY, +} from '@domain/ports/out/organization.repository'; +import { OrganizationMapper } from '../mappers/organization.mapper'; +import { OrganizationResponseDto, OrganizationListResponseDto } from '../dto/organization.dto'; + +// CSV Booking imports +import { TypeOrmCsvBookingRepository } from '@infrastructure/persistence/typeorm/repositories/csv-booking.repository'; + +/** + * Admin Controller + * + * Dedicated controller for admin-only endpoints that provide access to ALL data + * in the database without organization filtering. + * + * All endpoints require ADMIN role. + */ +@ApiTags('Admin') +@Controller('admin') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('admin') +@ApiBearerAuth() +export class AdminController { + private readonly logger = new Logger(AdminController.name); + + constructor( + @Inject(USER_REPOSITORY) private readonly userRepository: UserRepository, + @Inject(ORGANIZATION_REPOSITORY) + private readonly organizationRepository: OrganizationRepository, + private readonly csvBookingRepository: TypeOrmCsvBookingRepository + ) {} + + // ==================== USERS ENDPOINTS ==================== + + /** + * Get ALL users from database (admin only) + * + * Returns all users regardless of status (active/inactive) or organization + */ + @Get('users') + @ApiOperation({ + summary: 'Get all users (Admin only)', + description: + 'Retrieve ALL users from the database without any filters. Includes active and inactive users from all organizations.', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'All users retrieved successfully', + type: UserListResponseDto, + }) + @ApiResponse({ + status: 401, + description: 'Unauthorized - missing or invalid token', + }) + @ApiResponse({ + status: 403, + description: 'Forbidden - requires admin role', + }) + async getAllUsers(@CurrentUser() user: UserPayload): Promise { + this.logger.log(`[ADMIN: ${user.email}] Fetching ALL users from database`); + + let users = await this.userRepository.findAll(); + + // Security: Non-admin users (MANAGER and below) cannot see ADMIN users + if (user.role !== 'ADMIN') { + users = users.filter(u => u.role !== 'ADMIN'); + this.logger.log(`[SECURITY] Non-admin user ${user.email} - filtered out ADMIN users`); + } + + const userDtos = UserMapper.toDtoArray(users); + + this.logger.log(`[ADMIN] Retrieved ${users.length} users from database`); + + return { + users: userDtos, + total: users.length, + page: 1, + pageSize: users.length, + totalPages: 1, + }; + } + + /** + * Get user by ID (admin only) + */ + @Get('users/:id') + @ApiOperation({ + summary: 'Get user by ID (Admin only)', + description: 'Retrieve a specific user by ID', + }) + @ApiParam({ + name: 'id', + description: 'User ID (UUID)', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'User retrieved successfully', + type: UserResponseDto, + }) + @ApiNotFoundResponse({ + description: 'User not found', + }) + async getUserById( + @Param('id', ParseUUIDPipe) id: string, + @CurrentUser() user: UserPayload + ): Promise { + this.logger.log(`[ADMIN: ${user.email}] Fetching user: ${id}`); + + const foundUser = await this.userRepository.findById(id); + if (!foundUser) { + throw new NotFoundException(`User ${id} not found`); + } + + return UserMapper.toDto(foundUser); + } + + /** + * Update user (admin only) + */ + @Patch('users/:id') + @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) + @ApiOperation({ + summary: 'Update user (Admin only)', + description: 'Update user details (any user, any organization)', + }) + @ApiParam({ + name: 'id', + description: 'User ID (UUID)', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'User updated successfully', + type: UserResponseDto, + }) + @ApiNotFoundResponse({ + description: 'User not found', + }) + async updateUser( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: UpdateUserDto, + @CurrentUser() user: UserPayload + ): Promise { + this.logger.log(`[ADMIN: ${user.email}] Updating user: ${id}`); + + const foundUser = await this.userRepository.findById(id); + if (!foundUser) { + throw new NotFoundException(`User ${id} not found`); + } + + // Security: Prevent users from changing their own role + if (dto.role && id === user.id) { + this.logger.warn(`[SECURITY] User ${user.email} attempted to change their own role`); + throw new BadRequestException('You cannot change your own role'); + } + + // Apply updates + if (dto.firstName) { + foundUser.updateFirstName(dto.firstName); + } + if (dto.lastName) { + foundUser.updateLastName(dto.lastName); + } + if (dto.role) { + foundUser.updateRole(dto.role); + } + if (dto.isActive !== undefined) { + if (dto.isActive) { + foundUser.activate(); + } else { + foundUser.deactivate(); + } + } + + const updatedUser = await this.userRepository.update(foundUser); + this.logger.log(`[ADMIN] User updated successfully: ${updatedUser.id}`); + + return UserMapper.toDto(updatedUser); + } + + /** + * Delete user (admin only) + */ + @Delete('users/:id') + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ + summary: 'Delete user (Admin only)', + description: 'Permanently delete a user from the database', + }) + @ApiParam({ + name: 'id', + description: 'User ID (UUID)', + }) + @ApiResponse({ + status: HttpStatus.NO_CONTENT, + description: 'User deleted successfully', + }) + @ApiNotFoundResponse({ + description: 'User not found', + }) + async deleteUser( + @Param('id', ParseUUIDPipe) id: string, + @CurrentUser() user: UserPayload + ): Promise { + this.logger.log(`[ADMIN: ${user.email}] Deleting user: ${id}`); + + const foundUser = await this.userRepository.findById(id); + if (!foundUser) { + throw new NotFoundException(`User ${id} not found`); + } + + await this.userRepository.deleteById(id); + this.logger.log(`[ADMIN] User deleted successfully: ${id}`); + } + + // ==================== ORGANIZATIONS ENDPOINTS ==================== + + /** + * Get ALL organizations from database (admin only) + * + * Returns all organizations regardless of status (active/inactive) + */ + @Get('organizations') + @ApiOperation({ + summary: 'Get all organizations (Admin only)', + description: + 'Retrieve ALL organizations from the database without any filters. Includes active and inactive organizations.', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'All organizations retrieved successfully', + type: OrganizationListResponseDto, + }) + @ApiResponse({ + status: 401, + description: 'Unauthorized - missing or invalid token', + }) + @ApiResponse({ + status: 403, + description: 'Forbidden - requires admin role', + }) + async getAllOrganizations( + @CurrentUser() user: UserPayload + ): Promise { + this.logger.log(`[ADMIN: ${user.email}] Fetching ALL organizations from database`); + + const organizations = await this.organizationRepository.findAll(); + const orgDtos = OrganizationMapper.toDtoArray(organizations); + + this.logger.log(`[ADMIN] Retrieved ${organizations.length} organizations from database`); + + return { + organizations: orgDtos, + total: organizations.length, + page: 1, + pageSize: organizations.length, + totalPages: 1, + }; + } + + /** + * Get organization by ID (admin only) + */ + @Get('organizations/:id') + @ApiOperation({ + summary: 'Get organization by ID (Admin only)', + description: 'Retrieve a specific organization by ID', + }) + @ApiParam({ + name: 'id', + description: 'Organization ID (UUID)', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Organization retrieved successfully', + type: OrganizationResponseDto, + }) + @ApiNotFoundResponse({ + description: 'Organization not found', + }) + async getOrganizationById( + @Param('id', ParseUUIDPipe) id: string, + @CurrentUser() user: UserPayload + ): Promise { + this.logger.log(`[ADMIN: ${user.email}] Fetching organization: ${id}`); + + const organization = await this.organizationRepository.findById(id); + if (!organization) { + throw new NotFoundException(`Organization ${id} not found`); + } + + return OrganizationMapper.toDto(organization); + } + + // ==================== CSV BOOKINGS ENDPOINTS ==================== + + /** + * Get ALL csv bookings from database (admin only) + * + * Returns all csv bookings from all organizations + */ + @Get('bookings') + @ApiOperation({ + summary: 'Get all CSV bookings (Admin only)', + description: + 'Retrieve ALL CSV bookings from the database without any filters. Includes bookings from all organizations and all statuses.', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'All CSV bookings retrieved successfully', + }) + @ApiResponse({ + status: 401, + description: 'Unauthorized - missing or invalid token', + }) + @ApiResponse({ + status: 403, + description: 'Forbidden - requires admin role', + }) + async getAllBookings(@CurrentUser() user: UserPayload) { + this.logger.log(`[ADMIN: ${user.email}] Fetching ALL csv bookings from database`); + + const csvBookings = await this.csvBookingRepository.findAll(); + const bookingDtos = csvBookings.map(booking => this.csvBookingToDto(booking)); + + this.logger.log(`[ADMIN] Retrieved ${csvBookings.length} csv bookings from database`); + + return { + bookings: bookingDtos, + total: csvBookings.length, + page: 1, + pageSize: csvBookings.length, + totalPages: csvBookings.length > 0 ? 1 : 0, + }; + } + + /** + * Get csv booking by ID (admin only) + */ + @Get('bookings/:id') + @ApiOperation({ + summary: 'Get CSV booking by ID (Admin only)', + description: 'Retrieve a specific CSV booking by ID', + }) + @ApiParam({ + name: 'id', + description: 'Booking ID (UUID)', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'CSV booking retrieved successfully', + }) + @ApiNotFoundResponse({ + description: 'CSV booking not found', + }) + async getBookingById(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: UserPayload) { + this.logger.log(`[ADMIN: ${user.email}] Fetching csv booking: ${id}`); + + const csvBooking = await this.csvBookingRepository.findById(id); + if (!csvBooking) { + throw new NotFoundException(`CSV booking ${id} not found`); + } + + return this.csvBookingToDto(csvBooking); + } + + /** + * Update csv booking (admin only) + */ + @Patch('bookings/:id') + @ApiOperation({ + summary: 'Update CSV booking (Admin only)', + description: 'Update CSV booking status or details', + }) + @ApiParam({ + name: 'id', + description: 'Booking ID (UUID)', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'CSV booking updated successfully', + }) + @ApiNotFoundResponse({ + description: 'CSV booking not found', + }) + async updateBooking( + @Param('id', ParseUUIDPipe) id: string, + @Body() updateDto: any, + @CurrentUser() user: UserPayload + ) { + this.logger.log(`[ADMIN: ${user.email}] Updating csv booking: ${id}`); + + const csvBooking = await this.csvBookingRepository.findById(id); + if (!csvBooking) { + throw new NotFoundException(`CSV booking ${id} not found`); + } + + // Apply updates to the domain entity + // Note: This is a simplified version. You may want to add proper domain methods + const updatedBooking = await this.csvBookingRepository.update(csvBooking); + + this.logger.log(`[ADMIN] CSV booking updated successfully: ${id}`); + return this.csvBookingToDto(updatedBooking); + } + + /** + * Delete csv booking (admin only) + */ + @Delete('bookings/:id') + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ + summary: 'Delete CSV booking (Admin only)', + description: 'Permanently delete a CSV booking from the database', + }) + @ApiParam({ + name: 'id', + description: 'Booking ID (UUID)', + }) + @ApiResponse({ + status: HttpStatus.NO_CONTENT, + description: 'CSV booking deleted successfully', + }) + @ApiNotFoundResponse({ + description: 'CSV booking not found', + }) + async deleteBooking( + @Param('id', ParseUUIDPipe) id: string, + @CurrentUser() user: UserPayload + ): Promise { + this.logger.log(`[ADMIN: ${user.email}] Deleting csv booking: ${id}`); + + const csvBooking = await this.csvBookingRepository.findById(id); + if (!csvBooking) { + throw new NotFoundException(`CSV booking ${id} not found`); + } + + await this.csvBookingRepository.delete(id); + this.logger.log(`[ADMIN] CSV booking deleted successfully: ${id}`); + } + + /** + * Helper method to convert CSV booking domain entity to DTO + */ + private csvBookingToDto(booking: any) { + const primaryCurrency = booking.primaryCurrency as 'USD' | 'EUR'; + + return { + id: booking.id, + userId: booking.userId, + organizationId: booking.organizationId, + carrierName: booking.carrierName, + carrierEmail: booking.carrierEmail, + origin: booking.origin.getValue(), + destination: booking.destination.getValue(), + volumeCBM: booking.volumeCBM, + weightKG: booking.weightKG, + palletCount: booking.palletCount, + priceUSD: booking.priceUSD, + priceEUR: booking.priceEUR, + primaryCurrency: booking.primaryCurrency, + transitDays: booking.transitDays, + containerType: booking.containerType, + status: booking.status, + documents: booking.documents || [], + confirmationToken: booking.confirmationToken, + requestedAt: booking.requestedAt, + respondedAt: booking.respondedAt || null, + notes: booking.notes, + rejectionReason: booking.rejectionReason, + routeDescription: booking.getRouteDescription(), + isExpired: booking.isExpired(), + price: booking.getPriceInCurrency(primaryCurrency), + }; + } + + // ==================== DOCUMENTS ENDPOINTS ==================== + + /** + * Get ALL documents from all organizations (admin only) + * + * Returns documents grouped by organization + */ + @Get('documents') + @ApiOperation({ + summary: 'Get all documents (Admin only)', + description: 'Retrieve ALL documents from all organizations in the database.', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'All documents retrieved successfully', + }) + @ApiResponse({ + status: 401, + description: 'Unauthorized - missing or invalid token', + }) + @ApiResponse({ + status: 403, + description: 'Forbidden - requires admin role', + }) + async getAllDocuments(@CurrentUser() user: UserPayload): Promise { + this.logger.log(`[ADMIN: ${user.email}] Fetching ALL documents from database`); + + // Get all organizations + const organizations = await this.organizationRepository.findAll(); + + // Extract documents from all organizations + const allDocuments = organizations.flatMap(org => + org.documents.map(doc => ({ + ...doc, + organizationId: org.id, + organizationName: org.name, + })) + ); + + this.logger.log( + `[ADMIN] Retrieved ${allDocuments.length} documents from ${organizations.length} organizations` + ); + + return { + documents: allDocuments, + total: allDocuments.length, + organizationCount: organizations.length, + }; + } + + /** + * Get documents for a specific organization (admin only) + */ + @Get('organizations/:id/documents') + @ApiOperation({ + summary: 'Get organization documents (Admin only)', + description: 'Retrieve all documents for a specific organization', + }) + @ApiParam({ + name: 'id', + description: 'Organization ID (UUID)', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Organization documents retrieved successfully', + }) + @ApiNotFoundResponse({ + description: 'Organization not found', + }) + async getOrganizationDocuments( + @Param('id', ParseUUIDPipe) id: string, + @CurrentUser() user: UserPayload + ): Promise { + this.logger.log(`[ADMIN: ${user.email}] Fetching documents for organization: ${id}`); + + const organization = await this.organizationRepository.findById(id); + if (!organization) { + throw new NotFoundException(`Organization ${id} not found`); + } + + return { + organizationId: organization.id, + organizationName: organization.name, + documents: organization.documents, + total: organization.documents.length, + }; + } +} diff --git a/apps/backend/src/application/controllers/audit.controller.ts b/apps/backend/src/application/controllers/audit.controller.ts index 0ae9271..12589cf 100644 --- a/apps/backend/src/application/controllers/audit.controller.ts +++ b/apps/backend/src/application/controllers/audit.controller.ts @@ -38,7 +38,7 @@ class AuditLogResponseDto { timestamp: string; } -class AuditLogQueryDto { +class _AuditLogQueryDto { userId?: string; action?: AuditAction[]; status?: AuditStatus[]; diff --git a/apps/backend/src/application/controllers/bookings.controller.ts b/apps/backend/src/application/controllers/bookings.controller.ts index 5da7b5c..c2279b3 100644 --- a/apps/backend/src/application/controllers/bookings.controller.ts +++ b/apps/backend/src/application/controllers/bookings.controller.ts @@ -34,7 +34,7 @@ import { import { Response } from 'express'; import { CreateBookingRequestDto, BookingResponseDto, BookingListResponseDto } from '../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 { BookingService } from '@domain/services/booking.service'; 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 { FuzzySearchService } from '../services/fuzzy-search.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 { NotificationsGateway } from '../gateways/notifications.gateway'; import { WebhookService } from '../services/webhook.service'; diff --git a/apps/backend/src/application/controllers/csv-bookings.controller.ts b/apps/backend/src/application/controllers/csv-bookings.controller.ts index 8436ca0..9b085cd 100644 --- a/apps/backend/src/application/controllers/csv-bookings.controller.ts +++ b/apps/backend/src/application/controllers/csv-bookings.controller.ts @@ -13,8 +13,6 @@ import { BadRequestException, ParseIntPipe, DefaultValuePipe, - Res, - HttpStatus, } from '@nestjs/common'; import { FilesInterceptor } from '@nestjs/platform-express'; import { @@ -27,14 +25,12 @@ import { ApiQuery, ApiParam, } from '@nestjs/swagger'; -import { Response } from 'express'; import { JwtAuthGuard } from '../guards/jwt-auth.guard'; import { Public } from '../decorators/public.decorator'; import { CsvBookingService } from '../services/csv-booking.service'; import { CreateCsvBookingDto, CsvBookingResponseDto, - UpdateCsvBookingStatusDto, CsvBookingListResponseDto, CsvBookingStatsDto, } from '../dto/csv-booking.dto'; diff --git a/apps/backend/src/application/controllers/invitations.controller.ts b/apps/backend/src/application/controllers/invitations.controller.ts index 0a8677d..40436d2 100644 --- a/apps/backend/src/application/controllers/invitations.controller.ts +++ b/apps/backend/src/application/controllers/invitations.controller.ts @@ -8,15 +8,10 @@ import { HttpStatus, Logger, Param, - ParseUUIDPipe, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiParam } from '@nestjs/swagger'; import { InvitationService } from '../services/invitation.service'; -import { - CreateInvitationDto, - InvitationResponseDto, - VerifyInvitationDto, -} from '../dto/invitation.dto'; +import { CreateInvitationDto, InvitationResponseDto } from '../dto/invitation.dto'; import { JwtAuthGuard } from '../guards/jwt-auth.guard'; import { RolesGuard } from '../guards/roles.guard'; import { Roles } from '../decorators/roles.decorator'; diff --git a/apps/backend/src/application/controllers/users.controller.ts b/apps/backend/src/application/controllers/users.controller.ts index e6c46d4..1609a16 100644 --- a/apps/backend/src/application/controllers/users.controller.ts +++ b/apps/backend/src/application/controllers/users.controller.ts @@ -371,7 +371,9 @@ export class UsersController { ); // 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); // Security: Non-admin users cannot see ADMIN users @@ -379,7 +381,9 @@ export class UsersController { users = users.filter(u => u.role !== DomainUserRole.ADMIN); this.logger.log(`[SECURITY] Non-admin user ${currentUser.email} - filtered out ADMIN users`); } 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 diff --git a/apps/backend/src/application/controllers/webhooks.controller.ts b/apps/backend/src/application/controllers/webhooks.controller.ts index 0126b89..b83a038 100644 --- a/apps/backend/src/application/controllers/webhooks.controller.ts +++ b/apps/backend/src/application/controllers/webhooks.controller.ts @@ -17,11 +17,7 @@ import { ForbiddenException, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; -import { - WebhookService, - CreateWebhookInput, - UpdateWebhookInput, -} from '../services/webhook.service'; +import { WebhookService, CreateWebhookInput } from '../services/webhook.service'; import { JwtAuthGuard } from '../guards/jwt-auth.guard'; import { RolesGuard } from '../guards/roles.guard'; import { Roles } from '../decorators/roles.decorator'; diff --git a/apps/backend/src/application/dto/csv-booking.dto.ts b/apps/backend/src/application/dto/csv-booking.dto.ts index 675426f..5a69d81 100644 --- a/apps/backend/src/application/dto/csv-booking.dto.ts +++ b/apps/backend/src/application/dto/csv-booking.dto.ts @@ -6,14 +6,9 @@ import { Min, IsOptional, IsEnum, - IsArray, - ValidateNested, - IsUUID, - IsDateString, MinLength, MaxLength, } from 'class-validator'; -import { Type } from 'class-transformer'; /** * Create CSV Booking DTO diff --git a/apps/backend/src/application/dto/rate-search-filters.dto.ts b/apps/backend/src/application/dto/rate-search-filters.dto.ts index 6b86758..3270dbe 100644 --- a/apps/backend/src/application/dto/rate-search-filters.dto.ts +++ b/apps/backend/src/application/dto/rate-search-filters.dto.ts @@ -4,7 +4,6 @@ import { IsArray, IsNumber, Min, - Max, IsEnum, IsBoolean, IsDateString, diff --git a/apps/backend/src/application/dto/user.dto.ts b/apps/backend/src/application/dto/user.dto.ts index 4592401..d5c3cbf 100644 --- a/apps/backend/src/application/dto/user.dto.ts +++ b/apps/backend/src/application/dto/user.dto.ts @@ -5,7 +5,6 @@ import { IsEnum, IsNotEmpty, MinLength, - MaxLength, IsOptional, IsBoolean, IsUUID, diff --git a/apps/backend/src/application/guards/throttle.guard.ts b/apps/backend/src/application/guards/throttle.guard.ts index 37fc1ef..d71d722 100644 --- a/apps/backend/src/application/guards/throttle.guard.ts +++ b/apps/backend/src/application/guards/throttle.guard.ts @@ -23,7 +23,7 @@ export class CustomThrottlerGuard extends ThrottlerGuard { /** * Custom error message (override for new API) */ - protected async throwThrottlingException(context: ExecutionContext): Promise { + protected async throwThrottlingException(_context: ExecutionContext): Promise { throw new ThrottlerException('Too many requests. Please try again later.'); } } diff --git a/apps/backend/src/application/interceptors/performance-monitoring.interceptor.ts b/apps/backend/src/application/interceptors/performance-monitoring.interceptor.ts index d33c6e5..16befff 100644 --- a/apps/backend/src/application/interceptors/performance-monitoring.interceptor.ts +++ b/apps/backend/src/application/interceptors/performance-monitoring.interceptor.ts @@ -19,7 +19,7 @@ export class PerformanceMonitoringInterceptor implements NestInterceptor { const startTime = Date.now(); return next.handle().pipe( - tap(data => { + tap(_data => { const duration = Date.now() - startTime; const response = context.switchToHttp().getResponse(); diff --git a/apps/backend/src/application/mappers/booking.mapper.ts b/apps/backend/src/application/mappers/booking.mapper.ts index 820f116..bed8e26 100644 --- a/apps/backend/src/application/mappers/booking.mapper.ts +++ b/apps/backend/src/application/mappers/booking.mapper.ts @@ -1,19 +1,7 @@ import { Booking } from '@domain/entities/booking.entity'; import { RateQuote } from '@domain/entities/rate-quote.entity'; -import { - BookingResponseDto, - BookingAddressDto, - BookingPartyDto, - BookingContainerDto, - BookingRateQuoteDto, - BookingListItemDto, -} from '../dto/booking-response.dto'; -import { - CreateBookingRequestDto, - PartyDto, - AddressDto, - ContainerDto, -} from '../dto/create-booking-request.dto'; +import { BookingResponseDto, BookingListItemDto } from '../dto/booking-response.dto'; +import { CreateBookingRequestDto } from '../dto/create-booking-request.dto'; export class BookingMapper { /** diff --git a/apps/backend/src/application/mappers/csv-rate.mapper.ts b/apps/backend/src/application/mappers/csv-rate.mapper.ts index 28973c6..82ca23b 100644 --- a/apps/backend/src/application/mappers/csv-rate.mapper.ts +++ b/apps/backend/src/application/mappers/csv-rate.mapper.ts @@ -1,9 +1,6 @@ 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 { - CsvRateSearchInput, CsvRateSearchOutput, CsvRateSearchResult, RateSearchFilters, diff --git a/apps/backend/src/application/mappers/port.mapper.ts b/apps/backend/src/application/mappers/port.mapper.ts index 653083a..fdcc69f 100644 --- a/apps/backend/src/application/mappers/port.mapper.ts +++ b/apps/backend/src/application/mappers/port.mapper.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { Port } from '@domain/entities/port.entity'; -import { PortResponseDto, PortCoordinatesDto, PortSearchResponseDto } from '../dto/port.dto'; +import { PortResponseDto, PortSearchResponseDto } from '../dto/port.dto'; @Injectable() export class PortMapper { diff --git a/apps/backend/src/application/mappers/rate-quote.mapper.ts b/apps/backend/src/application/mappers/rate-quote.mapper.ts index b47bf9f..f4ce65e 100644 --- a/apps/backend/src/application/mappers/rate-quote.mapper.ts +++ b/apps/backend/src/application/mappers/rate-quote.mapper.ts @@ -1,11 +1,5 @@ import { RateQuote } from '@domain/entities/rate-quote.entity'; -import { - RateQuoteDto, - PortDto, - SurchargeDto, - PricingDto, - RouteSegmentDto, -} from '../dto/rate-search-response.dto'; +import { RateQuoteDto } from '../dto/rate-search-response.dto'; export class RateQuoteMapper { /** diff --git a/apps/backend/src/application/services/carrier-auth.service.ts b/apps/backend/src/application/services/carrier-auth.service.ts index ea078ed..a126133 100644 --- a/apps/backend/src/application/services/carrier-auth.service.ts +++ b/apps/backend/src/application/services/carrier-auth.service.ts @@ -4,13 +4,7 @@ * Handles carrier authentication and automatic account creation */ -import { - Injectable, - Logger, - UnauthorizedException, - ConflictException, - Inject, -} from '@nestjs/common'; +import { Injectable, Logger, UnauthorizedException, Inject } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; diff --git a/apps/backend/src/application/services/file-validation.service.ts b/apps/backend/src/application/services/file-validation.service.ts index 587bac7..c8b8d15 100644 --- a/apps/backend/src/application/services/file-validation.service.ts +++ b/apps/backend/src/application/services/file-validation.service.ts @@ -4,7 +4,7 @@ * 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 * as path from 'path'; diff --git a/apps/backend/src/application/services/webhook.service.ts b/apps/backend/src/application/services/webhook.service.ts index b19fd06..daffe97 100644 --- a/apps/backend/src/application/services/webhook.service.ts +++ b/apps/backend/src/application/services/webhook.service.ts @@ -9,7 +9,7 @@ import { HttpService } from '@nestjs/axios'; import { v4 as uuidv4 } from 'uuid'; import * as crypto from 'crypto'; import { firstValueFrom } from 'rxjs'; -import { Webhook, WebhookEvent, WebhookStatus } from '@domain/entities/webhook.entity'; +import { Webhook, WebhookEvent } from '@domain/entities/webhook.entity'; import { WebhookRepository, WEBHOOK_REPOSITORY, diff --git a/apps/backend/src/domain/entities/csv-booking.entity.spec.ts b/apps/backend/src/domain/entities/csv-booking.entity.spec.ts index 8d47188..7bcd7a8 100644 --- a/apps/backend/src/domain/entities/csv-booking.entity.spec.ts +++ b/apps/backend/src/domain/entities/csv-booking.entity.spec.ts @@ -9,7 +9,7 @@ import { PortCode } from '../value-objects/port-code.vo'; describe('CsvBooking Entity', () => { // Test data factory const createValidBooking = ( - overrides?: Partial[0]> + _overrides?: Partial[0]> ): CsvBooking => { const documents: CsvBookingDocument[] = [ { diff --git a/apps/backend/src/domain/entities/csv-rate.entity.ts b/apps/backend/src/domain/entities/csv-rate.entity.ts index 6321488..7b4bd0c 100644 --- a/apps/backend/src/domain/entities/csv-rate.entity.ts +++ b/apps/backend/src/domain/entities/csv-rate.entity.ts @@ -2,7 +2,7 @@ import { PortCode } from '../value-objects/port-code.vo'; import { ContainerType } from '../value-objects/container-type.vo'; import { Money } from '../value-objects/money.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'; /** diff --git a/apps/backend/src/domain/entities/rate-quote.entity.spec.ts b/apps/backend/src/domain/entities/rate-quote.entity.spec.ts index bc56517..89466d2 100644 --- a/apps/backend/src/domain/entities/rate-quote.entity.spec.ts +++ b/apps/backend/src/domain/entities/rate-quote.entity.spec.ts @@ -64,7 +64,7 @@ describe('RateQuote Entity', () => { it('should set validUntil to 15 minutes from now', () => { const before = new Date(); const rateQuote = RateQuote.create(validProps); - const after = new Date(); + const _after = new Date(); const expectedValidUntil = new Date(before.getTime() + 15 * 60 * 1000); const diff = Math.abs(rateQuote.validUntil.getTime() - expectedValidUntil.getTime()); diff --git a/apps/backend/src/domain/ports/in/search-csv-rates.port.ts b/apps/backend/src/domain/ports/in/search-csv-rates.port.ts index 7381fd0..7f56210 100644 --- a/apps/backend/src/domain/ports/in/search-csv-rates.port.ts +++ b/apps/backend/src/domain/ports/in/search-csv-rates.port.ts @@ -1,6 +1,4 @@ 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'; /** diff --git a/apps/backend/src/domain/services/csv-rate-search.service.ts b/apps/backend/src/domain/services/csv-rate-search.service.ts index 4deb351..bfcff6f 100644 --- a/apps/backend/src/domain/services/csv-rate-search.service.ts +++ b/apps/backend/src/domain/services/csv-rate-search.service.ts @@ -2,7 +2,6 @@ import { CsvRate } from '../entities/csv-rate.entity'; import { PortCode } from '../value-objects/port-code.vo'; import { ContainerType } from '../value-objects/container-type.vo'; import { Volume } from '../value-objects/volume.vo'; -import { Money } from '../value-objects/money.vo'; import { SearchCsvRatesPort, CsvRateSearchInput, diff --git a/apps/backend/src/infrastructure/carriers/cma-cgm/cma-cgm.connector.ts b/apps/backend/src/infrastructure/carriers/cma-cgm/cma-cgm.connector.ts index e421116..360ccca 100644 --- a/apps/backend/src/infrastructure/carriers/cma-cgm/cma-cgm.connector.ts +++ b/apps/backend/src/infrastructure/carriers/cma-cgm/cma-cgm.connector.ts @@ -4,7 +4,7 @@ * 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 { CarrierConnectorPort, diff --git a/apps/backend/src/infrastructure/carriers/hapag-lloyd/hapag-lloyd.connector.ts b/apps/backend/src/infrastructure/carriers/hapag-lloyd/hapag-lloyd.connector.ts index a03960f..e01a31f 100644 --- a/apps/backend/src/infrastructure/carriers/hapag-lloyd/hapag-lloyd.connector.ts +++ b/apps/backend/src/infrastructure/carriers/hapag-lloyd/hapag-lloyd.connector.ts @@ -4,7 +4,7 @@ * 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 { CarrierConnectorPort, diff --git a/apps/backend/src/infrastructure/carriers/maersk/maersk-response.mapper.ts b/apps/backend/src/infrastructure/carriers/maersk/maersk-response.mapper.ts index aa7eed7..ecc676a 100644 --- a/apps/backend/src/infrastructure/carriers/maersk/maersk-response.mapper.ts +++ b/apps/backend/src/infrastructure/carriers/maersk/maersk-response.mapper.ts @@ -25,8 +25,8 @@ export class MaerskResponseMapper { */ private static toRateQuote( result: MaerskRateResult, - originCode: string, - destinationCode: string + _originCode: string, + _destinationCode: string ): RateQuote { const surcharges = result.pricing.charges.map(charge => ({ type: charge.chargeCode, diff --git a/apps/backend/src/infrastructure/carriers/maersk/maersk.connector.ts b/apps/backend/src/infrastructure/carriers/maersk/maersk.connector.ts index 205e444..1b5ee77 100644 --- a/apps/backend/src/infrastructure/carriers/maersk/maersk.connector.ts +++ b/apps/backend/src/infrastructure/carriers/maersk/maersk.connector.ts @@ -6,7 +6,6 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { v4 as uuidv4 } from 'uuid'; import { BaseCarrierConnector, CarrierConfig } from '../base-carrier.connector'; import { CarrierRateSearchInput, @@ -15,7 +14,7 @@ import { import { RateQuote } from '@domain/entities/rate-quote.entity'; import { MaerskRequestMapper } from './maersk-request.mapper'; import { MaerskResponseMapper } from './maersk-response.mapper'; -import { MaerskRateSearchRequest, MaerskRateSearchResponse } from './maersk.types'; +import { MaerskRateSearchResponse } from './maersk.types'; @Injectable() export class MaerskConnector extends BaseCarrierConnector { diff --git a/apps/backend/src/infrastructure/carriers/msc/msc.connector.ts b/apps/backend/src/infrastructure/carriers/msc/msc.connector.ts index f64580e..6ba5b94 100644 --- a/apps/backend/src/infrastructure/carriers/msc/msc.connector.ts +++ b/apps/backend/src/infrastructure/carriers/msc/msc.connector.ts @@ -4,7 +4,7 @@ * Implements CarrierConnectorPort for MSC API integration */ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { CarrierConnectorPort, diff --git a/apps/backend/src/infrastructure/carriers/one/one.connector.ts b/apps/backend/src/infrastructure/carriers/one/one.connector.ts index 20c56ba..db65083 100644 --- a/apps/backend/src/infrastructure/carriers/one/one.connector.ts +++ b/apps/backend/src/infrastructure/carriers/one/one.connector.ts @@ -4,7 +4,7 @@ * Implements CarrierConnectorPort for ONE API */ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { CarrierConnectorPort, diff --git a/apps/backend/src/infrastructure/persistence/typeorm/repositories/csv-booking.repository.ts b/apps/backend/src/infrastructure/persistence/typeorm/repositories/csv-booking.repository.ts index 55cd155..af8c8b5 100644 --- a/apps/backend/src/infrastructure/persistence/typeorm/repositories/csv-booking.repository.ts +++ b/apps/backend/src/infrastructure/persistence/typeorm/repositories/csv-booking.repository.ts @@ -1,7 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/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 { CsvBookingOrmEntity } from '../entities/csv-booking.orm-entity'; import { CsvBookingMapper } from '../mappers/csv-booking.mapper'; @@ -110,7 +110,6 @@ export class TypeOrmCsvBookingRepository implements CsvBookingRepositoryPort { async findExpiringSoon(daysUntilExpiration: number): Promise { this.logger.log(`Finding CSV bookings expiring in ${daysUntilExpiration} days`); - const now = new Date(); const expirationDate = new Date(); expirationDate.setDate(expirationDate.getDate() + daysUntilExpiration); diff --git a/apps/backend/src/infrastructure/persistence/typeorm/repositories/typeorm-audit-log.repository.ts b/apps/backend/src/infrastructure/persistence/typeorm/repositories/typeorm-audit-log.repository.ts index 8db6d5b..eaf011f 100644 --- a/apps/backend/src/infrastructure/persistence/typeorm/repositories/typeorm-audit-log.repository.ts +++ b/apps/backend/src/infrastructure/persistence/typeorm/repositories/typeorm-audit-log.repository.ts @@ -4,7 +4,7 @@ import { Injectable } from '@nestjs/common'; 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 { AuditLog, AuditStatus, AuditAction } from '@domain/entities/audit-log.entity'; import { AuditLogOrmEntity } from '../entities/audit-log.orm-entity'; diff --git a/apps/backend/src/infrastructure/persistence/typeorm/repositories/typeorm-port.repository.ts b/apps/backend/src/infrastructure/persistence/typeorm/repositories/typeorm-port.repository.ts index ae89165..89c6652 100644 --- a/apps/backend/src/infrastructure/persistence/typeorm/repositories/typeorm-port.repository.ts +++ b/apps/backend/src/infrastructure/persistence/typeorm/repositories/typeorm-port.repository.ts @@ -6,7 +6,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, ILike } from 'typeorm'; +import { Repository } from 'typeorm'; import { Port } from '@domain/entities/port.entity'; import { PortRepository } from '@domain/ports/out/port.repository'; import { PortOrmEntity } from '../entities/port.orm-entity'; diff --git a/apps/backend/src/infrastructure/persistence/typeorm/seeds/test-organizations.seed.ts b/apps/backend/src/infrastructure/persistence/typeorm/seeds/test-organizations.seed.ts index 7eded0d..4372d7d 100644 --- a/apps/backend/src/infrastructure/persistence/typeorm/seeds/test-organizations.seed.ts +++ b/apps/backend/src/infrastructure/persistence/typeorm/seeds/test-organizations.seed.ts @@ -4,8 +4,6 @@ * Seeds test organizations for development */ -import { v4 as uuidv4 } from 'uuid'; - export interface OrganizationSeed { id: string; name: string; diff --git a/apps/backend/test/carrier-portal.e2e-spec.ts b/apps/backend/test/carrier-portal.e2e-spec.ts index 5824190..60c90b6 100644 --- a/apps/backend/test/carrier-portal.e2e-spec.ts +++ b/apps/backend/test/carrier-portal.e2e-spec.ts @@ -16,7 +16,7 @@ import { AppModule } from '../src/app.module'; describe('Carrier Portal (e2e)', () => { let app: INestApplication; let carrierAccessToken: string; - let carrierId: string; + let _carrierId: string; let bookingId: string; beforeAll(async () => { @@ -53,7 +53,7 @@ describe('Carrier Portal (e2e)', () => { // Save tokens for subsequent tests carrierAccessToken = res.body.accessToken; - carrierId = res.body.carrier.id; + _carrierId = res.body.carrier.id; }); }); diff --git a/apps/frontend/app/booking/confirm/[token]/page.tsx b/apps/frontend/app/booking/confirm/[token]/page.tsx index 5427b0e..e6c5ac9 100644 --- a/apps/frontend/app/booking/confirm/[token]/page.tsx +++ b/apps/frontend/app/booking/confirm/[token]/page.tsx @@ -7,7 +7,7 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { useParams, useRouter } from 'next/navigation'; import { acceptCsvBooking, type CsvBookingResponse } from '@/lib/api/bookings'; @@ -21,18 +21,7 @@ export default function BookingConfirmPage() { const [booking, setBooking] = useState(null); const [isAccepting, setIsAccepting] = useState(false); - useEffect(() => { - if (!token) { - setError('Token de confirmation invalide'); - setIsLoading(false); - return; - } - - // Auto-accept the booking - handleAccept(); - }, [token]); - - const handleAccept = async () => { + const handleAccept = useCallback(async () => { setIsAccepting(true); setError(null); @@ -50,7 +39,18 @@ export default function BookingConfirmPage() { setIsLoading(false); setIsAccepting(false); } - }; + }, [token]); + + useEffect(() => { + if (!token) { + setError('Token de confirmation invalide'); + setIsLoading(false); + return; + } + + // Auto-accept the booking + handleAccept(); + }, [token, handleAccept]); if (isLoading) { return ( diff --git a/apps/frontend/app/dashboard/admin/bookings/page.tsx b/apps/frontend/app/dashboard/admin/bookings/page.tsx index 6010a1c..1bb2a1a 100644 --- a/apps/frontend/app/dashboard/admin/bookings/page.tsx +++ b/apps/frontend/app/dashboard/admin/bookings/page.tsx @@ -34,8 +34,8 @@ interface Booking { createdAt?: string; updatedAt?: string; requestedAt?: string; - organizationId: string; - userId: string; + organizationId?: string; + userId?: string; } export default function AdminBookingsPage() { diff --git a/apps/frontend/app/dashboard/admin/documents/page.tsx b/apps/frontend/app/dashboard/admin/documents/page.tsx index 90f64b9..edfc288 100644 --- a/apps/frontend/app/dashboard/admin/documents/page.tsx +++ b/apps/frontend/app/dashboard/admin/documents/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { getAllBookings, getAllUsers } from '@/lib/api/admin'; interface Document { @@ -21,12 +21,12 @@ interface Booking { bookingNumber?: string; bookingId?: string; type?: string; - userId: string; - organizationId: string; + userId?: string; + organizationId?: string; origin?: string; destination?: string; carrierName?: string; - documents: Document[]; + documents?: Document[]; requestedAt?: string; status: string; } @@ -39,7 +39,6 @@ interface DocumentWithBooking extends Document { organizationId: string; route: string; status: string; - fileName?: string; fileType?: string; } @@ -94,11 +93,7 @@ export default function AdminDocumentsPage() { return typeMap[ext] || ext.toUpperCase(); }; - useEffect(() => { - fetchBookingsAndDocuments(); - }, []); - - const fetchBookingsAndDocuments = async () => { + const fetchBookingsAndDocuments = useCallback(async () => { try { setLoading(true); const response = await getAllBookings(); @@ -191,7 +186,11 @@ export default function AdminDocumentsPage() { } finally { setLoading(false); } - }; + }, []); + + useEffect(() => { + fetchBookingsAndDocuments(); + }, [fetchBookingsAndDocuments]); // Get unique users for filter (with names) const uniqueUsers = Array.from( diff --git a/apps/frontend/app/dashboard/admin/organizations/page.tsx b/apps/frontend/app/dashboard/admin/organizations/page.tsx index e6ffaa3..ddcabd7 100644 --- a/apps/frontend/app/dashboard/admin/organizations/page.tsx +++ b/apps/frontend/app/dashboard/admin/organizations/page.tsx @@ -34,7 +34,23 @@ export default function AdminOrganizationsPage() { const [showEditModal, setShowEditModal] = useState(false); // 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: '', type: 'FREIGHT_FORWARDER', scac: '', @@ -72,7 +88,19 @@ export default function AdminOrganizationsPage() { const handleCreate = async (e: React.FormEvent) => { e.preventDefault(); 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(); setShowCreateModal(false); resetForm(); diff --git a/apps/frontend/app/dashboard/admin/users/page.tsx b/apps/frontend/app/dashboard/admin/users/page.tsx index 48529eb..9b68847 100644 --- a/apps/frontend/app/dashboard/admin/users/page.tsx +++ b/apps/frontend/app/dashboard/admin/users/page.tsx @@ -4,13 +4,14 @@ import { useState, useEffect } from 'react'; import { getAllUsers, updateAdminUser, deleteAdminUser } from '@/lib/api/admin'; import { createUser } from '@/lib/api/users'; import { getAllOrganizations } from '@/lib/api/admin'; +import type { UserRole } from '@/types/api'; interface User { id: string; email: string; firstName: string; lastName: string; - role: string; + role: UserRole; organizationId: string; organizationName?: string; isActive: boolean; @@ -33,7 +34,14 @@ export default function AdminUsersPage() { const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); // Form state - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState<{ + email: string; + firstName: string; + lastName: string; + role: UserRole; + organizationId: string; + password: string; + }>({ email: '', firstName: '', lastName: '', @@ -290,7 +298,7 @@ export default function AdminUsersPage() { 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" > diff --git a/apps/frontend/app/dashboard/bookings/new/page.tsx b/apps/frontend/app/dashboard/bookings/new/page.tsx index 6623280..0e0489f 100644 --- a/apps/frontend/app/dashboard/bookings/new/page.tsx +++ b/apps/frontend/app/dashboard/bookings/new/page.tsx @@ -12,6 +12,7 @@ import { useState, useEffect } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; +import Image from 'next/image'; import { useMutation, useQuery } from '@tanstack/react-query'; import { createBooking } from '@/lib/api'; @@ -274,9 +275,11 @@ export default function NewBookingPage() {
{preselectedQuote.carrier.logoUrl ? ( - {preselectedQuote.carrier.name} ) : ( diff --git a/apps/frontend/app/dashboard/page.tsx b/apps/frontend/app/dashboard/page.tsx index f1afe89..caf812c 100644 --- a/apps/frontend/app/dashboard/page.tsx +++ b/apps/frontend/app/dashboard/page.tsx @@ -208,8 +208,8 @@ export default function DashboardPage() { cx="50%" cy="50%" labelLine={false} - label={({ name, percent }) => - `${name} ${(percent * 100).toFixed(0)}%` + label={({ name, percent }: any) => + `${name} ${((percent || 0) * 100).toFixed(0)}%` } outerRadius={70} fill="#8884d8" diff --git a/apps/frontend/app/dashboard/search-advanced/results/page.tsx b/apps/frontend/app/dashboard/search-advanced/results/page.tsx index 03c78af..c950e28 100644 --- a/apps/frontend/app/dashboard/search-advanced/results/page.tsx +++ b/apps/frontend/app/dashboard/search-advanced/results/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { searchCsvRatesWithOffers } from '@/lib/api/rates'; import type { CsvRateSearchResult } from '@/types/rates'; @@ -25,16 +25,7 @@ export default function SearchResultsPage() { const weightKG = parseFloat(searchParams.get('weightKG') || '0'); const palletCount = parseInt(searchParams.get('palletCount') || '0'); - useEffect(() => { - if (!origin || !destination || !volumeCBM || !weightKG) { - router.push('/dashboard/search-advanced'); - return; - } - - performSearch(); - }, [origin, destination, volumeCBM, weightKG, palletCount]); - - const performSearch = async () => { + const performSearch = useCallback(async () => { setIsLoading(true); setError(null); @@ -61,7 +52,16 @@ export default function SearchResultsPage() { } finally { 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 => { if (results.length === 0) return null; diff --git a/apps/frontend/app/dashboard/search/page.tsx b/apps/frontend/app/dashboard/search/page.tsx index f3de1af..557df4e 100644 --- a/apps/frontend/app/dashboard/search/page.tsx +++ b/apps/frontend/app/dashboard/search/page.tsx @@ -8,6 +8,7 @@ import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; +import Image from 'next/image'; import { searchRates } from '@/lib/api'; import { searchPorts, Port } from '@/lib/api/ports'; @@ -433,9 +434,11 @@ export default function RateSearchPage() {
{quote.carrier.logoUrl ? ( - {quote.carrier.name} ) : ( diff --git a/apps/frontend/app/dashboard/settings/organization/page.tsx b/apps/frontend/app/dashboard/settings/organization/page.tsx index ab006ee..993b406 100644 --- a/apps/frontend/app/dashboard/settings/organization/page.tsx +++ b/apps/frontend/app/dashboard/settings/organization/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback } from 'react'; import { useAuth } from '@/lib/context/auth-context'; import { getOrganization, updateOrganization } from '@/lib/api/organizations'; import type { OrganizationResponse } from '@/types/api'; @@ -42,17 +42,13 @@ export default function OrganizationSettingsPage() { // Check if user can edit organization (only ADMIN and MANAGER) const canEdit = user?.role === 'ADMIN' || user?.role === 'MANAGER'; - useEffect(() => { - if (user?.organizationId) { - loadOrganization(); - } - }, [user?.organizationId]); + const loadOrganization = useCallback(async () => { + if (!user?.organizationId) return; - const loadOrganization = async () => { try { setIsLoading(true); setError(null); - const org = await getOrganization(user!.organizationId); + const org = await getOrganization(user.organizationId); setOrganization(org); setFormData({ name: org.name || '', @@ -71,7 +67,13 @@ export default function OrganizationSettingsPage() { } finally { setIsLoading(false); } - }; + }, [user]); + + useEffect(() => { + if (user?.organizationId) { + loadOrganization(); + } + }, [user?.organizationId, loadOrganization]); const handleChange = (field: keyof OrganizationForm, value: string) => { setFormData(prev => ({ ...prev, [field]: value })); diff --git a/apps/frontend/app/test-image/page.tsx b/apps/frontend/app/test-image/page.tsx index 59aae34..771e595 100644 --- a/apps/frontend/app/test-image/page.tsx +++ b/apps/frontend/app/test-image/page.tsx @@ -1,3 +1,5 @@ +import Image from 'next/image'; + export default function TestImagePage() { return (
@@ -5,10 +7,12 @@ export default function TestImagePage() { {/* Test 1: Direct img tag */}
-

Test 1: Direct img tag

- Test 1: Direct Image component + test
diff --git a/apps/frontend/src/legacy-pages/CarrierMonitoring.tsx b/apps/frontend/src/legacy-pages/CarrierMonitoring.tsx index ae2f06c..820019d 100644 --- a/apps/frontend/src/legacy-pages/CarrierMonitoring.tsx +++ b/apps/frontend/src/legacy-pages/CarrierMonitoring.tsx @@ -2,7 +2,7 @@ * Carrier Monitoring Dashboard */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { CarrierStats, CarrierHealthCheck } from '@/types/carrier'; export const CarrierMonitoring: React.FC = () => { @@ -12,14 +12,7 @@ export const CarrierMonitoring: React.FC = () => { const [error, setError] = useState(null); const [timeRange, setTimeRange] = useState('24h'); - useEffect(() => { - fetchMonitoringData(); - // Refresh every 30 seconds - const interval = setInterval(fetchMonitoringData, 30000); - return () => clearInterval(interval); - }, [timeRange]); - - const fetchMonitoringData = async () => { + const fetchMonitoringData = useCallback(async () => { setLoading(true); setError(null); @@ -50,7 +43,14 @@ export const CarrierMonitoring: React.FC = () => { } finally { 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 => { return health.find(h => h.carrierId === carrierId);