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:
parent
b344650608
commit
1da5570e1c
@ -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 */}
|
||||||
|
|||||||
@ -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 }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user