Added Documents Feature
This commit is contained in:
168
frontend/src/features/documents/pages/DocumentDetailPage.tsx
Normal file
168
frontend/src/features/documents/pages/DocumentDetailPage.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useAuth0 } from '@auth0/auth0-react';
|
||||
import { isAxiosError } from 'axios';
|
||||
import { Card } from '../../../shared-minimal/components/Card';
|
||||
import { Button } from '../../../shared-minimal/components/Button';
|
||||
import { useDocument } from '../hooks/useDocuments';
|
||||
import { useUploadWithProgress } from '../hooks/useUploadWithProgress';
|
||||
import { documentsApi } from '../api/documents.api';
|
||||
import { DocumentPreview } from '../components/DocumentPreview';
|
||||
|
||||
export const DocumentDetailPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { isAuthenticated, isLoading: authLoading, loginWithRedirect } = useAuth0();
|
||||
const { data: doc, isLoading, error } = useDocument(id);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const upload = useUploadWithProgress(id!);
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (!id) return;
|
||||
const blob = await documentsApi.download(id);
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
|
||||
const handleUpload = () => {
|
||||
if (!inputRef.current) return;
|
||||
inputRef.current.onchange = () => {
|
||||
const file = inputRef.current?.files?.[0];
|
||||
if (file && id) {
|
||||
const allowed = new Set(['application/pdf', 'image/jpeg', 'image/png']);
|
||||
if (!file.type || !allowed.has(file.type)) {
|
||||
alert('Unsupported file type. Allowed: PDF, JPG/JPEG, PNG.');
|
||||
return;
|
||||
}
|
||||
upload.mutate(file);
|
||||
}
|
||||
};
|
||||
inputRef.current.click();
|
||||
};
|
||||
|
||||
// Show loading while auth is initializing
|
||||
if (authLoading) {
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<div className="text-slate-500">Checking authentication...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show login prompt when not authenticated
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<Card>
|
||||
<div className="p-8 text-center">
|
||||
<div className="mb-4">
|
||||
<svg className="mx-auto w-16 h-16 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-slate-800 mb-2">Authentication Required</h3>
|
||||
<p className="text-slate-600 mb-6">Please log in to view this document</p>
|
||||
<div className="space-x-3">
|
||||
<Button onClick={() => loginWithRedirect()}>Login to Continue</Button>
|
||||
<Button variant="secondary" onClick={() => navigate('/documents')}>Back to Documents</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Check for authentication error (401)
|
||||
const isAuthError = error && isAxiosError(error) && error.response?.status === 401;
|
||||
if (isAuthError) {
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<Card>
|
||||
<div className="p-8 text-center">
|
||||
<div className="mb-4">
|
||||
<svg className="mx-auto w-16 h-16 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-slate-800 mb-2">Session Expired</h3>
|
||||
<p className="text-slate-600 mb-6">Your session has expired. Please log in again to continue.</p>
|
||||
<div className="space-x-3">
|
||||
<Button onClick={() => loginWithRedirect()}>Login Again</Button>
|
||||
<Button variant="secondary" onClick={() => navigate('/documents')}>Back to Documents</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) return <div className="container mx-auto p-4">Loading document...</div>;
|
||||
|
||||
if (error && !isAuthError) {
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<Card>
|
||||
<div className="p-8 text-center">
|
||||
<div className="mb-4">
|
||||
<svg className="mx-auto w-16 h-16 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-slate-800 mb-2">Document Not Found</h3>
|
||||
<p className="text-slate-600 mb-6">The document you're looking for could not be found.</p>
|
||||
<div className="space-x-3">
|
||||
<Button onClick={() => window.location.reload()}>Retry</Button>
|
||||
<Button variant="secondary" onClick={() => navigate('/documents')}>Back to Documents</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!doc) {
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<Card>
|
||||
<div className="p-8 text-center">
|
||||
<h3 className="text-lg font-semibold text-slate-800 mb-2">Document Not Found</h3>
|
||||
<p className="text-slate-600 mb-6">The document you're looking for does not exist.</p>
|
||||
<Button onClick={() => navigate('/documents')}>Back to Documents</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<input ref={inputRef} type="file" accept="image/jpeg,image/png,application/pdf" capture="environment" className="hidden" />
|
||||
<Card>
|
||||
<div className="p-4 space-y-2">
|
||||
<h2 className="text-xl font-semibold">{doc.title}</h2>
|
||||
<div className="text-sm text-slate-500">Type: {doc.document_type}</div>
|
||||
<div className="text-sm text-slate-500">Vehicle: {doc.vehicle_id}</div>
|
||||
<div className="pt-2">
|
||||
<DocumentPreview doc={doc} />
|
||||
</div>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button onClick={handleDownload}>Download</Button>
|
||||
<Button onClick={handleUpload}>Upload/Replace</Button>
|
||||
</div>
|
||||
{upload.isPending && (
|
||||
<div className="text-sm text-slate-600">Uploading... {upload.progress}%</div>
|
||||
)}
|
||||
{upload.isError && (
|
||||
<div className="text-sm text-red-600">
|
||||
{((upload.error as any)?.response?.status === 415)
|
||||
? 'Unsupported file type. Allowed: PDF, JPG/JPEG, PNG.'
|
||||
: 'Failed to upload file. Please try again.'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentDetailPage;
|
||||
Reference in New Issue
Block a user