fix: resolve payment access, status labels, and document upload reliability
- Add PENDING_PAYMENT and PENDING_BANK_TRANSFER to document statuses translations (was displaying raw i18n key instead of human label) - Fix getStatusLabel/getStatusColor to handle all booking statuses - Fix select dropdown in Add Document modal to use getStatusLabel() instead of hardcoded PENDING/ACCEPTED binary - Add direct "Payer" button (orange) on PENDING_PAYMENT rows in both desktop table and mobile cards, linking to /dashboard/booking/[id]/pay - Update detail modal to show payment CTA for PENDING_PAYMENT bookings - Refactor file handling in document modals to use React state (addFiles, replaceFile) instead of reading from DOM ref — fixes upload reliability and shows selected filename feedback in the dropzone Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9950d96fd6
commit
b344650608
@ -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 } from 'lucide-react';
|
||||
import { Plus, Clock, Eye, X, CreditCard } from 'lucide-react';
|
||||
import ExportButton from '@/components/ExportButton';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { PageHeader } from '@/components/ui/PageHeader';
|
||||
@ -350,13 +350,24 @@ export default function BookingsListPage() {
|
||||
})
|
||||
: t('mobile.booking', { number: booking.bookingNumber || '-' })}
|
||||
</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 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>
|
||||
)}
|
||||
<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>
|
||||
))}
|
||||
@ -465,13 +476,24 @@ export default function BookingsListPage() {
|
||||
{booking.bookingNumber || '-'}
|
||||
</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>
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
))}
|
||||
@ -719,15 +741,16 @@ export default function BookingsListPage() {
|
||||
</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">
|
||||
<p className="text-sm text-orange-700 bg-orange-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"
|
||||
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"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t('detail.newBooking')}
|
||||
<CreditCard className="h-4 w-4" />
|
||||
{t('detail.payNow')}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -46,12 +46,14 @@ export default function UserDocumentsPage() {
|
||||
const [selectedBookingId, setSelectedBookingId] = useState<string | null>(null);
|
||||
const [uploadingFiles, setUploadingFiles] = useState(false);
|
||||
const [isDraggingAdd, setIsDraggingAdd] = useState(false);
|
||||
const [addFiles, setAddFiles] = useState<File[]>([]);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [showReplaceModal, setShowReplaceModal] = useState(false);
|
||||
const [documentToReplace, setDocumentToReplace] = useState<DocumentWithBooking | null>(null);
|
||||
const [replacingFile, setReplacingFile] = useState(false);
|
||||
const [isDraggingReplace, setIsDraggingReplace] = useState(false);
|
||||
const [replaceFile, setReplaceFile] = useState<File | null>(null);
|
||||
const replaceFileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [openDropdownId, setOpenDropdownId] = useState<string | null>(null);
|
||||
@ -189,6 +191,8 @@ export default function UserDocumentsPage() {
|
||||
|
||||
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',
|
||||
@ -198,8 +202,15 @@ export default function UserDocumentsPage() {
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
const key = status as 'PENDING' | 'ACCEPTED' | 'REJECTED' | 'CANCELLED';
|
||||
return t(`statuses.${key}` as any) || status;
|
||||
const labels: Record<string, string> = {
|
||||
PENDING_PAYMENT: t('statuses.PENDING_PAYMENT' as any),
|
||||
PENDING_BANK_TRANSFER: t('statuses.PENDING_BANK_TRANSFER' as any),
|
||||
PENDING: t('statuses.PENDING' as any),
|
||||
ACCEPTED: t('statuses.ACCEPTED' as any),
|
||||
REJECTED: t('statuses.REJECTED' as any),
|
||||
CANCELLED: t('statuses.CANCELLED' as any),
|
||||
};
|
||||
return labels[status] || status;
|
||||
};
|
||||
|
||||
const handleDownload = async (url: string, fileName: string) => {
|
||||
@ -251,11 +262,12 @@ export default function UserDocumentsPage() {
|
||||
const handleCloseModal = () => {
|
||||
setShowAddModal(false);
|
||||
setSelectedBookingId(null);
|
||||
setAddFiles([]);
|
||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||
};
|
||||
|
||||
const handleFileUpload = async () => {
|
||||
if (!selectedBookingId || !fileInputRef.current?.files?.length) {
|
||||
if (!selectedBookingId || addFiles.length === 0) {
|
||||
alert(t('addDocument.noBookingError'));
|
||||
return;
|
||||
}
|
||||
@ -263,10 +275,7 @@ export default function UserDocumentsPage() {
|
||||
setUploadingFiles(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
const files = fileInputRef.current.files;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
formData.append('documents', files[i]);
|
||||
}
|
||||
addFiles.forEach(file => formData.append('documents', file));
|
||||
|
||||
const token = localStorage.getItem('access_token');
|
||||
const response = await fetch(
|
||||
@ -274,7 +283,10 @@ export default function UserDocumentsPage() {
|
||||
{ method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: formData }
|
||||
);
|
||||
|
||||
if (!response.ok) throw new Error(t('addDocument.errorMessage'));
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || t('addDocument.errorMessage'));
|
||||
}
|
||||
|
||||
alert(t('addDocument.successMessage'));
|
||||
handleCloseModal();
|
||||
@ -308,11 +320,12 @@ export default function UserDocumentsPage() {
|
||||
const handleCloseReplaceModal = () => {
|
||||
setShowReplaceModal(false);
|
||||
setDocumentToReplace(null);
|
||||
setReplaceFile(null);
|
||||
if (replaceFileInputRef.current) replaceFileInputRef.current.value = '';
|
||||
};
|
||||
|
||||
const handleReplaceDocument = async () => {
|
||||
if (!documentToReplace || !replaceFileInputRef.current?.files?.length) {
|
||||
if (!documentToReplace || !replaceFile) {
|
||||
alert(t('replaceDocument.noFileError'));
|
||||
return;
|
||||
}
|
||||
@ -320,7 +333,7 @@ export default function UserDocumentsPage() {
|
||||
setReplacingFile(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('document', replaceFileInputRef.current.files[0]);
|
||||
formData.append('document', replaceFile);
|
||||
|
||||
const token = localStorage.getItem('access_token');
|
||||
const response = await fetch(
|
||||
@ -768,11 +781,7 @@ export default function UserDocumentsPage() {
|
||||
<option value="">{t('addDocument.selectBookingPlaceholder')}</option>
|
||||
{bookingsAvailableForDocuments.map(booking => (
|
||||
<option key={booking.id} value={booking.id}>
|
||||
{getQuoteNumber(booking)} - {booking.origin} → {booking.destination} (
|
||||
{booking.status === 'PENDING'
|
||||
? t('statuses.PENDING')
|
||||
: t('statuses.ACCEPTED')}
|
||||
)
|
||||
{getQuoteNumber(booking)} - {booking.origin} → {booking.destination} ({getStatusLabel(booking.status)})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@ -795,11 +804,8 @@ export default function UserDocumentsPage() {
|
||||
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;
|
||||
}
|
||||
const dropped = Array.from(e.dataTransfer.files);
|
||||
if (dropped.length > 0) setAddFiles(dropped);
|
||||
}}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
@ -816,11 +822,19 @@ export default function UserDocumentsPage() {
|
||||
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>
|
||||
{addFiles.length > 0 ? (
|
||||
<p className="text-sm font-medium text-blue-700">
|
||||
{addFiles.length === 1
|
||||
? addFiles[0].name
|
||||
: `${addFiles.length} fichiers sélectionnés`}
|
||||
</p>
|
||||
) : (
|
||||
<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
|
||||
ref={fileInputRef}
|
||||
@ -828,9 +842,9 @@ export default function UserDocumentsPage() {
|
||||
multiple
|
||||
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.gif"
|
||||
className="hidden"
|
||||
onChange={() => {
|
||||
// Force re-render to show selected file names
|
||||
setIsDraggingAdd(false);
|
||||
onChange={e => {
|
||||
const selected = Array.from(e.target.files || []);
|
||||
if (selected.length > 0) setAddFiles(selected);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -843,7 +857,7 @@ export default function UserDocumentsPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleFileUpload}
|
||||
disabled={uploadingFiles || !selectedBookingId}
|
||||
disabled={uploadingFiles || !selectedBookingId || addFiles.length === 0}
|
||||
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{uploadingFiles ? (
|
||||
@ -947,11 +961,8 @@ export default function UserDocumentsPage() {
|
||||
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;
|
||||
}
|
||||
const dropped = e.dataTransfer.files[0];
|
||||
if (dropped) setReplaceFile(dropped);
|
||||
}}
|
||||
onClick={() => replaceFileInputRef.current?.click()}
|
||||
>
|
||||
@ -968,18 +979,25 @@ export default function UserDocumentsPage() {
|
||||
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>
|
||||
{replaceFile ? (
|
||||
<p className="text-sm font-medium text-blue-700">{replaceFile.name}</p>
|
||||
) : (
|
||||
<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
|
||||
ref={replaceFileInputRef}
|
||||
type="file"
|
||||
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.gif"
|
||||
className="hidden"
|
||||
onChange={() => setIsDraggingReplace(false)}
|
||||
onChange={e => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) setReplaceFile(file);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -430,7 +430,8 @@
|
||||
"actions": "Actions"
|
||||
},
|
||||
"actions": {
|
||||
"view": "View"
|
||||
"view": "View",
|
||||
"pay": "Pay"
|
||||
},
|
||||
"detail": {
|
||||
"title": "Booking",
|
||||
@ -440,7 +441,8 @@
|
||||
"weight": "Weight / Volume",
|
||||
"pickupDate": "Requested pickup date",
|
||||
"price": "Price",
|
||||
"pendingPaymentNotice": "This booking is awaiting payment. Contact an administrator or create a new booking.",
|
||||
"pendingPaymentNotice": "This booking is awaiting commission payment. Click below to complete the payment.",
|
||||
"payNow": "Complete payment",
|
||||
"newBooking": "New booking",
|
||||
"close": "Close"
|
||||
},
|
||||
@ -1453,6 +1455,8 @@
|
||||
"actions": "Actions"
|
||||
},
|
||||
"statuses": {
|
||||
"PENDING_PAYMENT": "Payment pending",
|
||||
"PENDING_BANK_TRANSFER": "Bank transfer pending",
|
||||
"PENDING": "Pending",
|
||||
"ACCEPTED": "Accepted",
|
||||
"REJECTED": "Rejected",
|
||||
|
||||
@ -430,7 +430,8 @@
|
||||
"actions": "Actions"
|
||||
},
|
||||
"actions": {
|
||||
"view": "Voir"
|
||||
"view": "Voir",
|
||||
"pay": "Payer"
|
||||
},
|
||||
"detail": {
|
||||
"title": "Réservation",
|
||||
@ -440,7 +441,8 @@
|
||||
"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.",
|
||||
"pendingPaymentNotice": "Cette réservation est en attente du paiement de la commission. Cliquez ci-dessous pour finaliser le paiement.",
|
||||
"payNow": "Finaliser le paiement",
|
||||
"newBooking": "Nouvelle réservation",
|
||||
"close": "Fermer"
|
||||
},
|
||||
@ -1453,6 +1455,8 @@
|
||||
"actions": "Actions"
|
||||
},
|
||||
"statuses": {
|
||||
"PENDING_PAYMENT": "Paiement en attente",
|
||||
"PENDING_BANK_TRANSFER": "Virement en attente",
|
||||
"PENDING": "En attente",
|
||||
"ACCEPTED": "Accepté",
|
||||
"REJECTED": "Refusé",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user