fix: unauthorized on document upload and drag-drop in booking creation

- Replace localStorage.getItem('access_token') with getAuthToken() which
  also checks sessionStorage — fixes Unauthorized error for users logged
  in without 'remember me'
- Add real drag-and-drop support to booking creation page (step 2):
  onDragOver/onDragLeave/onDrop handlers on the dropzone container,
  isDragging state with visual feedback (blue border + text change),
  click-to-browse still works via hidden input ref

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
David 2026-06-07 17:42:27 +02:00
parent b344650608
commit 1da5570e1c
2 changed files with 33 additions and 11 deletions

View File

@ -6,7 +6,7 @@
'use client'; 'use client';
import { useState, useEffect, Suspense } from 'react'; import { useState, useEffect, useRef, Suspense } from 'react';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { ClipboardList, Mail, AlertTriangle } from 'lucide-react'; import { ClipboardList, Mail, AlertTriangle } from 'lucide-react';
import type { CsvRateSearchResult } from '@/types/rates'; import type { CsvRateSearchResult } from '@/types/rates';
@ -55,6 +55,8 @@ function NewBookingPageContent() {
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [termsAccepted, setTermsAccepted] = useState(false); const [termsAccepted, setTermsAccepted] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isDragging, setIsDragging] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const [formData, setFormData] = useState<BookingForm>({ const [formData, setFormData] = useState<BookingForm>({
carrierName: '', carrierName: '',
@ -419,8 +421,30 @@ function NewBookingPageContent() {
{/* File Upload */} {/* File Upload */}
<div className="space-y-4 mb-8"> <div className="space-y-4 mb-8">
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-blue-400 transition-colors"> <div
className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors cursor-pointer ${
isDragging
? 'border-blue-500 bg-blue-50'
: 'border-gray-300 hover:border-blue-400 hover:bg-gray-50'
}`}
onDragOver={e => {
e.preventDefault();
setIsDragging(true);
}}
onDragLeave={e => {
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
setIsDragging(false);
}
}}
onDrop={e => {
e.preventDefault();
setIsDragging(false);
handleFileChange(e.dataTransfer.files);
}}
onClick={() => fileInputRef.current?.click()}
>
<input <input
ref={fileInputRef}
type="file" type="file"
id="file-upload" id="file-upload"
multiple multiple
@ -428,12 +452,9 @@ function NewBookingPageContent() {
onChange={e => handleFileChange(e.target.files)} onChange={e => handleFileChange(e.target.files)}
className="hidden" className="hidden"
/> />
<label <div className="flex flex-col items-center">
htmlFor="file-upload"
className="cursor-pointer flex flex-col items-center"
>
<svg <svg
className="w-16 h-16 text-gray-400 mb-4" className={`w-16 h-16 mb-4 transition-colors ${isDragging ? 'text-blue-500' : 'text-gray-400'}`}
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -446,10 +467,10 @@ function NewBookingPageContent() {
/> />
</svg> </svg>
<p className="text-lg font-semibold text-gray-700 mb-2"> <p className="text-lg font-semibold text-gray-700 mb-2">
Cliquez pour sélectionner des fichiers {isDragging ? 'Déposez vos fichiers ici' : 'Cliquez pour sélectionner des fichiers'}
</p> </p>
<p className="text-sm text-gray-500">ou glissez-déposez vos documents ici</p> <p className="text-sm text-gray-500">ou glissez-déposez vos documents ici</p>
</label> </div>
</div> </div>
{/* Document Type Suggestions */} {/* Document Type Suggestions */}

View File

@ -3,6 +3,7 @@
import { useState, useEffect, useCallback, useRef } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
import { useTranslations, useLocale } from 'next-intl'; import { useTranslations, useLocale } from 'next-intl';
import { listCsvBookings, CsvBookingResponse } from '@/lib/api/bookings'; import { listCsvBookings, CsvBookingResponse } from '@/lib/api/bookings';
import { getAuthToken } from '@/lib/api/client';
import { FileText, Image as ImageIcon, FileEdit, FileSpreadsheet, Paperclip } from 'lucide-react'; import { FileText, Image as ImageIcon, FileEdit, FileSpreadsheet, Paperclip } from 'lucide-react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import ExportButton from '@/components/ExportButton'; import ExportButton from '@/components/ExportButton';
@ -277,7 +278,7 @@ export default function UserDocumentsPage() {
const formData = new FormData(); const formData = new FormData();
addFiles.forEach(file => formData.append('documents', file)); addFiles.forEach(file => formData.append('documents', file));
const token = localStorage.getItem('access_token'); const token = getAuthToken();
const response = await fetch( const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/csv-bookings/${selectedBookingId}/documents`, `${process.env.NEXT_PUBLIC_API_URL}/api/v1/csv-bookings/${selectedBookingId}/documents`,
{ method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: formData } { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: formData }
@ -335,7 +336,7 @@ export default function UserDocumentsPage() {
const formData = new FormData(); const formData = new FormData();
formData.append('document', replaceFile); formData.append('document', replaceFile);
const token = localStorage.getItem('access_token'); const token = getAuthToken();
const response = await fetch( const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/csv-bookings/${documentToReplace.bookingId}/documents/${documentToReplace.id}`, `${process.env.NEXT_PUBLIC_API_URL}/api/v1/csv-bookings/${documentToReplace.bookingId}/documents/${documentToReplace.id}`,
{ method: 'PATCH', headers: { Authorization: `Bearer ${token}` }, body: formData } { method: 'PATCH', headers: { Authorization: `Bearer ${token}` }, body: formData }