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

This commit is contained in:
David 2025-12-23 11:49:57 +01:00
parent c19af3b119
commit a1e255e816
53 changed files with 819 additions and 818 deletions

View File

@ -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: '^_',

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -38,7 +38,7 @@ class AuditLogResponseDto {
timestamp: string;
}
class AuditLogQueryDto {
class _AuditLogQueryDto {
userId?: string;
action?: AuditAction[];
status?: AuditStatus[];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,6 @@ import {
IsArray,
IsNumber,
Min,
Max,
IsEnum,
IsBoolean,
IsDateString,

View File

@ -5,7 +5,6 @@ import {
IsEnum,
IsNotEmpty,
MinLength,
MaxLength,
IsOptional,
IsBoolean,
IsUUID,

View File

@ -23,7 +23,7 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
/**
* 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.');
}
}

View File

@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ import { PortCode } from '../value-objects/port-code.vo';
describe('CsvBooking Entity', () => {
// Test data factory
const createValidBooking = (
overrides?: Partial<ConstructorParameters<typeof CsvBooking>[0]>
_overrides?: Partial<ConstructorParameters<typeof CsvBooking>[0]>
): CsvBooking => {
const documents: CsvBookingDocument[] = [
{

View File

@ -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';
/**

View File

@ -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());

View File

@ -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';
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<CsvBooking[]> {
this.logger.log(`Finding CSV bookings expiring in ${daysUntilExpiration} days`);
const now = new Date();
const expirationDate = new Date();
expirationDate.setDate(expirationDate.getDate() + daysUntilExpiration);

View File

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

View File

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

View File

@ -4,8 +4,6 @@
* Seeds test organizations for development
*/
import { v4 as uuidv4 } from 'uuid';
export interface OrganizationSeed {
id: string;
name: string;

View File

@ -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;
});
});

View File

@ -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<CsvBookingResponse | null>(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 (

View File

@ -34,8 +34,8 @@ interface Booking {
createdAt?: string;
updatedAt?: string;
requestedAt?: string;
organizationId: string;
userId: string;
organizationId?: string;
userId?: string;
}
export default function AdminBookingsPage() {

View File

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

View File

@ -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();

View File

@ -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() {
<label className="block text-sm font-medium text-gray-700">Role</label>
<select
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"
>
<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>
<select
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"
>
<option value="USER">User</option>

View File

@ -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() {
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
{preselectedQuote.carrier.logoUrl ? (
<img
<Image
src={preselectedQuote.carrier.logoUrl}
alt={preselectedQuote.carrier.name}
width={48}
height={48}
className="h-12 w-12 object-contain"
/>
) : (

View File

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

View File

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

View File

@ -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() {
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
{quote.carrier.logoUrl ? (
<img
<Image
src={quote.carrier.logoUrl}
alt={quote.carrier.name}
width={48}
height={48}
className="h-12 w-12 object-contain"
/>
) : (

View File

@ -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 }));

View File

@ -1,3 +1,5 @@
import Image from 'next/image';
export default function TestImagePage() {
return (
<div className="min-h-screen p-8">
@ -5,10 +7,12 @@ export default function TestImagePage() {
{/* Test 1: Direct img tag */}
<div className="mb-8">
<h2 className="font-semibold mb-2">Test 1: Direct img tag</h2>
<img
<h2 className="font-semibold mb-2">Test 1: Direct Image component</h2>
<Image
src="/assets/images/background-section-1-landingpage.png"
alt="test"
width={256}
height={128}
className="w-64 h-32 object-cover"
/>
</div>

View File

@ -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<string | null>(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);