727 lines
27 KiB
TypeScript
727 lines
27 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||
import LoginForm from './components/LoginForm';
|
||
import ProtectedRoute from './components/ProtectedRoute';
|
||
import DocumentUpload from './components/DocumentUpload';
|
||
import DocumentList from './components/DocumentList';
|
||
import DocumentViewer from './components/DocumentViewer';
|
||
import Analytics from './components/Analytics';
|
||
import LogoutButton from './components/LogoutButton';
|
||
import { documentService } from './services/documentService';
|
||
import {
|
||
Home,
|
||
Upload,
|
||
FileText,
|
||
BarChart3,
|
||
Plus,
|
||
Search,
|
||
TrendingUp
|
||
} from 'lucide-react';
|
||
import { cn } from './utils/cn';
|
||
// import { parseCIMReviewData } from './utils/parseCIMData';
|
||
|
||
// Mock data for demonstration
|
||
// const mockDocuments = [
|
||
// {
|
||
// id: '1',
|
||
// name: 'Sample CIM Document 1',
|
||
// originalName: 'sample_cim_1.pdf',
|
||
// status: 'completed' as const,
|
||
// uploadedAt: '2024-01-15T10:30:00Z',
|
||
// processedAt: '2024-01-15T10:35:00Z',
|
||
// uploadedBy: 'John Doe',
|
||
// fileSize: 2048576,
|
||
// pageCount: 25,
|
||
// summary: 'This is a sample CIM document for demonstration purposes.',
|
||
// },
|
||
// {
|
||
// id: '2',
|
||
// name: 'Sample CIM Document 2',
|
||
// originalName: 'sample_cim_2.pdf',
|
||
// status: 'processing' as const,
|
||
// uploadedAt: '2024-01-15T11:00:00Z',
|
||
// uploadedBy: 'Jane Smith',
|
||
// fileSize: 1536000,
|
||
// pageCount: 18,
|
||
// },
|
||
// ];
|
||
|
||
// const mockExtractedData = {
|
||
// companyName: 'Sample Company Inc.',
|
||
// industry: 'Technology',
|
||
// revenue: '$50M',
|
||
// ebitda: '$8M',
|
||
// employees: '150',
|
||
// founded: '2010',
|
||
// location: 'San Francisco, CA',
|
||
// summary: 'A technology company focused on innovative solutions.',
|
||
// keyMetrics: {
|
||
// 'Revenue Growth': '25%',
|
||
// 'EBITDA Margin': '16%',
|
||
// 'Employee Count': '150',
|
||
// },
|
||
// financials: {
|
||
// revenue: ['$40M', '$45M', '$50M'],
|
||
// ebitda: ['$6M', '$7M', '$8M'],
|
||
// margins: ['15%', '15.6%', '16%'],
|
||
// },
|
||
// risks: [
|
||
// 'Market competition',
|
||
// 'Technology disruption',
|
||
// 'Talent retention',
|
||
// ],
|
||
// opportunities: [
|
||
// 'Market expansion',
|
||
// 'Product diversification',
|
||
// 'Strategic partnerships',
|
||
// ],
|
||
// };
|
||
|
||
// Dashboard component
|
||
const Dashboard: React.FC = () => {
|
||
const { user } = useAuth();
|
||
const [documents, setDocuments] = useState<any[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [viewingDocument, setViewingDocument] = useState<string | null>(null);
|
||
const [searchTerm, setSearchTerm] = useState('');
|
||
const [activeTab, setActiveTab] = useState<'overview' | 'documents' | 'upload' | 'analytics'>('overview');
|
||
|
||
// Map backend status to frontend status
|
||
const mapBackendStatus = (backendStatus: string): string => {
|
||
switch (backendStatus) {
|
||
case 'uploaded':
|
||
return 'uploaded';
|
||
case 'extracting_text':
|
||
case 'processing_llm':
|
||
case 'generating_pdf':
|
||
return 'processing';
|
||
case 'completed':
|
||
return 'completed';
|
||
case 'failed':
|
||
return 'error';
|
||
default:
|
||
return 'pending';
|
||
}
|
||
};
|
||
|
||
// Fetch documents from API
|
||
const fetchDocuments = useCallback(async () => {
|
||
try {
|
||
setLoading(true);
|
||
const response = await fetch('/api/documents', {
|
||
headers: {
|
||
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
});
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
// The API returns an array directly, not wrapped in success/data
|
||
if (Array.isArray(result)) {
|
||
// Transform backend data to frontend format
|
||
const transformedDocs = result.map((doc: any) => ({
|
||
id: doc.id,
|
||
name: doc.name || doc.originalName,
|
||
originalName: doc.originalName,
|
||
status: mapBackendStatus(doc.status),
|
||
uploadedAt: doc.uploadedAt,
|
||
processedAt: doc.processedAt,
|
||
uploadedBy: user?.name || user?.email || 'Unknown',
|
||
fileSize: parseInt(doc.fileSize) || 0,
|
||
summary: doc.summary,
|
||
error: doc.error,
|
||
analysisData: doc.extractedData, // Include the enhanced BPCP CIM Review Template data
|
||
}));
|
||
setDocuments(transformedDocs);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to fetch documents:', error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [user?.name, user?.email]);
|
||
|
||
// Poll for status updates on documents that are being processed
|
||
const pollDocumentStatus = useCallback(async (documentId: string) => {
|
||
// Guard against undefined or null document IDs
|
||
if (!documentId || documentId === 'undefined' || documentId === 'null') {
|
||
console.warn('Attempted to poll for document with invalid ID:', documentId);
|
||
return false; // Stop polling
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/documents/${documentId}/progress`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
});
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
const progress = result.data;
|
||
|
||
// Update the document status based on progress
|
||
setDocuments(prev => prev.map(doc => {
|
||
if (doc.id === documentId) {
|
||
let newStatus = doc.status;
|
||
|
||
if (progress.status === 'processing') {
|
||
newStatus = 'processing';
|
||
} else if (progress.status === 'completed') {
|
||
newStatus = 'completed';
|
||
} else if (progress.status === 'error') {
|
||
newStatus = 'error';
|
||
}
|
||
|
||
return {
|
||
...doc,
|
||
status: newStatus,
|
||
progress: progress.progress || 0,
|
||
message: progress.message || doc.message,
|
||
};
|
||
}
|
||
return doc;
|
||
}));
|
||
|
||
// Stop polling if completed or error
|
||
if (progress.status === 'completed' || progress.status === 'error') {
|
||
// Refresh the documents list to get the latest data including summary
|
||
fetchDocuments();
|
||
return false; // Stop polling
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to fetch document progress:', error);
|
||
}
|
||
|
||
return true; // Continue polling
|
||
}, []);
|
||
|
||
// Set up polling for documents that are being processed or uploaded (might be processing)
|
||
useEffect(() => {
|
||
const processingDocuments = documents.filter(doc =>
|
||
(doc.status === 'processing' || doc.status === 'uploaded' || doc.status === 'extracting_text') && doc.id
|
||
);
|
||
|
||
if (processingDocuments.length === 0) {
|
||
return;
|
||
}
|
||
|
||
const pollIntervals: NodeJS.Timeout[] = [];
|
||
|
||
processingDocuments.forEach(doc => {
|
||
// Skip if document ID is undefined or null
|
||
if (!doc.id) {
|
||
console.warn('Skipping polling for document with undefined ID:', doc);
|
||
return;
|
||
}
|
||
|
||
const interval = setInterval(async () => {
|
||
const shouldContinue = await pollDocumentStatus(doc.id);
|
||
if (!shouldContinue) {
|
||
clearInterval(interval);
|
||
}
|
||
}, 3000); // Poll every 3 seconds
|
||
|
||
pollIntervals.push(interval);
|
||
});
|
||
|
||
// Cleanup intervals on unmount or when documents change
|
||
return () => {
|
||
pollIntervals.forEach(interval => clearInterval(interval));
|
||
};
|
||
}, [documents, pollDocumentStatus]);
|
||
|
||
// Load documents on component mount and refresh periodically
|
||
React.useEffect(() => {
|
||
fetchDocuments();
|
||
|
||
// Refresh documents every 30 seconds to catch any updates
|
||
const refreshInterval = setInterval(() => {
|
||
fetchDocuments();
|
||
}, 30000);
|
||
|
||
return () => clearInterval(refreshInterval);
|
||
}, [fetchDocuments]);
|
||
|
||
const handleUploadComplete = (fileId: string) => {
|
||
console.log('Upload completed:', fileId);
|
||
// Refresh documents list after upload
|
||
fetchDocuments();
|
||
};
|
||
|
||
const handleUploadError = (error: string) => {
|
||
console.error('Upload error:', error);
|
||
// In a real app, this would show an error notification
|
||
};
|
||
|
||
const handleViewDocument = (documentId: string) => {
|
||
setViewingDocument(documentId);
|
||
};
|
||
|
||
const handleDownloadDocument = async (documentId: string) => {
|
||
try {
|
||
console.log('Downloading document:', documentId);
|
||
const blob = await documentService.downloadDocument(documentId);
|
||
|
||
// Create download link
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `document-${documentId}.pdf`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
window.URL.revokeObjectURL(url);
|
||
document.body.removeChild(a);
|
||
|
||
console.log('Download completed');
|
||
} catch (error) {
|
||
console.error('Download failed:', error);
|
||
alert('Failed to download document. Please try again.');
|
||
}
|
||
};
|
||
|
||
const handleDeleteDocument = async (documentId: string) => {
|
||
// Show confirmation dialog
|
||
const confirmed = window.confirm('Are you sure you want to delete this document? This action cannot be undone.');
|
||
if (!confirmed) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Call the backend API to delete the document
|
||
await documentService.deleteDocument(documentId);
|
||
|
||
// Remove from local state
|
||
setDocuments(prev => prev.filter(doc => doc.id !== documentId));
|
||
|
||
// Show success message
|
||
alert('Document deleted successfully');
|
||
} catch (error) {
|
||
console.error('Failed to delete document:', error);
|
||
alert('Failed to delete document. Please try again.');
|
||
}
|
||
};
|
||
|
||
const handleRetryProcessing = (documentId: string) => {
|
||
console.log('Retrying processing for document:', documentId);
|
||
// In a real app, this would retry the processing
|
||
};
|
||
|
||
const handleBackFromViewer = () => {
|
||
setViewingDocument(null);
|
||
};
|
||
|
||
const filteredDocuments = documents.filter(doc =>
|
||
doc.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
doc.originalName.toLowerCase().includes(searchTerm.toLowerCase())
|
||
);
|
||
|
||
const stats = {
|
||
totalDocuments: documents.length,
|
||
completedDocuments: documents.filter(d => d.status === 'completed').length,
|
||
processingDocuments: documents.filter(d => d.status === 'processing').length,
|
||
errorDocuments: documents.filter(d => d.status === 'error').length,
|
||
};
|
||
|
||
if (viewingDocument) {
|
||
const document = documents.find(d => d.id === viewingDocument);
|
||
if (!document) return null;
|
||
|
||
|
||
|
||
// The new analysisData is already in the BPCP template format
|
||
const cimReviewData = document.analysisData;
|
||
|
||
const extractedData = cimReviewData ? {
|
||
companyName: cimReviewData?.dealOverview?.targetCompanyName || 'Not specified',
|
||
industry: cimReviewData?.dealOverview?.industrySector || 'Not specified',
|
||
// For revenue and ebitda, we'll take the most recent value from the financial summary.
|
||
revenue: cimReviewData?.financialSummary?.financials?.ltm?.revenue || 'N/A',
|
||
ebitda: cimReviewData?.financialSummary?.financials?.ltm?.ebitda || 'N/A',
|
||
employees: cimReviewData?.dealOverview?.employeeCount || 'Not specified',
|
||
founded: 'Not specified', // This field is not in the new schema
|
||
location: cimReviewData?.dealOverview?.geography || 'Not specified',
|
||
summary: cimReviewData?.preliminaryInvestmentThesis?.keyAttractions || 'No summary available',
|
||
keyMetrics: {
|
||
'Transaction Type': cimReviewData?.dealOverview?.transactionType || 'Not specified',
|
||
'Deal Source': cimReviewData?.dealOverview?.dealSource || 'Not specified',
|
||
},
|
||
financials: {
|
||
revenue: [
|
||
cimReviewData?.financialSummary?.financials?.fy3?.revenue || 'N/A',
|
||
cimReviewData?.financialSummary?.financials?.fy2?.revenue || 'N/A',
|
||
cimReviewData?.financialSummary?.financials?.fy1?.revenue || 'N/A',
|
||
cimReviewData?.financialSummary?.financials?.ltm?.revenue || 'N/A',
|
||
],
|
||
ebitda: [
|
||
cimReviewData?.financialSummary?.financials?.fy3?.ebitda || 'N/A',
|
||
cimReviewData?.financialSummary?.financials?.fy2?.ebitda || 'N/A',
|
||
cimReviewData?.financialSummary?.financials?.fy1?.ebitda || 'N/A',
|
||
cimReviewData?.financialSummary?.financials?.ltm?.ebitda || 'N/A',
|
||
],
|
||
margins: [
|
||
cimReviewData?.financialSummary?.financials?.fy3?.ebitdaMargin || 'N/A',
|
||
cimReviewData?.financialSummary?.financials?.fy2?.ebitdaMargin || 'N/A',
|
||
cimReviewData?.financialSummary?.financials?.fy1?.ebitdaMargin || 'N/A',
|
||
cimReviewData?.financialSummary?.financials?.ltm?.ebitdaMargin || 'N/A',
|
||
],
|
||
},
|
||
risks: [cimReviewData?.preliminaryInvestmentThesis?.potentialRisks || 'Not specified'],
|
||
opportunities: [cimReviewData?.preliminaryInvestmentThesis?.valueCreationLevers || 'Not specified'],
|
||
} : undefined;
|
||
|
||
return (
|
||
<DocumentViewer
|
||
documentId={document.id}
|
||
documentName={document.name}
|
||
extractedData={extractedData}
|
||
cimReviewData={cimReviewData}
|
||
onBack={handleBackFromViewer}
|
||
onDownload={() => handleDownloadDocument(document.id)}
|
||
onShare={() => console.log('Share document:', document.id)}
|
||
/>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50">
|
||
{/* Navigation */}
|
||
<nav className="bg-primary-600 shadow-soft border-b border-primary-700">
|
||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||
<div className="flex justify-between h-16">
|
||
<div className="flex items-center">
|
||
<h1 className="text-xl font-semibold text-white">
|
||
CIM Document Processor
|
||
</h1>
|
||
</div>
|
||
<div className="flex items-center space-x-4">
|
||
<span className="text-sm text-white">
|
||
Welcome, {user?.name || user?.email}
|
||
</span>
|
||
<LogoutButton variant="link" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||
{/* Tab Navigation */}
|
||
<div className="bg-white shadow-soft border-b border-gray-200 mb-6">
|
||
<div className="px-4 sm:px-6 lg:px-8">
|
||
<nav className="-mb-px flex space-x-8">
|
||
<button
|
||
onClick={() => setActiveTab('overview')}
|
||
className={cn(
|
||
'flex items-center py-4 px-1 border-b-2 font-medium text-sm transition-colors duration-200',
|
||
activeTab === 'overview'
|
||
? 'border-primary-600 text-primary-700'
|
||
: 'border-transparent text-gray-500 hover:text-primary-600 hover:border-primary-300'
|
||
)}
|
||
>
|
||
<Home className="h-4 w-4 mr-2" />
|
||
Overview
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('documents')}
|
||
className={cn(
|
||
'flex items-center py-4 px-1 border-b-2 font-medium text-sm transition-colors duration-200',
|
||
activeTab === 'documents'
|
||
? 'border-primary-600 text-primary-700'
|
||
: 'border-transparent text-gray-500 hover:text-primary-600 hover:border-primary-300'
|
||
)}
|
||
>
|
||
<FileText className="h-4 w-4 mr-2" />
|
||
Documents
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('upload')}
|
||
className={cn(
|
||
'flex items-center py-4 px-1 border-b-2 font-medium text-sm transition-colors duration-200',
|
||
activeTab === 'upload'
|
||
? 'border-primary-600 text-primary-700'
|
||
: 'border-transparent text-gray-500 hover:text-primary-600 hover:border-primary-300'
|
||
)}
|
||
>
|
||
<Upload className="h-4 w-4 mr-2" />
|
||
Upload
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('analytics')}
|
||
className={cn(
|
||
'flex items-center py-4 px-1 border-b-2 font-medium text-sm transition-colors duration-200',
|
||
activeTab === 'analytics'
|
||
? 'border-primary-600 text-primary-700'
|
||
: 'border-transparent text-gray-500 hover:text-primary-600 hover:border-primary-300'
|
||
)}
|
||
>
|
||
<TrendingUp className="h-4 w-4 mr-2" />
|
||
Analytics
|
||
</button>
|
||
</nav>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Content */}
|
||
<div className="px-4 sm:px-0">
|
||
{activeTab === 'overview' && (
|
||
<div className="space-y-6">
|
||
{/* Stats Cards */}
|
||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||
<div className="bg-white overflow-hidden shadow-soft rounded-lg border border-gray-100">
|
||
<div className="p-5">
|
||
<div className="flex items-center">
|
||
<div className="flex-shrink-0">
|
||
<FileText className="h-6 w-6 text-primary-500" />
|
||
</div>
|
||
<div className="ml-5 w-0 flex-1">
|
||
<dl>
|
||
<dt className="text-sm font-medium text-gray-600 truncate">
|
||
Total Documents
|
||
</dt>
|
||
<dd className="text-lg font-semibold text-primary-800">
|
||
{stats.totalDocuments}
|
||
</dd>
|
||
</dl>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white overflow-hidden shadow-soft rounded-lg border border-gray-100">
|
||
<div className="p-5">
|
||
<div className="flex items-center">
|
||
<div className="flex-shrink-0">
|
||
<BarChart3 className="h-6 w-6 text-success-500" />
|
||
</div>
|
||
<div className="ml-5 w-0 flex-1">
|
||
<dl>
|
||
<dt className="text-sm font-medium text-gray-600 truncate">
|
||
Completed
|
||
</dt>
|
||
<dd className="text-lg font-semibold text-primary-800">
|
||
{stats.completedDocuments}
|
||
</dd>
|
||
</dl>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white overflow-hidden shadow-soft rounded-lg border border-gray-100">
|
||
<div className="p-5">
|
||
<div className="flex items-center">
|
||
<div className="flex-shrink-0">
|
||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-accent-500" />
|
||
</div>
|
||
<div className="ml-5 w-0 flex-1">
|
||
<dl>
|
||
<dt className="text-sm font-medium text-gray-600 truncate">
|
||
Processing
|
||
</dt>
|
||
<dd className="text-lg font-semibold text-primary-800">
|
||
{stats.processingDocuments}
|
||
</dd>
|
||
</dl>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white overflow-hidden shadow-soft rounded-lg border border-gray-100">
|
||
<div className="p-5">
|
||
<div className="flex items-center">
|
||
<div className="flex-shrink-0">
|
||
<div className="h-6 w-6 text-error-500">⚠️</div>
|
||
</div>
|
||
<div className="ml-5 w-0 flex-1">
|
||
<dl>
|
||
<dt className="text-sm font-medium text-gray-600 truncate">
|
||
Errors
|
||
</dt>
|
||
<dd className="text-lg font-semibold text-primary-800">
|
||
{stats.errorDocuments}
|
||
</dd>
|
||
</dl>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Recent Documents */}
|
||
<div className="bg-white shadow-soft rounded-lg border border-gray-100">
|
||
<div className="px-4 py-5 sm:p-6">
|
||
<h3 className="text-lg leading-6 font-medium text-primary-800 mb-4">
|
||
Recent Documents
|
||
</h3>
|
||
<DocumentList
|
||
documents={documents.slice(0, 3)}
|
||
onViewDocument={handleViewDocument}
|
||
onDownloadDocument={handleDownloadDocument}
|
||
onDeleteDocument={handleDeleteDocument}
|
||
onRetryProcessing={handleRetryProcessing}
|
||
onRefresh={fetchDocuments}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'documents' && (
|
||
<div className="space-y-6">
|
||
{/* Search and Actions */}
|
||
<div className="bg-white shadow-soft rounded-lg border border-gray-100 p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex-1 max-w-lg">
|
||
<label htmlFor="search" className="sr-only">
|
||
Search documents
|
||
</label>
|
||
<div className="relative">
|
||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||
<Search className="h-5 w-5 text-gray-400" />
|
||
</div>
|
||
<input
|
||
id="search"
|
||
name="search"
|
||
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-primary-500 focus:border-primary-500 sm:text-sm transition-colors duration-200"
|
||
placeholder="Search documents..."
|
||
type="search"
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="flex space-x-3">
|
||
<button
|
||
onClick={fetchDocuments}
|
||
disabled={loading}
|
||
className="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-soft text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 transition-colors duration-200"
|
||
>
|
||
<div className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`}>🔄</div>
|
||
Refresh
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('upload')}
|
||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-soft text-white bg-accent-500 hover:bg-accent-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-500 transition-colors duration-200"
|
||
>
|
||
<Plus className="h-4 w-4 mr-2" />
|
||
Upload New
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Documents List */}
|
||
{loading ? (
|
||
<div className="text-center py-12">
|
||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-accent-500 mx-auto mb-4"></div>
|
||
<p className="text-gray-600">Loading documents...</p>
|
||
</div>
|
||
) : (
|
||
<DocumentList
|
||
documents={filteredDocuments}
|
||
onViewDocument={handleViewDocument}
|
||
onDownloadDocument={handleDownloadDocument}
|
||
onDeleteDocument={handleDeleteDocument}
|
||
onRetryProcessing={handleRetryProcessing}
|
||
onRefresh={fetchDocuments}
|
||
/>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'upload' && (
|
||
<div className="bg-white shadow-soft rounded-lg border border-gray-100 p-6">
|
||
<h3 className="text-lg leading-6 font-medium text-primary-800 mb-6">
|
||
Upload CIM Documents
|
||
</h3>
|
||
<DocumentUpload
|
||
onUploadComplete={handleUploadComplete}
|
||
onUploadError={handleUploadError}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'analytics' && (
|
||
<Analytics />
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// Login page component
|
||
const LoginPage: React.FC = () => {
|
||
const { user } = useAuth();
|
||
|
||
// Redirect to dashboard if already authenticated
|
||
if (user) {
|
||
return <Navigate to="/dashboard" replace />;
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||
CIM Document Processor
|
||
</h2>
|
||
</div>
|
||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||
<LoginForm />
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// Unauthorized page component
|
||
const UnauthorizedPage: React.FC = () => {
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||
<div className="text-center">
|
||
<h2 className="text-2xl font-bold text-gray-900 mb-4">
|
||
Access Denied
|
||
</h2>
|
||
<p className="text-gray-600 mb-6">
|
||
You don't have permission to access this resource.
|
||
</p>
|
||
<LogoutButton />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const App: React.FC = () => {
|
||
return (
|
||
<AuthProvider>
|
||
<Router>
|
||
<Routes>
|
||
<Route path="/login" element={<LoginPage />} />
|
||
<Route path="/unauthorized" element={<UnauthorizedPage />} />
|
||
<Route
|
||
path="/dashboard"
|
||
element={
|
||
<ProtectedRoute>
|
||
<Dashboard />
|
||
</ProtectedRoute>
|
||
}
|
||
/>
|
||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||
</Routes>
|
||
</Router>
|
||
</AuthProvider>
|
||
);
|
||
};
|
||
|
||
export default App; |