Added Documents Feature
This commit is contained in:
211
frontend/src/features/documents/mobile/DocumentsMobileScreen.tsx
Normal file
211
frontend/src/features/documents/mobile/DocumentsMobileScreen.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { useAuth0 } from '@auth0/auth0-react';
|
||||
import { isAxiosError } from 'axios';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard';
|
||||
import { useDocumentsList } from '../hooks/useDocuments';
|
||||
import { useUploadWithProgress } from '../hooks/useUploadWithProgress';
|
||||
import { Button } from '../../../shared-minimal/components/Button';
|
||||
import { AddDocumentDialog } from '../components/AddDocumentDialog';
|
||||
|
||||
export const DocumentsMobileScreen: React.FC = () => {
|
||||
console.log('[DocumentsMobileScreen] Component initializing');
|
||||
|
||||
// Auth is managed at App level; keep hook to support session-expired UI.
|
||||
// In test environments without provider, fall back gracefully.
|
||||
let auth = { isAuthenticated: true, isLoading: false, loginWithRedirect: () => {} } as any;
|
||||
try {
|
||||
auth = useAuth0();
|
||||
} catch {
|
||||
// Tests render without Auth0Provider; assume authenticated for unit tests.
|
||||
}
|
||||
|
||||
const { isAuthenticated, isLoading: authLoading, loginWithRedirect } = auth;
|
||||
|
||||
// Data hooks (unconditional per React rules)
|
||||
const { data, isLoading, error } = useDocumentsList();
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [currentId, setCurrentId] = React.useState<string | null>(null);
|
||||
const upload = useUploadWithProgress(currentId || '');
|
||||
const navigate = useNavigate();
|
||||
const [isAddOpen, setIsAddOpen] = React.useState(false);
|
||||
|
||||
const triggerUpload = (docId: string) => {
|
||||
try {
|
||||
setCurrentId(docId);
|
||||
if (!inputRef.current) return;
|
||||
inputRef.current.value = '';
|
||||
inputRef.current.click();
|
||||
} catch (error) {
|
||||
console.error('[Documents Mobile] Upload trigger error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const onFileChange = () => {
|
||||
try {
|
||||
const file = inputRef.current?.files?.[0];
|
||||
if (file && currentId) {
|
||||
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);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Documents Mobile] File change error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Show loading while auth is initializing
|
||||
if (authLoading) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<GlassCard>
|
||||
<div className="p-4">
|
||||
<h2 className="text-lg font-semibold text-slate-800 mb-2">Documents</h2>
|
||||
<div className="text-slate-500 py-6 text-center">Loading...</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show login prompt when not authenticated
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<GlassCard>
|
||||
<div className="p-6 text-center">
|
||||
<div className="mb-4">
|
||||
<div className="mx-auto w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-blue-600" 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>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-slate-800 mb-2">Login Required</h3>
|
||||
<p className="text-slate-600 text-sm mb-4">Please log in to view your documents</p>
|
||||
<button
|
||||
onClick={() => loginWithRedirect()}
|
||||
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Login to Continue
|
||||
</button>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Check for authentication error (401)
|
||||
const isAuthError = error && isAxiosError(error) && error.response?.status === 401;
|
||||
const hasError = !!error;
|
||||
if (isAuthError) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<GlassCard>
|
||||
<div className="p-6 text-center">
|
||||
<div className="mb-4">
|
||||
<div className="mx-auto w-16 h-16 bg-orange-100 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-orange-600" 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>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-slate-800 mb-2">Session Expired</h3>
|
||||
<p className="text-slate-600 text-sm mb-4">Your session has expired. Please log in again.</p>
|
||||
<button
|
||||
onClick={() => loginWithRedirect()}
|
||||
className="w-full px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition-colors"
|
||||
>
|
||||
Login Again
|
||||
</button>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<input ref={inputRef} type="file" accept="image/jpeg,image/png,application/pdf" capture="environment" className="hidden" onChange={onFileChange} />
|
||||
<AddDocumentDialog open={isAddOpen} onClose={() => setIsAddOpen(false)} />
|
||||
<GlassCard>
|
||||
<div className="p-4">
|
||||
<h2 className="text-lg font-semibold text-slate-800 mb-2">Documents</h2>
|
||||
|
||||
<div className="flex justify-end mb-2">
|
||||
<Button onClick={() => setIsAddOpen(true)} className="min-h-[44px]">Add Document</Button>
|
||||
</div>
|
||||
|
||||
{isLoading && <div className="text-slate-500 py-6 text-center">Loading...</div>}
|
||||
|
||||
{hasError && !isAuthError && (
|
||||
<div className="py-6 text-center">
|
||||
<div className="mb-4">
|
||||
<div className="mx-auto w-12 h-12 bg-red-100 rounded-full flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} 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>
|
||||
</div>
|
||||
<p className="text-red-600 text-sm mb-3">Failed to load documents</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && !hasError && data && data.length === 0 && (
|
||||
<div className="py-8 text-center">
|
||||
<div className="mb-4">
|
||||
<div className="mx-auto w-12 h-12 bg-slate-100 rounded-full flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-slate-600 text-sm mb-3">No documents yet</p>
|
||||
<p className="text-slate-500 text-xs">Documents will appear here once you create them</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && !hasError && data && data.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{data.map((doc) => {
|
||||
const vehicleLabel = doc.vehicle_id ? `${doc.vehicle_id.slice(0, 8)}...` : '—';
|
||||
return (
|
||||
<div key={doc.id} className="flex items-center justify-between border rounded-xl p-3">
|
||||
<div>
|
||||
<div className="font-medium text-slate-800">{doc.title}</div>
|
||||
<div className="text-xs text-slate-500">{doc.document_type} • {vehicleLabel}</div>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Button onClick={() => navigate(`/documents/${doc.id}`)}>Open</Button>
|
||||
<Button onClick={() => triggerUpload(doc.id)}>Upload</Button>
|
||||
{upload.isPending && currentId === doc.id && (
|
||||
<span className="text-xs text-slate-500">{upload.progress}%</span>
|
||||
)}
|
||||
{upload.isError && currentId === doc.id && (
|
||||
<span className="text-xs text-red-600">
|
||||
{((upload.error as any)?.response?.status === 415)
|
||||
? 'Unsupported file type. Use PDF, JPG/JPEG, PNG.'
|
||||
: 'Upload failed'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentsMobileScreen;
|
||||
Reference in New Issue
Block a user