fix: correct 5 reported bugs across booking and document management
- SIRET/SIREN admin: controller applied siren but skipped siret — add missing updateSiret() call - Document upload on validated booking: backend blocked PENDING_BANK_TRANSFER; frontend filter excluded it — both now allow all non-terminal statuses - Accent encoding in filenames: Multer stores originalname as latin1; re-decode to utf8 before storing and displaying - Drag & drop in document modals: replace bare input with styled dropzone supporting click and native drag-and-drop in both Add and Replace modals - Resume existing booking: add Actions column with View button and booking detail modal to the bookings list page Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
87bddcfb95
commit
9950d96fd6
@ -252,6 +252,10 @@ export class OrganizationsController {
|
|||||||
organization.updateSiren(dto.siren);
|
organization.updateSiren(dto.siren);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dto.siret) {
|
||||||
|
organization.updateSiret(dto.siret);
|
||||||
|
}
|
||||||
|
|
||||||
if (dto.eori) {
|
if (dto.eori) {
|
||||||
organization.updateEori(dto.eori);
|
organization.updateEori(dto.eori);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -989,7 +989,9 @@ export class CsvBookingService {
|
|||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const documentId = uuidv4();
|
const documentId = uuidv4();
|
||||||
const fileKey = `csv-bookings/${bookingId}/${documentId}-${file.originalname}`;
|
// Multer decodes originalname as latin1; re-encode to utf8 to preserve accented characters
|
||||||
|
const fileName = Buffer.from(file.originalname, 'latin1').toString('utf8');
|
||||||
|
const fileKey = `csv-bookings/${bookingId}/${documentId}-${fileName}`;
|
||||||
|
|
||||||
// Upload to S3
|
// Upload to S3
|
||||||
const uploadResult = await this.storageAdapter.upload({
|
const uploadResult = await this.storageAdapter.upload({
|
||||||
@ -1000,12 +1002,12 @@ export class CsvBookingService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Determine document type from filename or default to OTHER
|
// Determine document type from filename or default to OTHER
|
||||||
const documentType = this.inferDocumentType(file.originalname);
|
const documentType = this.inferDocumentType(fileName);
|
||||||
|
|
||||||
const document = new CsvBookingDocumentImpl(
|
const document = new CsvBookingDocumentImpl(
|
||||||
documentId,
|
documentId,
|
||||||
documentType,
|
documentType,
|
||||||
file.originalname,
|
fileName,
|
||||||
uploadResult.url,
|
uploadResult.url,
|
||||||
file.mimetype,
|
file.mimetype,
|
||||||
file.size,
|
file.size,
|
||||||
@ -1064,9 +1066,10 @@ export class CsvBookingService {
|
|||||||
throw new NotFoundException(`Booking with ID ${bookingId} not found`);
|
throw new NotFoundException(`Booking with ID ${bookingId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow adding documents to PENDING_PAYMENT, PENDING, or ACCEPTED bookings
|
// Allow adding documents to PENDING_PAYMENT, PENDING_BANK_TRANSFER, PENDING, or ACCEPTED bookings
|
||||||
if (
|
if (
|
||||||
booking.status !== CsvBookingStatus.PENDING_PAYMENT &&
|
booking.status !== CsvBookingStatus.PENDING_PAYMENT &&
|
||||||
|
booking.status !== CsvBookingStatus.PENDING_BANK_TRANSFER &&
|
||||||
booking.status !== CsvBookingStatus.PENDING &&
|
booking.status !== CsvBookingStatus.PENDING &&
|
||||||
booking.status !== CsvBookingStatus.ACCEPTED
|
booking.status !== CsvBookingStatus.ACCEPTED
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { listCsvBookings } from '@/lib/api';
|
import { listCsvBookings } from '@/lib/api';
|
||||||
import { Link } from '@/i18n/navigation';
|
import { Link } from '@/i18n/navigation';
|
||||||
import { Plus, Clock } from 'lucide-react';
|
import { Plus, Clock, Eye, X } from 'lucide-react';
|
||||||
import ExportButton from '@/components/ExportButton';
|
import ExportButton from '@/components/ExportButton';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { PageHeader } from '@/components/ui/PageHeader';
|
import { PageHeader } from '@/components/ui/PageHeader';
|
||||||
@ -22,6 +22,7 @@ export default function BookingsListPage() {
|
|||||||
const [statusFilter, setStatusFilter] = useState('');
|
const [statusFilter, setStatusFilter] = useState('');
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [showTransferBanner, setShowTransferBanner] = useState(false);
|
const [showTransferBanner, setShowTransferBanner] = useState(false);
|
||||||
|
const [selectedBooking, setSelectedBooking] = useState<any | null>(null);
|
||||||
const ITEMS_PER_PAGE = 20;
|
const ITEMS_PER_PAGE = 20;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -341,6 +342,7 @@ export default function BookingsListPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<div className="text-xs text-gray-400">
|
<div className="text-xs text-gray-400">
|
||||||
{booking.type === 'csv'
|
{booking.type === 'csv'
|
||||||
? t('mobile.ref', {
|
? t('mobile.ref', {
|
||||||
@ -348,6 +350,14 @@ export default function BookingsListPage() {
|
|||||||
})
|
})
|
||||||
: t('mobile.booking', { number: booking.bookingNumber || '-' })}
|
: t('mobile.booking', { number: booking.bookingNumber || '-' })}
|
||||||
</div>
|
</div>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -377,6 +387,9 @@ export default function BookingsListPage() {
|
|||||||
<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-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
{t('columns.bookingNumber')}
|
{t('columns.bookingNumber')}
|
||||||
</th>
|
</th>
|
||||||
|
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
{t('columns.actions')}
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
@ -451,6 +464,15 @@ export default function BookingsListPage() {
|
|||||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium text-brand-navy">
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium text-brand-navy">
|
||||||
{booking.bookingNumber || '-'}
|
{booking.bookingNumber || '-'}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||||
|
<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>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -601,6 +623,127 @@ export default function BookingsListPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Booking Detail Modal */}
|
||||||
|
{selectedBooking && (
|
||||||
|
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||||
|
<div className="flex items-center justify-center min-h-screen px-4 py-8">
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
|
||||||
|
onClick={() => setSelectedBooking(null)}
|
||||||
|
/>
|
||||||
|
<div className="relative bg-white rounded-xl shadow-xl w-full max-w-2xl">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900">
|
||||||
|
{t('detail.title')}
|
||||||
|
{selectedBooking.bookingId
|
||||||
|
? ` #${selectedBooking.bookingId}`
|
||||||
|
: ` #${selectedBooking.id?.slice(0, 8).toUpperCase()}`}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedBooking(null)}
|
||||||
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="px-6 py-4 space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`px-2.5 py-1 rounded-full text-xs font-semibold ${getStatusColor(selectedBooking.status)}`}>
|
||||||
|
{getStatusLabel(selectedBooking.status)}
|
||||||
|
</span>
|
||||||
|
{selectedBooking.carrierName && (
|
||||||
|
<span className="text-sm text-gray-500">{selectedBooking.carrierName}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-gray-400 uppercase tracking-wide">{t('detail.route')}</p>
|
||||||
|
<p className="mt-1 text-gray-900">
|
||||||
|
{selectedBooking.origin || selectedBooking.originCity || 'N/A'}
|
||||||
|
{' → '}
|
||||||
|
{selectedBooking.destination || selectedBooking.destinationCity || 'N/A'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-gray-400 uppercase tracking-wide">{t('detail.date')}</p>
|
||||||
|
<p className="mt-1 text-gray-900">
|
||||||
|
{selectedBooking.createdAt || selectedBooking.requestedAt
|
||||||
|
? new Date(
|
||||||
|
selectedBooking.createdAt || selectedBooking.requestedAt
|
||||||
|
).toLocaleDateString(dateLocale, {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
})
|
||||||
|
: 'N/A'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-gray-400 uppercase tracking-wide">{t('detail.cargo')}</p>
|
||||||
|
<p className="mt-1 text-gray-900">
|
||||||
|
{selectedBooking.palletCount
|
||||||
|
? t('units.palletsCount', { count: selectedBooking.palletCount })
|
||||||
|
: '-'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-gray-400 uppercase tracking-wide">{t('detail.weight')}</p>
|
||||||
|
<p className="mt-1 text-gray-900">
|
||||||
|
{selectedBooking.weightKG ? t('units.kg', { value: selectedBooking.weightKG }) : 'N/A'}
|
||||||
|
{selectedBooking.volumeCBM ? ` / ${t('units.cbm', { value: selectedBooking.volumeCBM })}` : ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{selectedBooking.requestedPickupDate && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-gray-400 uppercase tracking-wide">{t('detail.pickupDate')}</p>
|
||||||
|
<p className="mt-1 text-gray-900">
|
||||||
|
{new Date(selectedBooking.requestedPickupDate).toLocaleDateString(dateLocale, {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(selectedBooking.priceEUR || selectedBooking.priceUSD) && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-gray-400 uppercase tracking-wide">{t('detail.price')}</p>
|
||||||
|
<p className="mt-1 font-semibold text-gray-900">
|
||||||
|
{selectedBooking.priceEUR
|
||||||
|
? `${selectedBooking.priceEUR} €`
|
||||||
|
: `${selectedBooking.priceUSD} $`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{selectedBooking.status === 'PENDING_PAYMENT' && (
|
||||||
|
<div className="mt-4 pt-4 border-t border-gray-100">
|
||||||
|
<p className="text-sm text-amber-700 bg-amber-50 rounded-lg px-3 py-2">
|
||||||
|
{t('detail.pendingPaymentNotice')}
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/dashboard/search-advanced"
|
||||||
|
className="mt-3 inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
{t('detail.newBooking')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="px-6 py-4 border-t border-gray-200 flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedBooking(null)}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
{t('detail.close')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,11 +45,13 @@ export default function UserDocumentsPage() {
|
|||||||
const [showAddModal, setShowAddModal] = useState(false);
|
const [showAddModal, setShowAddModal] = useState(false);
|
||||||
const [selectedBookingId, setSelectedBookingId] = useState<string | null>(null);
|
const [selectedBookingId, setSelectedBookingId] = useState<string | null>(null);
|
||||||
const [uploadingFiles, setUploadingFiles] = useState(false);
|
const [uploadingFiles, setUploadingFiles] = useState(false);
|
||||||
|
const [isDraggingAdd, setIsDraggingAdd] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const [showReplaceModal, setShowReplaceModal] = useState(false);
|
const [showReplaceModal, setShowReplaceModal] = useState(false);
|
||||||
const [documentToReplace, setDocumentToReplace] = useState<DocumentWithBooking | null>(null);
|
const [documentToReplace, setDocumentToReplace] = useState<DocumentWithBooking | null>(null);
|
||||||
const [replacingFile, setReplacingFile] = useState(false);
|
const [replacingFile, setReplacingFile] = useState(false);
|
||||||
|
const [isDraggingReplace, setIsDraggingReplace] = useState(false);
|
||||||
const replaceFileInputRef = useRef<HTMLInputElement>(null);
|
const replaceFileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const [openDropdownId, setOpenDropdownId] = useState<string | null>(null);
|
const [openDropdownId, setOpenDropdownId] = useState<string | null>(null);
|
||||||
@ -235,7 +237,11 @@ export default function UserDocumentsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const bookingsAvailableForDocuments = bookings.filter(
|
const bookingsAvailableForDocuments = bookings.filter(
|
||||||
b => b.status === 'PENDING' || b.status === 'ACCEPTED'
|
b =>
|
||||||
|
b.status === 'PENDING' ||
|
||||||
|
b.status === 'ACCEPTED' ||
|
||||||
|
b.status === 'PENDING_PAYMENT' ||
|
||||||
|
b.status === 'PENDING_BANK_TRANSFER'
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleAddDocumentClick = () => {
|
const handleAddDocumentClick = () => {
|
||||||
@ -775,16 +781,59 @@ export default function UserDocumentsPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
{t('addDocument.filesToAdd')}
|
{t('addDocument.filesToAdd')}
|
||||||
</label>
|
</label>
|
||||||
|
<div
|
||||||
|
className={`relative border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors ${
|
||||||
|
isDraggingAdd
|
||||||
|
? 'border-blue-500 bg-blue-50'
|
||||||
|
: 'border-gray-300 hover:border-blue-400 hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
onDragOver={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDraggingAdd(true);
|
||||||
|
}}
|
||||||
|
onDragLeave={() => setIsDraggingAdd(false)}
|
||||||
|
onDrop={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDraggingAdd(false);
|
||||||
|
if (fileInputRef.current && e.dataTransfer.files.length > 0) {
|
||||||
|
const dt = new DataTransfer();
|
||||||
|
Array.from(e.dataTransfer.files).forEach(f => dt.items.add(f));
|
||||||
|
fileInputRef.current.files = dt.files;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="mx-auto h-10 w-10 text-gray-400 mb-2"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
{isDraggingAdd
|
||||||
|
? t('addDocument.dropHere')
|
||||||
|
: t('addDocument.dragOrClick')}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400 mt-1">{t('addDocument.acceptedFormats')}</p>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
multiple
|
multiple
|
||||||
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.gif"
|
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.gif"
|
||||||
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
|
className="hidden"
|
||||||
|
onChange={() => {
|
||||||
|
// Force re-render to show selected file names
|
||||||
|
setIsDraggingAdd(false);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
</div>
|
||||||
{t('addDocument.acceptedFormats')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -884,15 +933,55 @@ export default function UserDocumentsPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
{t('replaceDocument.newFile')}
|
{t('replaceDocument.newFile')}
|
||||||
</label>
|
</label>
|
||||||
|
<div
|
||||||
|
className={`relative border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors ${
|
||||||
|
isDraggingReplace
|
||||||
|
? 'border-blue-500 bg-blue-50'
|
||||||
|
: 'border-gray-300 hover:border-blue-400 hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
onDragOver={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDraggingReplace(true);
|
||||||
|
}}
|
||||||
|
onDragLeave={() => setIsDraggingReplace(false)}
|
||||||
|
onDrop={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDraggingReplace(false);
|
||||||
|
if (replaceFileInputRef.current && e.dataTransfer.files.length > 0) {
|
||||||
|
const dt = new DataTransfer();
|
||||||
|
dt.items.add(e.dataTransfer.files[0]);
|
||||||
|
replaceFileInputRef.current.files = dt.files;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={() => replaceFileInputRef.current?.click()}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="mx-auto h-10 w-10 text-gray-400 mb-2"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
{isDraggingReplace
|
||||||
|
? t('replaceDocument.dropHere')
|
||||||
|
: t('replaceDocument.dragOrClick')}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400 mt-1">{t('replaceDocument.acceptedFormats')}</p>
|
||||||
<input
|
<input
|
||||||
ref={replaceFileInputRef}
|
ref={replaceFileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.gif"
|
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.gif"
|
||||||
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
|
className="hidden"
|
||||||
|
onChange={() => setIsDraggingReplace(false)}
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
</div>
|
||||||
{t('replaceDocument.acceptedFormats')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -426,7 +426,23 @@
|
|||||||
"status": "Status",
|
"status": "Status",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"quoteNumber": "Quote No.",
|
"quoteNumber": "Quote No.",
|
||||||
"bookingNumber": "Booking No."
|
"bookingNumber": "Booking No.",
|
||||||
|
"actions": "Actions"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"view": "View"
|
||||||
|
},
|
||||||
|
"detail": {
|
||||||
|
"title": "Booking",
|
||||||
|
"route": "Route",
|
||||||
|
"date": "Creation date",
|
||||||
|
"cargo": "Cargo",
|
||||||
|
"weight": "Weight / Volume",
|
||||||
|
"pickupDate": "Requested pickup date",
|
||||||
|
"price": "Price",
|
||||||
|
"pendingPaymentNotice": "This booking is awaiting payment. Contact an administrator or create a new booking.",
|
||||||
|
"newBooking": "New booking",
|
||||||
|
"close": "Close"
|
||||||
},
|
},
|
||||||
"mobile": {
|
"mobile": {
|
||||||
"pallets": "Pallets",
|
"pallets": "Pallets",
|
||||||
@ -1472,6 +1488,8 @@
|
|||||||
"selectBookingPlaceholder": "-- Choose a booking --",
|
"selectBookingPlaceholder": "-- Choose a booking --",
|
||||||
"filesToAdd": "Files to add",
|
"filesToAdd": "Files to add",
|
||||||
"acceptedFormats": "Accepted formats: PDF, Word, Excel, Images (max 10 files)",
|
"acceptedFormats": "Accepted formats: PDF, Word, Excel, Images (max 10 files)",
|
||||||
|
"dragOrClick": "Drag & drop your files here or click to select",
|
||||||
|
"dropHere": "Drop your files here",
|
||||||
"uploading": "Uploading...",
|
"uploading": "Uploading...",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
@ -1485,6 +1503,8 @@
|
|||||||
"booking": "Booking",
|
"booking": "Booking",
|
||||||
"newFile": "New file",
|
"newFile": "New file",
|
||||||
"acceptedFormats": "Accepted formats: PDF, Word, Excel, Images",
|
"acceptedFormats": "Accepted formats: PDF, Word, Excel, Images",
|
||||||
|
"dragOrClick": "Drag & drop your file here or click to select",
|
||||||
|
"dropHere": "Drop your file here",
|
||||||
"replacing": "Replacing...",
|
"replacing": "Replacing...",
|
||||||
"replace": "Replace",
|
"replace": "Replace",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
|||||||
@ -426,7 +426,23 @@
|
|||||||
"status": "Statut",
|
"status": "Statut",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"quoteNumber": "N° Devis",
|
"quoteNumber": "N° Devis",
|
||||||
"bookingNumber": "N° Booking"
|
"bookingNumber": "N° Booking",
|
||||||
|
"actions": "Actions"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"view": "Voir"
|
||||||
|
},
|
||||||
|
"detail": {
|
||||||
|
"title": "Réservation",
|
||||||
|
"route": "Route",
|
||||||
|
"date": "Date de création",
|
||||||
|
"cargo": "Marchandise",
|
||||||
|
"weight": "Poids / Volume",
|
||||||
|
"pickupDate": "Date d'enlèvement souhaitée",
|
||||||
|
"price": "Prix",
|
||||||
|
"pendingPaymentNotice": "Cette réservation est en attente de paiement. Contactez l'administrateur ou créez une nouvelle réservation.",
|
||||||
|
"newBooking": "Nouvelle réservation",
|
||||||
|
"close": "Fermer"
|
||||||
},
|
},
|
||||||
"mobile": {
|
"mobile": {
|
||||||
"pallets": "Palettes",
|
"pallets": "Palettes",
|
||||||
@ -1472,6 +1488,8 @@
|
|||||||
"selectBookingPlaceholder": "-- Choisir une réservation --",
|
"selectBookingPlaceholder": "-- Choisir une réservation --",
|
||||||
"filesToAdd": "Fichiers à ajouter",
|
"filesToAdd": "Fichiers à ajouter",
|
||||||
"acceptedFormats": "Formats acceptés: PDF, Word, Excel, Images (max 10 fichiers)",
|
"acceptedFormats": "Formats acceptés: PDF, Word, Excel, Images (max 10 fichiers)",
|
||||||
|
"dragOrClick": "Glissez-déposez vos fichiers ici ou cliquez pour sélectionner",
|
||||||
|
"dropHere": "Déposez vos fichiers ici",
|
||||||
"uploading": "Envoi en cours...",
|
"uploading": "Envoi en cours...",
|
||||||
"add": "Ajouter",
|
"add": "Ajouter",
|
||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
@ -1485,6 +1503,8 @@
|
|||||||
"booking": "Réservation",
|
"booking": "Réservation",
|
||||||
"newFile": "Nouveau fichier",
|
"newFile": "Nouveau fichier",
|
||||||
"acceptedFormats": "Formats acceptés: PDF, Word, Excel, Images",
|
"acceptedFormats": "Formats acceptés: PDF, Word, Excel, Images",
|
||||||
|
"dragOrClick": "Glissez-déposez votre fichier ici ou cliquez pour sélectionner",
|
||||||
|
"dropHere": "Déposez votre fichier ici",
|
||||||
"replacing": "Remplacement en cours...",
|
"replacing": "Remplacement en cours...",
|
||||||
"replace": "Remplacer",
|
"replace": "Remplacer",
|
||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
|
|||||||
@ -51,7 +51,7 @@ export interface CsvBookingResponse {
|
|||||||
primaryCurrency: string;
|
primaryCurrency: string;
|
||||||
transitDays: number;
|
transitDays: number;
|
||||||
containerType: string;
|
containerType: string;
|
||||||
status: 'PENDING_PAYMENT' | 'PENDING' | 'ACCEPTED' | 'REJECTED' | 'CANCELLED';
|
status: 'PENDING_PAYMENT' | 'PENDING_BANK_TRANSFER' | 'PENDING' | 'ACCEPTED' | 'REJECTED' | 'CANCELLED';
|
||||||
documents: Array<{
|
documents: Array<{
|
||||||
type: string;
|
type: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user