fix improve
This commit is contained in:
parent
fd1f57dd1d
commit
baf5981847
182
CLAUDE.md
182
CLAUDE.md
@ -4,21 +4,27 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Xpeditis** is a B2B SaaS maritime freight booking platform. Freight forwarders search and compare real-time shipping rates, book containers, and manage shipments. Built as a monorepo with NestJS backend (Hexagonal Architecture) and Next.js 14 frontend.
|
||||
**Xpeditis** is a B2B SaaS maritime freight booking platform. Freight forwarders search and compare real-time shipping rates, book containers, and manage shipments. Monorepo with NestJS 10 backend (Hexagonal Architecture) and Next.js 14 frontend.
|
||||
|
||||
## Development Commands
|
||||
|
||||
All commands run from repo root unless noted otherwise.
|
||||
|
||||
```bash
|
||||
# Start infrastructure (PostgreSQL + Redis + MinIO)
|
||||
# Infrastructure (PostgreSQL 15 + Redis 7 + MinIO)
|
||||
docker-compose up -d
|
||||
|
||||
# Install dependencies
|
||||
npm install && cd apps/backend && npm install && cd ../frontend && npm install
|
||||
# Install all dependencies
|
||||
npm run install:all
|
||||
|
||||
# Run database migrations
|
||||
# Environment setup (required on first run)
|
||||
cp apps/backend/.env.example apps/backend/.env
|
||||
cp apps/frontend/.env.example apps/frontend/.env
|
||||
|
||||
# Database migrations (from apps/backend/)
|
||||
cd apps/backend && npm run migration:run
|
||||
|
||||
# Start development servers
|
||||
# Development servers
|
||||
npm run backend:dev # http://localhost:4000, Swagger: /api/docs
|
||||
npm run frontend:dev # http://localhost:3000
|
||||
```
|
||||
@ -27,15 +33,32 @@ npm run frontend:dev # http://localhost:3000
|
||||
|
||||
```bash
|
||||
# Backend (from apps/backend/)
|
||||
npm test # Unit tests
|
||||
npm test # Unit tests (Jest)
|
||||
npm test -- booking.entity.spec.ts # Single file
|
||||
npm run test:cov # With coverage
|
||||
npm run test:integration # Integration tests (needs DB/Redis)
|
||||
npm run test:integration # Integration tests (needs DB/Redis, 30s timeout)
|
||||
npm run test:e2e # E2E tests
|
||||
|
||||
# Frontend (from apps/frontend/)
|
||||
npm test
|
||||
npx playwright test # E2E tests
|
||||
npm run test:e2e # Playwright (chromium, firefox, webkit + mobile)
|
||||
|
||||
# From root
|
||||
npm run backend:test
|
||||
npm run frontend:test
|
||||
```
|
||||
|
||||
Backend test config is in `apps/backend/package.json` (Jest). Integration test config: `apps/backend/jest-integration.json` (covers infrastructure layer, setup in `test/setup-integration.ts`). Frontend E2E config: `apps/frontend/playwright.config.ts`.
|
||||
|
||||
### Linting, Formatting & Type Checking
|
||||
|
||||
```bash
|
||||
npm run backend:lint # ESLint backend
|
||||
npm run frontend:lint # ESLint frontend
|
||||
npm run format # Prettier (all files)
|
||||
npm run format:check # Check formatting
|
||||
# From apps/frontend/
|
||||
npm run type-check # TypeScript checking (frontend only)
|
||||
```
|
||||
|
||||
### Database Migrations
|
||||
@ -50,10 +73,17 @@ npm run migration:revert
|
||||
### Build
|
||||
|
||||
```bash
|
||||
npm run backend:build # Compiles TS with path alias resolution (tsc-alias)
|
||||
npm run frontend:build # Next.js production build
|
||||
npm run backend:build # NestJS build with tsc-alias for path resolution
|
||||
npm run frontend:build # Next.js production build (standalone output)
|
||||
```
|
||||
|
||||
## Local Infrastructure
|
||||
|
||||
Docker-compose defaults (no `.env` changes needed for local dev):
|
||||
- **PostgreSQL**: `xpeditis:xpeditis_dev_password@localhost:5432/xpeditis_dev`
|
||||
- **Redis**: password `xpeditis_redis_password`, port 6379
|
||||
- **MinIO** (S3-compatible storage): `minioadmin:minioadmin`, API port 9000, console port 9001
|
||||
|
||||
## Architecture
|
||||
|
||||
### Hexagonal Architecture (Backend)
|
||||
@ -61,74 +91,108 @@ npm run frontend:build # Next.js production build
|
||||
```
|
||||
apps/backend/src/
|
||||
├── domain/ # CORE - Pure TypeScript, NO framework imports
|
||||
│ ├── entities/ # Booking, RateQuote, User (with private props, static create())
|
||||
│ ├── value-objects/# Money, Email, BookingNumber (immutable, validated)
|
||||
│ ├── services/ # Domain services
|
||||
│ ├── entities/ # Booking, RateQuote, User, Carrier, Port, Container, CsvBooking, etc.
|
||||
│ ├── value-objects/ # Money, Email, BookingNumber, BookingStatus, PortCode, ContainerType, Volume, LicenseStatus, SubscriptionPlan, etc.
|
||||
│ ├── services/ # Pure domain services (rate-search, csv-rate-price-calculator, booking, port-search, etc.)
|
||||
│ ├── ports/
|
||||
│ │ ├── in/ # Use case interfaces
|
||||
│ │ └── out/ # Repository interfaces (SPIs)
|
||||
│ └── exceptions/ # Domain exceptions
|
||||
├── application/ # Controllers, DTOs, Guards (depends ONLY on domain)
|
||||
└── infrastructure/ # TypeORM, Redis, Carrier APIs (depends ONLY on domain)
|
||||
│ │ ├── in/ # Use case interfaces with execute() method
|
||||
│ │ └── out/ # Repository/SPI interfaces (token constants like BOOKING_REPOSITORY = 'BookingRepository')
|
||||
│ └── exceptions/ # Domain-specific exceptions
|
||||
├── application/ # Controllers, DTOs (class-validator), Guards, Decorators, Mappers
|
||||
└── infrastructure/ # TypeORM entities/repos/mappers, Redis cache, carrier APIs, MinIO/S3, email (MJML+Nodemailer), Sentry
|
||||
```
|
||||
|
||||
**Critical Rules**:
|
||||
- Domain layer: Zero imports from NestJS, TypeORM, Redis
|
||||
- Dependencies flow inward: Infrastructure → Application → Domain
|
||||
- Use path aliases: `@domain/*`, `@application/*`, `@infrastructure/*`
|
||||
**Critical dependency rules**:
|
||||
- Domain layer: zero imports from NestJS, TypeORM, Redis, or any framework
|
||||
- Dependencies flow inward only: Infrastructure → Application → Domain
|
||||
- Path aliases: `@domain/*`, `@application/*`, `@infrastructure/*` (defined in `apps/backend/tsconfig.json`)
|
||||
- Domain tests run without NestJS TestingModule
|
||||
- Backend has strict TypeScript: `strict: true`, `strictNullChecks: true` (but `strictPropertyInitialization: false`)
|
||||
|
||||
### NestJS Modules (app.module.ts)
|
||||
|
||||
Global guards: JwtAuthGuard (all routes protected by default), CustomThrottlerGuard.
|
||||
|
||||
Feature modules: Auth, Rates, Ports, Bookings, CsvBookings, Organizations, Users, Dashboard, Audit, Notifications, Webhooks, GDPR, Admin, Subscriptions.
|
||||
|
||||
Infrastructure modules: CacheModule, CarrierModule, SecurityModule, CsvRateModule.
|
||||
|
||||
Swagger plugin enabled in `nest-cli.json` — DTOs auto-documented.
|
||||
|
||||
### Frontend (Next.js 14 App Router)
|
||||
|
||||
```
|
||||
apps/frontend/
|
||||
├── app/ # App Router pages
|
||||
│ ├── dashboard/ # Protected routes
|
||||
│ │ ├── bookings/ # Booking management
|
||||
│ │ ├── admin/ # Admin features (ADMIN role)
|
||||
│ │ └── settings/ # User/org settings
|
||||
│ ├── dashboard/ # Protected routes (bookings, admin, settings)
|
||||
│ └── carrier/ # Carrier portal (magic link auth)
|
||||
└── src/
|
||||
├── components/ # React components (shadcn/ui in ui/)
|
||||
├── hooks/ # useBookings, useNotifications
|
||||
├── lib/api/ # API client modules
|
||||
└── types/ # TypeScript definitions
|
||||
├── components/ # React components (shadcn/ui in ui/, layout/, bookings/, admin/)
|
||||
├── hooks/ # useBookings, useNotifications, useCsvRateSearch, useCompanies, useFilterOptions
|
||||
├── lib/
|
||||
│ ├── api/ # Fetch-based API client with auto token refresh (client.ts + per-module files)
|
||||
│ ├── context/ # Auth context
|
||||
│ └── fonts.ts # Manrope (headings) + Montserrat (body)
|
||||
├── types/ # TypeScript type definitions
|
||||
└── utils/ # Export utilities (Excel, PDF)
|
||||
```
|
||||
|
||||
Path aliases: `@/*` → `./src/*`, `@/components/*`, `@/lib/*`, `@/hooks/*`
|
||||
Path aliases: `@/*` → `./src/*`, `@/components/*`, `@/lib/*`, `@/app/*` → `./app/*`, `@/types/*`, `@/hooks/*`, `@/utils/*`
|
||||
|
||||
**Note**: Frontend tsconfig has `strict: false`, `noImplicitAny: false`, `strictNullChecks: false` (unlike backend which is strict).
|
||||
|
||||
### Brand Design
|
||||
|
||||
Colors: Navy `#10183A` (primary), Turquoise `#34CCCD` (accent), Green `#067224` (success), Gray `#F2F2F2`.
|
||||
Fonts: Manrope (headings), Montserrat (body).
|
||||
Landing page is in French.
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### Entity Pattern (Domain)
|
||||
Private constructor + static `create()` factory. Immutable — mutation methods return new instances. Some entities also have `fromPersistence()` for reconstitution and `toObject()` for serialization.
|
||||
```typescript
|
||||
export class Booking {
|
||||
private readonly props: BookingProps;
|
||||
static create(props: Omit<BookingProps, 'bookingNumber'>): Booking { ... }
|
||||
static create(props: Omit<BookingProps, 'bookingNumber' | 'status'>): Booking { ... }
|
||||
updateStatus(newStatus: BookingStatus): Booking { // Returns new instance
|
||||
return new Booking({ ...this.props, status: newStatus });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Repository Pattern
|
||||
- Interface in `domain/ports/out/booking.repository.ts`
|
||||
- Implementation in `infrastructure/persistence/typeorm/repositories/typeorm-booking.repository.ts`
|
||||
- Separate mappers for Domain ↔ ORM entity conversions
|
||||
### Value Object Pattern
|
||||
Immutable, self-validating via static `create()`. E.g. `Money` supports USD, EUR, GBP, CNY, JPY with arithmetic and formatting methods.
|
||||
|
||||
### Circuit Breaker (External APIs)
|
||||
- Library: `opossum`, Timeout: 5s
|
||||
- Used for carrier API calls (Maersk, MSC, CMA CGM)
|
||||
### Repository Pattern
|
||||
- Interface in `domain/ports/out/` with token constant (e.g. `BOOKING_REPOSITORY = 'BookingRepository'`)
|
||||
- Implementation in `infrastructure/persistence/typeorm/repositories/`
|
||||
- ORM entities: `infrastructure/persistence/typeorm/entities/*.orm-entity.ts`
|
||||
- Separate mapper classes (`infrastructure/persistence/typeorm/mappers/`) with static `toOrm()`, `toDomain()`, `toDomainMany()` methods
|
||||
|
||||
### Frontend API Client
|
||||
Custom Fetch wrapper in `src/lib/api/client.ts` — exports `get()`, `post()`, `patch()`, `del()`, `upload()`, `download()`. Auto-refreshes JWT on 401. Tokens stored in localStorage. Per-module files (auth.ts, bookings.ts, rates.ts, etc.) import from client.
|
||||
|
||||
### Application Decorators
|
||||
- `@Public()` — skip JWT auth
|
||||
- `@Roles()` — role-based access control
|
||||
- `@CurrentUser()` — inject authenticated user
|
||||
|
||||
### Carrier Connectors
|
||||
Five carrier connectors (Maersk, MSC, CMA CGM, Hapag-Lloyd, ONE) extending `base-carrier.connector.ts`, each with request/response mappers. Circuit breaker via `opossum` (5s timeout).
|
||||
|
||||
### Caching
|
||||
- Redis with 15-min TTL for rate quotes
|
||||
- Cache key format: `rate:{origin}:{destination}:{containerType}`
|
||||
Redis with 15-min TTL for rate quotes. Key format: `rate:{origin}:{destination}:{containerType}`.
|
||||
|
||||
## Business Rules
|
||||
|
||||
- Booking number format: `WCM-YYYY-XXXXXX`
|
||||
- Booking status flow: draft → confirmed → shipped → delivered
|
||||
- Rate quotes expire after 15 minutes
|
||||
- Multi-currency: USD, EUR
|
||||
- Multi-currency: USD, EUR, GBP, CNY, JPY
|
||||
- RBAC Roles: ADMIN, MANAGER, USER, VIEWER, CARRIER
|
||||
- JWT: access token 15min, refresh token 7d
|
||||
- Password hashing: Argon2
|
||||
|
||||
### Carrier Portal Workflow
|
||||
1. Admin creates CSV booking → assigns carrier
|
||||
@ -136,35 +200,33 @@ export class Booking {
|
||||
3. Carrier auto-login → accept/reject booking
|
||||
4. Activity logged in `carrier_activities` table
|
||||
|
||||
## Tech Stack
|
||||
|
||||
**Backend**: NestJS 10, TypeORM 0.3, PostgreSQL 15, Redis 7, Argon2, Pino, Sentry
|
||||
**Frontend**: Next.js 14, React 18, TanStack Query/Table, React Hook Form + Zod, Tailwind + shadcn/ui, Socket.IO
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Never import NestJS/TypeORM in domain layer
|
||||
- Never use `any` type (strict mode enabled)
|
||||
- Never use `DATABASE_SYNC=true` in production
|
||||
- Never modify applied migrations - create new ones
|
||||
- Always validate DTOs with `class-validator`
|
||||
- Always create mappers for Domain ↔ ORM conversions
|
||||
- Never use `any` type in backend (strict mode enabled)
|
||||
- Never modify applied migrations — create new ones
|
||||
- Always validate DTOs with `class-validator` decorators
|
||||
- Always create separate mappers for Domain ↔ ORM conversions
|
||||
- ORM entity files must match pattern `*.orm-entity.{ts,js}` (auto-discovered by data-source)
|
||||
- Migration files must be in `infrastructure/persistence/typeorm/migrations/`
|
||||
- Database synchronize is hard-coded to `false` — always use migrations
|
||||
|
||||
## Adding a New Feature
|
||||
|
||||
1. **Domain Entity** → `domain/entities/*.entity.ts` (pure TS, unit tests)
|
||||
2. **Value Objects** → `domain/value-objects/*.vo.ts` (immutable)
|
||||
3. **Port Interface** → `domain/ports/out/*.repository.ts`
|
||||
3. **Port Interface** → `domain/ports/out/*.repository.ts` (with token constant)
|
||||
4. **ORM Entity** → `infrastructure/persistence/typeorm/entities/*.orm-entity.ts`
|
||||
5. **Generate Migration** → `npm run migration:generate -- ...`
|
||||
5. **Migration** → `npm run migration:generate -- src/infrastructure/persistence/typeorm/migrations/MigrationName`
|
||||
6. **Repository Impl** → `infrastructure/persistence/typeorm/repositories/`
|
||||
7. **DTOs** → `application/dto/` (with class-validator decorators)
|
||||
8. **Controller** → `application/controllers/` (with Swagger decorators)
|
||||
9. **Module** → Register and import in `app.module.ts`
|
||||
7. **Mapper** → `infrastructure/persistence/typeorm/mappers/` (static toOrm/toDomain/toDomainMany)
|
||||
8. **DTOs** → `application/dto/` (with class-validator decorators)
|
||||
9. **Controller** → `application/controllers/` (with Swagger decorators)
|
||||
10. **Module** → Register and import in `app.module.ts`
|
||||
|
||||
## Documentation
|
||||
|
||||
- API Docs: http://localhost:4000/api/docs (Swagger)
|
||||
- Architecture: `docs/architecture.md`
|
||||
- API Docs: http://localhost:4000/api/docs (Swagger, when running)
|
||||
- Setup guide: `docs/installation/START-HERE.md`
|
||||
- Carrier Portal API: `apps/backend/docs/CARRIER_PORTAL_API.md`
|
||||
- Full docs index: `docs/README.md`
|
||||
|
||||
@ -201,6 +201,12 @@ export class CsvBookingResponseDto {
|
||||
})
|
||||
id: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Booking number (e.g. XPD-2026-W75VPT)',
|
||||
example: 'XPD-2026-W75VPT',
|
||||
})
|
||||
bookingNumber?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'User ID who created the booking',
|
||||
example: '987fcdeb-51a2-43e8-9c6d-8b9a1c2d3e4f',
|
||||
|
||||
@ -176,6 +176,7 @@ export class CsvBookingService {
|
||||
fileName: doc.fileName,
|
||||
})),
|
||||
confirmationToken,
|
||||
notes: dto.notes,
|
||||
});
|
||||
this.logger.log(`Email sent to carrier: ${dto.carrierEmail}`);
|
||||
} catch (error: any) {
|
||||
@ -921,6 +922,7 @@ export class CsvBookingService {
|
||||
|
||||
return {
|
||||
id: booking.id,
|
||||
bookingNumber: booking.bookingNumber,
|
||||
userId: booking.userId,
|
||||
organizationId: booking.organizationId,
|
||||
carrierName: booking.carrierName,
|
||||
|
||||
@ -79,7 +79,8 @@ export class CsvBooking {
|
||||
public readonly requestedAt: Date,
|
||||
public respondedAt?: Date,
|
||||
public notes?: string,
|
||||
public rejectionReason?: string
|
||||
public rejectionReason?: string,
|
||||
public readonly bookingNumber?: string
|
||||
) {
|
||||
this.validate();
|
||||
}
|
||||
@ -361,7 +362,8 @@ export class CsvBooking {
|
||||
requestedAt: Date,
|
||||
respondedAt?: Date,
|
||||
notes?: string,
|
||||
rejectionReason?: string
|
||||
rejectionReason?: string,
|
||||
bookingNumber?: string
|
||||
): CsvBooking {
|
||||
// Create instance without calling constructor validation
|
||||
const booking = Object.create(CsvBooking.prototype);
|
||||
@ -389,6 +391,7 @@ export class CsvBooking {
|
||||
booking.respondedAt = respondedAt;
|
||||
booking.notes = notes;
|
||||
booking.rejectionReason = rejectionReason;
|
||||
booking.bookingNumber = bookingNumber;
|
||||
|
||||
return booking;
|
||||
}
|
||||
|
||||
@ -102,6 +102,7 @@ export interface EmailPort {
|
||||
fileName: string;
|
||||
}>;
|
||||
confirmationToken: string;
|
||||
notes?: string;
|
||||
}
|
||||
): Promise<void>;
|
||||
|
||||
|
||||
@ -256,6 +256,7 @@ export class EmailAdapter implements EmailPort {
|
||||
fileName: string;
|
||||
}>;
|
||||
confirmationToken: string;
|
||||
notes?: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
// Use APP_URL (frontend) for accept/reject links
|
||||
|
||||
@ -277,6 +277,7 @@ export class EmailTemplates {
|
||||
type: string;
|
||||
fileName: string;
|
||||
}>;
|
||||
notes?: string;
|
||||
acceptUrl: string;
|
||||
rejectUrl: string;
|
||||
}): Promise<string> {
|
||||
@ -557,6 +558,14 @@ export class EmailTemplates {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{#if notes}}
|
||||
<!-- Notes -->
|
||||
<div style="background-color: #f0f9ff; border-left: 4px solid #0284c7; padding: 15px; margin: 20px 0; border-radius: 4px;">
|
||||
<p style="margin: 0 0 5px 0; font-weight: 700; color: #045a8d; font-size: 14px;">📝 Notes du client</p>
|
||||
<p style="margin: 0; font-size: 14px; color: #333; line-height: 1.6;">{{notes}}</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons">
|
||||
<p>Veuillez confirmer votre décision :</p>
|
||||
|
||||
@ -41,7 +41,8 @@ export class CsvBookingMapper {
|
||||
ormEntity.requestedAt,
|
||||
ormEntity.respondedAt,
|
||||
ormEntity.notes,
|
||||
ormEntity.rejectionReason
|
||||
ormEntity.rejectionReason,
|
||||
ormEntity.bookingNumber ?? undefined
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import {
|
||||
FileText,
|
||||
Download,
|
||||
@ -296,9 +297,9 @@ export default function CarrierDocumentsPage() {
|
||||
// Loading state
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-sky-50">
|
||||
<div className="min-h-screen flex items-center justify-center bg-brand-gray">
|
||||
<div className="bg-white p-8 rounded-xl shadow-lg max-w-md w-full text-center">
|
||||
<Loader2 className="w-16 h-16 text-sky-600 mx-auto mb-4 animate-spin" />
|
||||
<Loader2 className="w-16 h-16 text-brand-turquoise mx-auto mb-4 animate-spin" />
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">Chargement...</h1>
|
||||
<p className="text-gray-600">Veuillez patienter</p>
|
||||
</div>
|
||||
@ -309,14 +310,14 @@ export default function CarrierDocumentsPage() {
|
||||
// Error state
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-red-50 to-orange-50">
|
||||
<div className="min-h-screen flex items-center justify-center bg-brand-gray">
|
||||
<div className="bg-white p-8 rounded-xl shadow-lg max-w-md w-full text-center">
|
||||
<XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-4">Erreur</h1>
|
||||
<p className="text-gray-600 mb-6">{error}</p>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
className="w-full px-4 py-3 bg-gray-800 text-white rounded-lg hover:bg-gray-900 font-medium transition-colors"
|
||||
className="w-full px-4 py-3 bg-brand-navy text-white rounded-lg hover:bg-brand-navy/90 font-medium transition-colors"
|
||||
>
|
||||
Réessayer
|
||||
</button>
|
||||
@ -328,11 +329,11 @@ export default function CarrierDocumentsPage() {
|
||||
// Password form state
|
||||
if (requirements?.requiresPassword && !data) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-sky-50 to-cyan-50 p-4">
|
||||
<div className="min-h-screen flex items-center justify-center bg-brand-gray p-4">
|
||||
<div className="bg-white p-8 rounded-xl shadow-lg max-w-md w-full">
|
||||
<div className="text-center mb-6">
|
||||
<div className="mx-auto w-16 h-16 bg-sky-100 rounded-full flex items-center justify-center mb-4">
|
||||
<Lock className="w-8 h-8 text-sky-600" />
|
||||
<div className="mx-auto w-16 h-16 bg-brand-turquoise/10 rounded-full flex items-center justify-center mb-4">
|
||||
<Lock className="w-8 h-8 text-brand-turquoise" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">Accès sécurisé</h1>
|
||||
<p className="text-gray-600">
|
||||
@ -358,7 +359,7 @@ export default function CarrierDocumentsPage() {
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value.toUpperCase())}
|
||||
placeholder="Ex: A3B7K9"
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-sky-500 focus:border-sky-500 text-center text-xl tracking-widest font-mono uppercase"
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-turquoise focus:border-brand-turquoise text-center text-xl tracking-widest font-mono uppercase"
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
/>
|
||||
@ -381,7 +382,7 @@ export default function CarrierDocumentsPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={verifying}
|
||||
className="w-full px-4 py-3 bg-sky-600 text-white rounded-lg hover:bg-sky-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium transition-colors flex items-center justify-center gap-2"
|
||||
className="w-full px-4 py-3 bg-brand-navy text-white rounded-lg hover:bg-brand-navy/90 disabled:opacity-50 disabled:cursor-not-allowed font-medium transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
{verifying ? (
|
||||
<>
|
||||
@ -415,17 +416,22 @@ export default function CarrierDocumentsPage() {
|
||||
const { booking, documents } = data;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-sky-50 to-cyan-50">
|
||||
<div className="min-h-screen bg-brand-gray">
|
||||
{/* Header */}
|
||||
<header className="bg-white shadow-sm border-b">
|
||||
<div className="max-w-4xl mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Ship className="w-8 h-8 text-sky-600" />
|
||||
<span className="text-xl font-bold text-gray-900">Xpeditis</span>
|
||||
<Image
|
||||
src="/assets/logos/logo-black.svg"
|
||||
alt="Xpeditis"
|
||||
width={40}
|
||||
height={48}
|
||||
className="h-auto"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
className="text-sm text-sky-600 hover:text-sky-700 font-medium"
|
||||
className="text-sm text-brand-turquoise hover:text-brand-navy font-medium"
|
||||
>
|
||||
Actualiser
|
||||
</button>
|
||||
@ -435,14 +441,14 @@ export default function CarrierDocumentsPage() {
|
||||
<main className="max-w-4xl mx-auto px-4 py-8">
|
||||
{/* Booking Summary Card */}
|
||||
<div className="bg-white rounded-xl shadow-md overflow-hidden mb-6">
|
||||
<div className="bg-gradient-to-r from-sky-600 to-cyan-600 px-6 py-4">
|
||||
<div className="bg-gradient-to-r from-brand-navy to-brand-navy/80 px-6 py-4">
|
||||
<div className="flex items-center justify-center gap-4 text-white">
|
||||
<span className="text-2xl font-bold">{booking.origin}</span>
|
||||
<ArrowRight className="w-6 h-6" />
|
||||
<span className="text-2xl font-bold">{booking.destination}</span>
|
||||
</div>
|
||||
{booking.bookingNumber && (
|
||||
<p className="text-center text-sky-100 text-sm mt-1">
|
||||
<p className="text-center text-white/70 text-sm mt-1">
|
||||
N° {booking.bookingNumber}
|
||||
</p>
|
||||
)}
|
||||
@ -491,7 +497,7 @@ export default function CarrierDocumentsPage() {
|
||||
<div className="bg-white rounded-xl shadow-md overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-100">
|
||||
<h2 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
|
||||
<FileText className="w-5 h-5 text-sky-600" />
|
||||
<FileText className="w-5 h-5 text-brand-turquoise" />
|
||||
Documents ({documents.length})
|
||||
</h2>
|
||||
</div>
|
||||
@ -516,7 +522,7 @@ export default function CarrierDocumentsPage() {
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">{doc.fileName}</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-xs px-2 py-0.5 bg-sky-100 text-sky-700 rounded-full">
|
||||
<span className="text-xs px-2 py-0.5 bg-brand-turquoise/10 text-brand-navy rounded-full">
|
||||
{documentTypeLabels[doc.type] || doc.type}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">{formatFileSize(doc.size)}</span>
|
||||
@ -527,7 +533,7 @@ export default function CarrierDocumentsPage() {
|
||||
<button
|
||||
onClick={() => handleDownload(doc)}
|
||||
disabled={downloading === doc.id}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-sky-600 text-white rounded-lg hover:bg-sky-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
|
||||
className="flex items-center gap-2 px-4 py-2 bg-brand-navy text-white rounded-lg hover:bg-brand-navy/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
|
||||
>
|
||||
{downloading === doc.id ? (
|
||||
<>
|
||||
@ -554,7 +560,7 @@ export default function CarrierDocumentsPage() {
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="mt-auto py-6 text-center text-sm text-gray-500">
|
||||
<footer className="mt-auto py-6 text-center text-sm text-brand-navy/50">
|
||||
<p>© {new Date().getFullYear()} Xpeditis - Plateforme de fret maritime</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@ -50,6 +50,7 @@ function NewBookingPageContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [termsAccepted, setTermsAccepted] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [formData, setFormData] = useState<BookingForm>({
|
||||
@ -188,7 +189,7 @@ function NewBookingPageContent() {
|
||||
|
||||
const canProceedToStep2 = formData.carrierName && formData.origin && formData.destination;
|
||||
const canProceedToStep3 = formData.documents.length >= 1;
|
||||
const canSubmit = canProceedToStep3;
|
||||
const canSubmit = canProceedToStep3 && termsAccepted;
|
||||
|
||||
const formatPrice = (price: number, currency: string) => {
|
||||
return new Intl.NumberFormat('fr-FR', {
|
||||
@ -218,10 +219,10 @@ function NewBookingPageContent() {
|
||||
</div>
|
||||
|
||||
{/* Progress Steps */}
|
||||
<div className="mb-8">
|
||||
<div className="mb-8 bg-white rounded-lg shadow-md p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
{[1, 2, 3].map(step => (
|
||||
<div key={step} className="flex items-center flex-1">
|
||||
<div key={step} className={`flex items-center ${step < 3 ? 'flex-1' : ''}`}>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center font-semibold ${
|
||||
@ -608,8 +609,9 @@ function NewBookingPageContent() {
|
||||
<label className="flex items-start cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={termsAccepted}
|
||||
onChange={(e) => setTermsAccepted(e.target.checked)}
|
||||
className="mt-1 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
required
|
||||
/>
|
||||
<span className="ml-3 text-sm text-gray-700">
|
||||
Je confirme que les informations fournies sont exactes et que j'accepte les{' '}
|
||||
|
||||
@ -288,9 +288,12 @@ export default function BookingsListPage() {
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Date
|
||||
</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
N° Devis
|
||||
</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
N° Booking
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
@ -352,11 +355,14 @@ export default function BookingsListPage() {
|
||||
})
|
||||
: 'N/A'}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-left text-sm font-medium">
|
||||
{booking.type === 'csv'
|
||||
? `#${booking.bookingId || booking.id.slice(0, 8).toUpperCase()}`
|
||||
: booking.bookingNumber || `#${booking.id.slice(0, 8).toUpperCase()}`}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium text-brand-navy">
|
||||
{booking.bookingNumber || '-'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@ -19,7 +19,6 @@ import {
|
||||
FileText,
|
||||
Search,
|
||||
BookOpen,
|
||||
User,
|
||||
Building2,
|
||||
Users,
|
||||
LogOut,
|
||||
@ -36,7 +35,6 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
||||
{ name: 'Documents', href: '/dashboard/documents', icon: FileText },
|
||||
{ name: 'Suivi', href: '/dashboard/track-trace', icon: Search },
|
||||
{ name: 'Wiki Maritime', href: '/dashboard/wiki', icon: BookOpen },
|
||||
{ name: 'Mon Profil', href: '/dashboard/profile', icon: User },
|
||||
{ name: 'Organisation', href: '/dashboard/settings/organization', icon: Building2 },
|
||||
// ADMIN and MANAGER only navigation items
|
||||
...(user?.role === 'ADMIN' || user?.role === 'MANAGER' ? [
|
||||
@ -171,19 +169,10 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
||||
{/* Notifications */}
|
||||
<NotificationDropdown />
|
||||
|
||||
{/* User Role Badge */}
|
||||
{user?.role === 'ADMIN' ? (
|
||||
<Link
|
||||
href="/dashboard/admin/users"
|
||||
className="px-3 py-1 text-xs font-medium text-blue-800 bg-blue-100 rounded-full hover:bg-blue-200 transition-colors cursor-pointer"
|
||||
>
|
||||
{user.role}
|
||||
{/* User Initials */}
|
||||
<Link href="/dashboard/profile" className="w-9 h-9 bg-blue-600 rounded-full flex items-center justify-center text-white text-sm font-semibold hover:bg-blue-700 transition-colors">
|
||||
{user?.firstName?.[0]}{user?.lastName?.[0]}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="px-3 py-1 text-xs font-medium text-blue-800 bg-blue-100 rounded-full">
|
||||
{user?.role}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -9,6 +9,8 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getSubscriptionOverview } from '@/lib/api/subscriptions';
|
||||
import Link from 'next/link';
|
||||
import { UserPlus } from 'lucide-react';
|
||||
|
||||
export default function LicensesTab() {
|
||||
const [error, setError] = useState('');
|
||||
@ -110,10 +112,17 @@ export default function LicensesTab() {
|
||||
|
||||
{/* Active Licenses */}
|
||||
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
||||
<div className="px-6 py-4 border-b border-gray-200 bg-gray-50 flex items-center justify-between">
|
||||
<h3 className="text-lg font-medium text-gray-900">
|
||||
Licences actives ({activeLicenses.length})
|
||||
</h3>
|
||||
<Link
|
||||
href="/dashboard/settings/users"
|
||||
className="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<UserPlus className="w-4 h-4 mr-2" />
|
||||
Inviter un utilisateur
|
||||
</Link>
|
||||
</div>
|
||||
{activeLicenses.length === 0 ? (
|
||||
<div className="px-6 py-8 text-center text-gray-500">
|
||||
@ -335,9 +344,6 @@ export default function LicensesTab() {
|
||||
<li>
|
||||
Chaque utilisateur actif de votre organisation consomme une licence
|
||||
</li>
|
||||
<li>
|
||||
<strong>Les administrateurs (ADMIN) ont des licences illimitées</strong> et ne sont pas comptés dans le quota
|
||||
</li>
|
||||
<li>
|
||||
Les licences sont automatiquement assignées lors de l'ajout d'un
|
||||
utilisateur
|
||||
|
||||
Loading…
Reference in New Issue
Block a user