feat(bookings): Phase 2 edition avant paiement + filtre + ergonomie
- Edition d'une reservation avant paiement: page /booking/:id/edit (volume, poids, palettes, notes + gestion documents), endpoint PATCH /csv-bookings/:id/details (owner + PENDING_PAYMENT), methode domaine editDetails. Acces "Modifier" depuis la liste, la modale detail et la page de paiement. - Filtre "A finaliser" (PENDING_PAYMENT + PENDING_BANK_TRANSFER) + libelles et couleurs de tous les statuts. - Tableau des reservations: 6 colonnes + colonne Actions collante a droite (plus de scroll horizontal pour atteindre Payer/Voir). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
19b96517d2
commit
7234343e20
@ -47,6 +47,7 @@ import {
|
||||
CsvBookingResponseDto,
|
||||
CsvBookingListResponseDto,
|
||||
CsvBookingStatsDto,
|
||||
UpdateCsvBookingDetailsDto,
|
||||
} from '../dto/csv-booking.dto';
|
||||
|
||||
/**
|
||||
@ -560,6 +561,37 @@ export class CsvBookingsController {
|
||||
return await this.csvBookingService.cancelBooking(id, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update booking cargo details before payment
|
||||
*
|
||||
* PATCH /api/v1/csv-bookings/:id/details
|
||||
*/
|
||||
@Patch(':id/details')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({
|
||||
summary: 'Update booking details before payment',
|
||||
description:
|
||||
'Edit cargo characteristics (volume, weight, pallets, notes) of a booking awaiting payment. Only the owner can edit, and only while the booking is PENDING_PAYMENT.',
|
||||
})
|
||||
@ApiParam({ name: 'id', description: 'Booking ID (UUID)' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Booking details updated successfully',
|
||||
type: CsvBookingResponseDto,
|
||||
})
|
||||
@ApiResponse({ status: 400, description: 'Booking cannot be edited (invalid status or values)' })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
@ApiResponse({ status: 404, description: 'Booking not found' })
|
||||
async updateBookingDetails(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateCsvBookingDetailsDto,
|
||||
@Request() req: any
|
||||
): Promise<CsvBookingResponseDto> {
|
||||
const userId = req.user.id;
|
||||
return await this.csvBookingService.updateBookingDetails(id, userId, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add documents to an existing booking
|
||||
*
|
||||
|
||||
@ -173,6 +173,38 @@ export class CreateCsvBookingDto {
|
||||
// Not included in DTO validation but processed separately
|
||||
}
|
||||
|
||||
/**
|
||||
* Update CSV Booking Details DTO
|
||||
*
|
||||
* Editable cargo characteristics before payment. Carrier, route and price are
|
||||
* derived from the selected rate and cannot be changed here.
|
||||
*/
|
||||
export class UpdateCsvBookingDetailsDto {
|
||||
@ApiPropertyOptional({ description: 'Total volume in cubic meters (CBM)', example: 12.5 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0.01)
|
||||
volumeCBM?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Total weight in kilograms', example: 1500 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0.01)
|
||||
weightKG?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Number of pallets', example: 5 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
palletCount?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Additional notes', example: 'Please handle with care' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(2000)
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Document DTO for response
|
||||
*/
|
||||
|
||||
@ -31,6 +31,7 @@ import {
|
||||
CsvBookingDocumentDto,
|
||||
CsvBookingListResponseDto,
|
||||
CsvBookingStatsDto,
|
||||
UpdateCsvBookingDetailsDto,
|
||||
} from '../dto/csv-booking.dto';
|
||||
import { CarrierDocumentsResponseDto } from '../dto/carrier-documents.dto';
|
||||
import { SubscriptionService } from './subscription.service';
|
||||
@ -1011,6 +1012,47 @@ export class CsvBookingService {
|
||||
return this.toResponseDto(updatedBooking);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the cargo details of a booking before payment (user action).
|
||||
*
|
||||
* Only the owner can edit, and only while the booking is PENDING_PAYMENT.
|
||||
*/
|
||||
async updateBookingDetails(
|
||||
id: string,
|
||||
userId: string,
|
||||
dto: UpdateCsvBookingDetailsDto
|
||||
): Promise<CsvBookingResponseDto> {
|
||||
this.logger.log(`Updating booking details ${id} by user ${userId}`);
|
||||
|
||||
const booking = await this.csvBookingRepository.findById(id);
|
||||
|
||||
if (!booking) {
|
||||
throw new NotFoundException('Booking not found');
|
||||
}
|
||||
|
||||
// Verify user owns this booking
|
||||
if (booking.userId !== userId) {
|
||||
throw new NotFoundException('Booking not found');
|
||||
}
|
||||
|
||||
// Apply the changes (domain logic validates status + values)
|
||||
try {
|
||||
booking.editDetails({
|
||||
volumeCBM: dto.volumeCBM,
|
||||
weightKG: dto.weightKG,
|
||||
palletCount: dto.palletCount,
|
||||
notes: dto.notes,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new BadRequestException(err instanceof Error ? err.message : 'Invalid booking details');
|
||||
}
|
||||
|
||||
const updatedBooking = await this.csvBookingRepository.update(booking);
|
||||
this.logger.log(`Booking ${id} details updated`);
|
||||
|
||||
return this.toResponseDto(updatedBooking);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bookings for a user (paginated)
|
||||
*/
|
||||
|
||||
@ -67,9 +67,10 @@ export class CsvBooking {
|
||||
public readonly carrierEmail: string,
|
||||
public readonly origin: PortCode,
|
||||
public readonly destination: PortCode,
|
||||
public readonly volumeCBM: number,
|
||||
public readonly weightKG: number,
|
||||
public readonly palletCount: number,
|
||||
// Cargo characteristics — editable before payment (see editDetails).
|
||||
public volumeCBM: number,
|
||||
public weightKG: number,
|
||||
public palletCount: number,
|
||||
public readonly priceUSD: number,
|
||||
public readonly priceEUR: number,
|
||||
public readonly primaryCurrency: string,
|
||||
@ -291,6 +292,53 @@ export class CsvBooking {
|
||||
this.respondedAt = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the cargo details of a booking before it is paid.
|
||||
*
|
||||
* Only allowed while the booking is awaiting payment (PENDING_PAYMENT), i.e.
|
||||
* before it is sent to the carrier. Carrier, route and price derive from the
|
||||
* selected rate and are not editable here.
|
||||
*
|
||||
* @throws Error if the booking is not in PENDING_PAYMENT status or values are invalid
|
||||
*/
|
||||
editDetails(details: {
|
||||
volumeCBM?: number;
|
||||
weightKG?: number;
|
||||
palletCount?: number;
|
||||
notes?: string;
|
||||
}): void {
|
||||
if (this.status !== CsvBookingStatus.PENDING_PAYMENT) {
|
||||
throw new Error(
|
||||
`Cannot edit booking with status ${this.status}. Only PENDING_PAYMENT bookings can be edited.`
|
||||
);
|
||||
}
|
||||
|
||||
if (details.volumeCBM !== undefined) {
|
||||
if (details.volumeCBM <= 0) {
|
||||
throw new Error('Volume must be positive');
|
||||
}
|
||||
this.volumeCBM = details.volumeCBM;
|
||||
}
|
||||
|
||||
if (details.weightKG !== undefined) {
|
||||
if (details.weightKG <= 0) {
|
||||
throw new Error('Weight must be positive');
|
||||
}
|
||||
this.weightKG = details.weightKG;
|
||||
}
|
||||
|
||||
if (details.palletCount !== undefined) {
|
||||
if (details.palletCount < 0) {
|
||||
throw new Error('Pallet count cannot be negative');
|
||||
}
|
||||
this.palletCount = details.palletCount;
|
||||
}
|
||||
|
||||
if (details.notes !== undefined) {
|
||||
this.notes = details.notes;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if booking has expired (7 days without response)
|
||||
*
|
||||
|
||||
416
apps/frontend/app/[locale]/dashboard/booking/[id]/edit/page.tsx
Normal file
416
apps/frontend/app/[locale]/dashboard/booking/[id]/edit/page.tsx
Normal file
@ -0,0 +1,416 @@
|
||||
/**
|
||||
* Edit Booking Page (before payment)
|
||||
*
|
||||
* Lets the owner adjust cargo details (volume, weight, pallets, notes) and manage
|
||||
* documents of a booking that is still awaiting payment (PENDING_PAYMENT), then
|
||||
* continue to the payment page. Carrier, route and price derive from the selected
|
||||
* rate and are shown read-only.
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Loader2,
|
||||
AlertTriangle,
|
||||
Save,
|
||||
CreditCard,
|
||||
FileText,
|
||||
Trash2,
|
||||
Upload,
|
||||
Package,
|
||||
} from 'lucide-react';
|
||||
import { getCsvBooking, updateCsvBookingDetails } from '@/lib/api/bookings';
|
||||
import { upload, del } from '@/lib/api/client';
|
||||
|
||||
interface BookingDoc {
|
||||
id?: string;
|
||||
type: string;
|
||||
fileName: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface BookingData {
|
||||
id: string;
|
||||
bookingNumber?: string;
|
||||
carrierName: string;
|
||||
origin: string;
|
||||
destination: string;
|
||||
volumeCBM: number;
|
||||
weightKG: number;
|
||||
palletCount: number;
|
||||
containerType: string;
|
||||
status: string;
|
||||
notes?: string;
|
||||
documents: BookingDoc[];
|
||||
}
|
||||
|
||||
export default function EditBookingPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const bookingId = params.id as string;
|
||||
|
||||
const [booking, setBooking] = useState<BookingData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
|
||||
const [volumeCBM, setVolumeCBM] = useState('');
|
||||
const [weightKG, setWeightKG] = useState('');
|
||||
const [palletCount, setPalletCount] = useState('');
|
||||
const [notes, setNotes] = useState('');
|
||||
|
||||
const [uploadingDocs, setUploadingDocs] = useState(false);
|
||||
const [deletingDocId, setDeletingDocId] = useState<string | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const applyBooking = (data: BookingData) => {
|
||||
setBooking(data);
|
||||
setVolumeCBM(String(data.volumeCBM ?? ''));
|
||||
setWeightKG(String(data.weightKG ?? ''));
|
||||
setPalletCount(String(data.palletCount ?? ''));
|
||||
setNotes(data.notes ?? '');
|
||||
};
|
||||
|
||||
const loadBooking = async () => {
|
||||
try {
|
||||
const data = (await getCsvBooking(bookingId)) as any as BookingData;
|
||||
if (data.status !== 'PENDING_PAYMENT') {
|
||||
// Only bookings awaiting payment can be edited.
|
||||
router.replace('/dashboard/bookings');
|
||||
return;
|
||||
}
|
||||
applyBooking(data);
|
||||
} catch (err) {
|
||||
setError('Impossible de charger la réservation');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (bookingId) loadBooking();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [bookingId]);
|
||||
|
||||
const persistDetails = async (): Promise<boolean> => {
|
||||
setError(null);
|
||||
const vol = parseFloat(volumeCBM);
|
||||
const wgt = parseFloat(weightKG);
|
||||
const plt = parseInt(palletCount, 10);
|
||||
|
||||
if (!Number.isFinite(vol) || vol <= 0) {
|
||||
setError('Le volume doit être un nombre positif.');
|
||||
return false;
|
||||
}
|
||||
if (!Number.isFinite(wgt) || wgt <= 0) {
|
||||
setError('Le poids doit être un nombre positif.');
|
||||
return false;
|
||||
}
|
||||
if (!Number.isFinite(plt) || plt < 0) {
|
||||
setError('Le nombre de palettes est invalide.');
|
||||
return false;
|
||||
}
|
||||
|
||||
const updated = (await updateCsvBookingDetails(bookingId, {
|
||||
volumeCBM: vol,
|
||||
weightKG: wgt,
|
||||
palletCount: plt,
|
||||
notes: notes.trim() || undefined,
|
||||
})) as any as BookingData;
|
||||
applyBooking(updated);
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
setSuccess(null);
|
||||
try {
|
||||
const ok = await persistDetails();
|
||||
if (ok) setSuccess('Modifications enregistrées.');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Erreur lors de l'enregistrement");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleContinueToPayment = async () => {
|
||||
setSaving(true);
|
||||
setSuccess(null);
|
||||
try {
|
||||
const ok = await persistDetails();
|
||||
if (ok) router.push(`/dashboard/booking/${bookingId}/pay`);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Erreur lors de l'enregistrement");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddDocuments = async (files: FileList | null) => {
|
||||
if (!files || files.length === 0) return;
|
||||
setUploadingDocs(true);
|
||||
setError(null);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
Array.from(files).forEach(file => formData.append('documents', file));
|
||||
await upload(`/api/v1/csv-bookings/${bookingId}/documents`, formData);
|
||||
await loadBooking();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Erreur lors de l'ajout des documents");
|
||||
} finally {
|
||||
setUploadingDocs(false);
|
||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteDocument = async (docId?: string) => {
|
||||
if (!docId) return;
|
||||
if (!confirm('Supprimer ce document ?')) return;
|
||||
setDeletingDocId(docId);
|
||||
setError(null);
|
||||
try {
|
||||
await del(`/api/v1/csv-bookings/${bookingId}/documents/${docId}`);
|
||||
await loadBooking();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Erreur lors de la suppression du document');
|
||||
} finally {
|
||||
setDeletingDocId(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-blue-50">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-blue-600" />
|
||||
<span className="text-gray-600">Chargement...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!booking) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-blue-50">
|
||||
<div className="bg-white rounded-xl shadow-md p-8 max-w-md">
|
||||
<AlertTriangle className="h-12 w-12 text-red-500 mx-auto mb-4" />
|
||||
<p className="text-center text-gray-700">{error || 'Réservation introuvable'}</p>
|
||||
<button
|
||||
onClick={() => router.push('/dashboard/bookings')}
|
||||
className="mt-4 w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
Retour aux réservations
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50 py-10 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="mb-6 flex items-center text-blue-600 hover:text-blue-800 font-medium"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Retour
|
||||
</button>
|
||||
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-1">Modifier la réservation</h1>
|
||||
<p className="text-gray-500 mb-8">
|
||||
Corrigez les informations de votre réservation avant de procéder au paiement.
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4 flex items-start space-x-3">
|
||||
<AlertTriangle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-red-700 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
{success && (
|
||||
<div className="mb-6 bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<p className="text-green-700 text-sm">{success}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Read-only rate summary */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-5 mb-6">
|
||||
<h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
|
||||
Réservation
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-gray-500">Transporteur</p>
|
||||
<p className="font-semibold text-gray-900">{booking.carrierName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500">Trajet</p>
|
||||
<p className="font-semibold text-gray-900">
|
||||
{booking.origin} → {booking.destination}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500">Type</p>
|
||||
<p className="font-semibold text-gray-900">{booking.containerType}</p>
|
||||
</div>
|
||||
{booking.bookingNumber && (
|
||||
<div>
|
||||
<p className="text-gray-500">Numéro</p>
|
||||
<p className="font-semibold text-gray-900">{booking.bookingNumber}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-3">
|
||||
Le transporteur, le trajet et le prix proviennent du tarif sélectionné et ne sont pas
|
||||
modifiables ici.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Editable cargo details */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-5 mb-6 space-y-4">
|
||||
<h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wide flex items-center gap-2">
|
||||
<Package className="h-4 w-4" />
|
||||
Caractéristiques de la marchandise
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Volume (CBM)</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
value={volumeCBM}
|
||||
onChange={e => setVolumeCBM(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Poids (kg)</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
value={weightKG}
|
||||
onChange={e => setWeightKG(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Palettes</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
value={palletCount}
|
||||
onChange={e => setPalletCount(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Notes</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={notes}
|
||||
onChange={e => setNotes(e.target.value)}
|
||||
placeholder="Instructions particulières..."
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Documents */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-5 mb-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wide flex items-center gap-2">
|
||||
<FileText className="h-4 w-4" />
|
||||
Documents
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploadingDocs}
|
||||
className="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-blue-700 bg-blue-50 hover:bg-blue-100 rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
{uploadingDocs ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Upload className="h-4 w-4" />
|
||||
)}
|
||||
Ajouter
|
||||
</button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={e => handleAddDocuments(e.target.files)}
|
||||
/>
|
||||
</div>
|
||||
{booking.documents.length === 0 ? (
|
||||
<p className="text-sm text-gray-400">Aucun document.</p>
|
||||
) : (
|
||||
<ul className="divide-y divide-gray-100">
|
||||
{booking.documents.map((doc, idx) => (
|
||||
<li
|
||||
key={doc.id || `${doc.fileName}-${idx}`}
|
||||
className="flex items-center justify-between py-2.5"
|
||||
>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<FileText className="h-4 w-4 text-gray-400 flex-shrink-0" />
|
||||
<span className="text-sm text-gray-800 truncate">{doc.fileName}</span>
|
||||
</div>
|
||||
{doc.id && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDeleteDocument(doc.id)}
|
||||
disabled={deletingDocId === doc.id}
|
||||
className="p-1.5 text-gray-400 hover:text-red-600 transition-colors disabled:opacity-50"
|
||||
title="Supprimer"
|
||||
>
|
||||
{deletingDocId === doc.id ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="flex-1 py-3 bg-white border border-gray-300 text-gray-800 rounded-lg font-semibold hover:bg-gray-50 disabled:opacity-50 flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
{saving ? <Loader2 className="h-5 w-5 animate-spin" /> : <Save className="h-5 w-5" />}
|
||||
Enregistrer les modifications
|
||||
</button>
|
||||
<button
|
||||
onClick={handleContinueToPayment}
|
||||
disabled={saving}
|
||||
className="flex-1 py-3 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 disabled:opacity-50 flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
{saving ? (
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
) : (
|
||||
<CreditCard className="h-5 w-5" />
|
||||
)}
|
||||
Continuer vers le paiement
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -223,9 +223,16 @@ export default function PayCommissionPage() {
|
||||
</button>
|
||||
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-1">Paiement du forfait booking</h1>
|
||||
<p className="text-gray-500 mb-8">
|
||||
<p className="text-gray-500 mb-4">
|
||||
Finalisez votre booking en réglant le forfait par booking
|
||||
</p>
|
||||
<button
|
||||
onClick={() => router.push(`/dashboard/booking/${bookingId}/edit`)}
|
||||
className="mb-8 inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-blue-700 bg-blue-50 hover:bg-blue-100 rounded-lg transition-colors"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Modifier la réservation avant de payer
|
||||
</button>
|
||||
|
||||
{/* SIRET/SIREN non vérifié — bannière de demande */}
|
||||
{siretNotVerified && (
|
||||
|
||||
@ -4,7 +4,7 @@ import { useState, useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { listCsvBookings } from '@/lib/api';
|
||||
import { Link } from '@/i18n/navigation';
|
||||
import { Plus, Clock, Eye, X, CreditCard } from 'lucide-react';
|
||||
import { Plus, Clock, Eye, X, CreditCard, Pencil } from 'lucide-react';
|
||||
import ExportButton from '@/components/ExportButton';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { PageHeader } from '@/components/ui/PageHeader';
|
||||
@ -12,6 +12,9 @@ import { useTranslations, useLocale } from 'next-intl';
|
||||
|
||||
type SearchType = 'pallets' | 'weight' | 'route' | 'status' | 'date' | 'quote';
|
||||
|
||||
// Statuses considered "not finalized": booking started but not yet paid.
|
||||
const TO_FINALIZE_STATUSES = ['PENDING_PAYMENT', 'PENDING_BANK_TRANSFER'];
|
||||
|
||||
export default function BookingsListPage() {
|
||||
const t = useTranslations('dashboard.bookingsList');
|
||||
const locale = useLocale();
|
||||
@ -50,7 +53,13 @@ export default function BookingsListPage() {
|
||||
let filtered = bookings;
|
||||
|
||||
if (statusFilter) {
|
||||
filtered = filtered.filter((booking: any) => booking.status === statusFilter);
|
||||
if (statusFilter === 'to_finalize') {
|
||||
filtered = filtered.filter((booking: any) =>
|
||||
TO_FINALIZE_STATUSES.includes(booking.status)
|
||||
);
|
||||
} else {
|
||||
filtered = filtered.filter((booking: any) => booking.status === statusFilter);
|
||||
}
|
||||
}
|
||||
|
||||
if (searchTerm.trim()) {
|
||||
@ -101,9 +110,13 @@ export default function BookingsListPage() {
|
||||
|
||||
const statusOptions = [
|
||||
{ value: '', label: t('statusFilter.all') },
|
||||
{ value: 'to_finalize', label: t('statusFilter.toFinalize') },
|
||||
{ value: 'PENDING_PAYMENT', label: t('status.pendingPayment') },
|
||||
{ value: 'PENDING_BANK_TRANSFER', label: t('status.pendingBankTransfer') },
|
||||
{ value: 'PENDING', label: t('status.pending') },
|
||||
{ value: 'ACCEPTED', label: t('status.accepted') },
|
||||
{ value: 'REJECTED', label: t('status.rejected') },
|
||||
{ value: 'CANCELLED', label: t('status.cancelled') },
|
||||
];
|
||||
|
||||
const searchTypeOptions: { value: SearchType; label: string }[] = [
|
||||
@ -129,18 +142,24 @@ export default function BookingsListPage() {
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
PENDING_PAYMENT: 'bg-orange-100 text-orange-800',
|
||||
PENDING_BANK_TRANSFER: 'bg-blue-100 text-blue-800',
|
||||
PENDING: 'bg-yellow-100 text-yellow-800',
|
||||
ACCEPTED: 'bg-green-100 text-green-800',
|
||||
REJECTED: 'bg-red-100 text-red-800',
|
||||
CANCELLED: 'bg-gray-100 text-gray-800',
|
||||
};
|
||||
return colors[status] || 'bg-gray-100 text-gray-800';
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
PENDING_PAYMENT: t('status.pendingPayment'),
|
||||
PENDING_BANK_TRANSFER: t('status.pendingBankTransfer'),
|
||||
PENDING: t('status.pending'),
|
||||
ACCEPTED: t('status.accepted'),
|
||||
REJECTED: t('status.rejected'),
|
||||
CANCELLED: t('status.cancelled'),
|
||||
};
|
||||
return map[status] || status;
|
||||
};
|
||||
@ -352,13 +371,22 @@ export default function BookingsListPage() {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{booking.status === 'PENDING_PAYMENT' && (
|
||||
<Link
|
||||
href={`/dashboard/booking/${booking.id}/pay`}
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium text-white bg-orange-500 hover:bg-orange-600 rounded-lg transition-colors"
|
||||
>
|
||||
<CreditCard className="h-3.5 w-3.5" />
|
||||
{t('actions.pay')}
|
||||
</Link>
|
||||
<>
|
||||
<Link
|
||||
href={`/dashboard/booking/${booking.id}/edit`}
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
{t('actions.edit')}
|
||||
</Link>
|
||||
<Link
|
||||
href={`/dashboard/booking/${booking.id}/pay`}
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium text-white bg-orange-500 hover:bg-orange-600 rounded-lg transition-colors"
|
||||
>
|
||||
<CreditCard className="h-3.5 w-3.5" />
|
||||
{t('actions.pay')}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setSelectedBooking(booking)}
|
||||
@ -378,125 +406,123 @@ export default function BookingsListPage() {
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
{t('columns.palletsPackages')}
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
{t('columns.weight')}
|
||||
{t('columns.reference')}
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
{t('columns.route')}
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
{t('columns.cargo')}
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
{t('columns.status')}
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
{t('columns.date')}
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
{t('columns.quoteNumber')}
|
||||
</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
{t('columns.bookingNumber')}
|
||||
</th>
|
||||
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th className="sticky right-0 z-10 bg-gray-50 px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
{t('columns.actions')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{paginatedBookings.map((booking: any) => (
|
||||
<tr key={`${booking.type}-${booking.id}`} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{booking.type === 'csv'
|
||||
? t('units.palletsCount', { count: booking.palletCount })
|
||||
: t('units.containersCount', {
|
||||
count: booking.containers?.length || 0,
|
||||
})}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{booking.type === 'csv' ? 'LCL' : booking.containerType || 'FCL'}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{booking.type === 'csv'
|
||||
? t('units.kg', { value: booking.weightKG })
|
||||
: booking.totalWeight
|
||||
? t('units.kg', { value: booking.totalWeight })
|
||||
: 'N/A'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{booking.type === 'csv'
|
||||
? t('units.cbm', { value: booking.volumeCBM })
|
||||
: booking.totalVolume
|
||||
? t('units.cbm', { value: booking.totalVolume })
|
||||
: ''}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="text-sm text-gray-900">
|
||||
{booking.type === 'csv'
|
||||
? `${booking.origin} → ${booking.destination}`
|
||||
: booking.route || 'N/A'}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{booking.type === 'csv'
|
||||
? `${booking.carrierName}`
|
||||
: booking.carrier || ''}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
className={`px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(
|
||||
booking.status
|
||||
)}`}
|
||||
>
|
||||
{getStatusLabel(booking.status)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{booking.createdAt || booking.requestedAt
|
||||
? new Date(booking.createdAt || booking.requestedAt).toLocaleDateString(
|
||||
dateLocale,
|
||||
{
|
||||
{paginatedBookings.map((booking: any) => {
|
||||
const reference =
|
||||
booking.type === 'csv'
|
||||
? `#${booking.bookingId || booking.id.slice(0, 8).toUpperCase()}`
|
||||
: booking.bookingNumber || `#${booking.id.slice(0, 8).toUpperCase()}`;
|
||||
return (
|
||||
<tr key={`${booking.type}-${booking.id}`} className="group hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-semibold text-brand-navy">
|
||||
{booking.bookingNumber || reference}
|
||||
</div>
|
||||
{booking.bookingNumber && (
|
||||
<div className="text-xs text-gray-400">{reference}</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="text-sm text-gray-900">
|
||||
{booking.type === 'csv'
|
||||
? `${booking.origin} → ${booking.destination}`
|
||||
: booking.route || 'N/A'}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{booking.type === 'csv'
|
||||
? `${booking.carrierName}`
|
||||
: booking.carrier || ''}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-900">
|
||||
{booking.type === 'csv'
|
||||
? t('units.palletsCount', { count: booking.palletCount })
|
||||
: t('units.containersCount', {
|
||||
count: booking.containers?.length || 0,
|
||||
})}
|
||||
{' · '}
|
||||
{booking.type === 'csv' ? 'LCL' : booking.containerType || 'FCL'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{booking.type === 'csv'
|
||||
? `${t('units.kg', { value: booking.weightKG })} · ${t('units.cbm', { value: booking.volumeCBM })}`
|
||||
: booking.totalWeight
|
||||
? t('units.kg', { value: booking.totalWeight })
|
||||
: ''}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
className={`px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(
|
||||
booking.status
|
||||
)}`}
|
||||
>
|
||||
{getStatusLabel(booking.status)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{booking.createdAt || booking.requestedAt
|
||||
? new Date(
|
||||
booking.createdAt || booking.requestedAt
|
||||
).toLocaleDateString(dateLocale, {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
}
|
||||
)
|
||||
: 'N/A'}
|
||||
</td>
|
||||
<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>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{booking.status === 'PENDING_PAYMENT' && (
|
||||
<Link
|
||||
href={`/dashboard/booking/${booking.id}/pay`}
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium text-white bg-orange-500 hover:bg-orange-600 rounded-lg transition-colors"
|
||||
})
|
||||
: 'N/A'}
|
||||
</td>
|
||||
<td className="sticky right-0 z-10 bg-white group-hover:bg-gray-50 px-6 py-4 whitespace-nowrap text-right shadow-[-8px_0_8px_-8px_rgba(0,0,0,0.08)]">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{booking.status === 'PENDING_PAYMENT' && (
|
||||
<>
|
||||
<Link
|
||||
href={`/dashboard/booking/${booking.id}/edit`}
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
{t('actions.edit')}
|
||||
</Link>
|
||||
<Link
|
||||
href={`/dashboard/booking/${booking.id}/pay`}
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium text-white bg-orange-500 hover:bg-orange-600 rounded-lg transition-colors"
|
||||
>
|
||||
<CreditCard className="h-3.5 w-3.5" />
|
||||
{t('actions.pay')}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setSelectedBooking(booking)}
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium text-blue-700 bg-blue-50 hover:bg-blue-100 rounded-lg transition-colors"
|
||||
>
|
||||
<CreditCard className="h-3.5 w-3.5" />
|
||||
{t('actions.pay')}
|
||||
</Link>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setSelectedBooking(booking)}
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium text-blue-700 bg-blue-50 hover:bg-blue-100 rounded-lg transition-colors"
|
||||
>
|
||||
<Eye className="h-3.5 w-3.5" />
|
||||
{t('actions.view')}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
<Eye className="h-3.5 w-3.5" />
|
||||
{t('actions.view')}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -744,14 +770,24 @@ export default function BookingsListPage() {
|
||||
<p className="text-sm text-orange-700 bg-orange-50 rounded-lg px-3 py-2">
|
||||
{t('detail.pendingPaymentNotice')}
|
||||
</p>
|
||||
<Link
|
||||
href={`/dashboard/booking/${selectedBooking.id}/pay`}
|
||||
onClick={() => setSelectedBooking(null)}
|
||||
className="mt-3 inline-flex items-center gap-2 px-4 py-2 bg-orange-500 text-white text-sm font-medium rounded-lg hover:bg-orange-600 transition-colors"
|
||||
>
|
||||
<CreditCard className="h-4 w-4" />
|
||||
{t('detail.payNow')}
|
||||
</Link>
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<Link
|
||||
href={`/dashboard/booking/${selectedBooking.id}/edit`}
|
||||
onClick={() => setSelectedBooking(null)}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 text-sm font-medium rounded-lg hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
{t('actions.edit')}
|
||||
</Link>
|
||||
<Link
|
||||
href={`/dashboard/booking/${selectedBooking.id}/pay`}
|
||||
onClick={() => setSelectedBooking(null)}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-orange-500 text-white text-sm font-medium rounded-lg hover:bg-orange-600 transition-colors"
|
||||
>
|
||||
<CreditCard className="h-4 w-4" />
|
||||
{t('detail.payNow')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -434,12 +434,16 @@
|
||||
},
|
||||
"statusFilter": {
|
||||
"label": "Status",
|
||||
"all": "All statuses"
|
||||
"all": "All statuses",
|
||||
"toFinalize": "To finalize"
|
||||
},
|
||||
"status": {
|
||||
"pendingPayment": "Payment pending",
|
||||
"pendingBankTransfer": "Transfer pending",
|
||||
"pending": "Pending",
|
||||
"accepted": "Accepted",
|
||||
"rejected": "Rejected"
|
||||
"rejected": "Rejected",
|
||||
"cancelled": "Cancelled"
|
||||
},
|
||||
"loading": "Loading bookings...",
|
||||
"search": "Search",
|
||||
@ -447,6 +451,8 @@
|
||||
"palletsPackages": "Pallets/Packages",
|
||||
"weight": "Weight",
|
||||
"route": "Route",
|
||||
"cargo": "Cargo",
|
||||
"reference": "Reference",
|
||||
"status": "Status",
|
||||
"date": "Date",
|
||||
"quoteNumber": "Quote No.",
|
||||
@ -455,7 +461,8 @@
|
||||
},
|
||||
"actions": {
|
||||
"view": "View",
|
||||
"pay": "Pay"
|
||||
"pay": "Pay",
|
||||
"edit": "Edit"
|
||||
},
|
||||
"detail": {
|
||||
"title": "Booking",
|
||||
|
||||
@ -434,12 +434,16 @@
|
||||
},
|
||||
"statusFilter": {
|
||||
"label": "Statut",
|
||||
"all": "Tous les statuts"
|
||||
"all": "Tous les statuts",
|
||||
"toFinalize": "À finaliser"
|
||||
},
|
||||
"status": {
|
||||
"pendingPayment": "Paiement en attente",
|
||||
"pendingBankTransfer": "Virement en attente",
|
||||
"pending": "En attente",
|
||||
"accepted": "Accepté",
|
||||
"rejected": "Refusé"
|
||||
"rejected": "Refusé",
|
||||
"cancelled": "Annulé"
|
||||
},
|
||||
"loading": "Chargement des réservations...",
|
||||
"search": "Rechercher",
|
||||
@ -447,6 +451,8 @@
|
||||
"palletsPackages": "Palettes/Colis",
|
||||
"weight": "Poids",
|
||||
"route": "Route",
|
||||
"cargo": "Marchandise",
|
||||
"reference": "Référence",
|
||||
"status": "Statut",
|
||||
"date": "Date",
|
||||
"quoteNumber": "N° Devis",
|
||||
@ -455,7 +461,8 @@
|
||||
},
|
||||
"actions": {
|
||||
"view": "Voir",
|
||||
"pay": "Payer"
|
||||
"pay": "Payer",
|
||||
"edit": "Modifier"
|
||||
},
|
||||
"detail": {
|
||||
"title": "Réservation",
|
||||
|
||||
@ -263,6 +263,24 @@ export async function cancelCsvBooking(id: string): Promise<SuccessResponse> {
|
||||
return patch<SuccessResponse>(`/api/v1/csv-bookings/${id}/cancel`, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update cargo details of a booking awaiting payment
|
||||
* PATCH /api/v1/csv-bookings/:id/details
|
||||
*/
|
||||
export interface UpdateCsvBookingDetailsRequest {
|
||||
volumeCBM?: number;
|
||||
weightKG?: number;
|
||||
palletCount?: number;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export async function updateCsvBookingDetails(
|
||||
id: string,
|
||||
data: UpdateCsvBookingDetailsRequest
|
||||
): Promise<CsvBookingResponse> {
|
||||
return patch<CsvBookingResponse>(`/api/v1/csv-bookings/${id}/details`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a CSV booking (public endpoint, no auth required)
|
||||
* POST /api/v1/csv-bookings/:token/accept
|
||||
|
||||
Loading…
Reference in New Issue
Block a user