|
@@ -45,6 +45,12 @@ interface ImportDetail {
|
|
|
}>;
|
|
}>;
|
|
|
};
|
|
};
|
|
|
cintasSummaries: CintasSummary[];
|
|
cintasSummaries: CintasSummary[];
|
|
|
|
|
+ file?: {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ filename: string;
|
|
|
|
|
+ size: number;
|
|
|
|
|
+ contentType: string;
|
|
|
|
|
+ };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
interface ImportDetailDialogProps {
|
|
interface ImportDetailDialogProps {
|
|
@@ -57,6 +63,8 @@ export function ImportDetailDialog({ open, onOpenChange, importId }: ImportDetai
|
|
|
const [importDetail, setImportDetail] = useState<ImportDetail | null>(null);
|
|
const [importDetail, setImportDetail] = useState<ImportDetail | null>(null);
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [loading, setLoading] = useState(true);
|
|
|
const [calculating, setCalculating] = useState(false);
|
|
const [calculating, setCalculating] = useState(false);
|
|
|
|
|
+ const [processing, setProcessing] = useState(false);
|
|
|
|
|
+ const [importStatus, setImportStatus] = useState<'idle' | 'processing' | 'completed' | 'failed'>('idle');
|
|
|
const { toast } = useToast();
|
|
const { toast } = useToast();
|
|
|
|
|
|
|
|
const loadImportDetail = useCallback(async () => {
|
|
const loadImportDetail = useCallback(async () => {
|
|
@@ -118,6 +126,88 @@ export function ImportDetailDialog({ open, onOpenChange, importId }: ImportDetai
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ async function handleTriggerImport() {
|
|
|
|
|
+ if (!importDetail?.file) {
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: 'Error',
|
|
|
|
|
+ description: 'No file attached to this import',
|
|
|
|
|
+ variant: 'destructive',
|
|
|
|
|
+ });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ setProcessing(true);
|
|
|
|
|
+ setImportStatus('processing');
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch(`/api/imports/${importId}/trigger`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const result = await response.json();
|
|
|
|
|
+
|
|
|
|
|
+ if (result.success) {
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: 'Success',
|
|
|
|
|
+ description: 'Import process started successfully',
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Set up WebSocket connection for progress updates
|
|
|
|
|
+ const ws = new WebSocket(`ws://localhost:3001/import-progress/${importId}`);
|
|
|
|
|
+
|
|
|
|
|
+ ws.onmessage = (event) => {
|
|
|
|
|
+ const progress = JSON.parse(event.data);
|
|
|
|
|
+
|
|
|
|
|
+ if (progress.status === 'completed') {
|
|
|
|
|
+ setImportStatus('completed');
|
|
|
|
|
+ setProcessing(false);
|
|
|
|
|
+ ws.close();
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: 'Import Complete',
|
|
|
|
|
+ description: `Successfully imported ${progress.totalInserted || 0} rows`,
|
|
|
|
|
+ });
|
|
|
|
|
+ } else if (progress.status === 'failed') {
|
|
|
|
|
+ setImportStatus('failed');
|
|
|
|
|
+ setProcessing(false);
|
|
|
|
|
+ ws.close();
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: 'Import Failed',
|
|
|
|
|
+ description: progress.errors?.[0] || 'Import process failed',
|
|
|
|
|
+ variant: 'destructive',
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ ws.onerror = () => {
|
|
|
|
|
+ setProcessing(false);
|
|
|
|
|
+ setImportStatus('failed');
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: 'Connection Error',
|
|
|
|
|
+ description: 'Failed to connect to import progress server',
|
|
|
|
|
+ variant: 'destructive',
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ } else {
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: 'Error',
|
|
|
|
|
+ description: result.error || 'Failed to start import process',
|
|
|
|
|
+ variant: 'destructive',
|
|
|
|
|
+ });
|
|
|
|
|
+ setProcessing(false);
|
|
|
|
|
+ setImportStatus('failed');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: 'Error',
|
|
|
|
|
+ description: 'Failed to trigger import process',
|
|
|
|
|
+ variant: 'destructive',
|
|
|
|
|
+ });
|
|
|
|
|
+ setProcessing(false);
|
|
|
|
|
+ setImportStatus('failed');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (loading) {
|
|
if (loading) {
|
|
|
return (
|
|
return (
|
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
@@ -166,6 +256,12 @@ export function ImportDetailDialog({ open, onOpenChange, importId }: ImportDetai
|
|
|
<span className="font-medium">Import Date:</span>
|
|
<span className="font-medium">Import Date:</span>
|
|
|
<span>{format(importDetail.importDate, 'PPpp')}</span>
|
|
<span>{format(importDetail.importDate, 'PPpp')}</span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ {importDetail.file && (
|
|
|
|
|
+ <div className="flex justify-between">
|
|
|
|
|
+ <span className="font-medium">File:</span>
|
|
|
|
|
+ <span>{importDetail.file.filename}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
</CardContent>
|
|
</CardContent>
|
|
|
</Card>
|
|
</Card>
|
|
|
|
|
|
|
@@ -200,16 +296,56 @@ export function ImportDetailDialog({ open, onOpenChange, importId }: ImportDetai
|
|
|
<Card>
|
|
<Card>
|
|
|
<CardHeader>
|
|
<CardHeader>
|
|
|
<div className="flex justify-between items-center">
|
|
<div className="flex justify-between items-center">
|
|
|
- <CardTitle>Cintas Summaries</CardTitle>
|
|
|
|
|
- <Button
|
|
|
|
|
- onClick={handleCalculateSummaries}
|
|
|
|
|
- disabled={calculating}
|
|
|
|
|
- size="sm"
|
|
|
|
|
- >
|
|
|
|
|
- {calculating ? 'Calculating...' : 'Calculate Summaries'}
|
|
|
|
|
- </Button>
|
|
|
|
|
|
|
+ <CardTitle>Import Actions</CardTitle>
|
|
|
|
|
+ <div className="flex gap-2">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ onClick={handleTriggerImport}
|
|
|
|
|
+ disabled={processing || !importDetail.file}
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ variant="default"
|
|
|
|
|
+ >
|
|
|
|
|
+ {processing ? 'Processing...' : 'Start Import'}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ onClick={handleCalculateSummaries}
|
|
|
|
|
+ disabled={calculating || processing}
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ variant="secondary"
|
|
|
|
|
+ >
|
|
|
|
|
+ {calculating ? 'Calculating...' : 'Calculate Summaries'}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</CardHeader>
|
|
</CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ {importStatus === 'processing' && (
|
|
|
|
|
+ <div className="flex items-center gap-2 text-sm text-blue-600">
|
|
|
|
|
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600"></div>
|
|
|
|
|
+ Import is currently processing...
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {importStatus === 'completed' && (
|
|
|
|
|
+ <div className="text-sm text-green-600">
|
|
|
|
|
+ Import completed successfully!
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {importStatus === 'failed' && (
|
|
|
|
|
+ <div className="text-sm text-red-600">
|
|
|
|
|
+ Import failed. Please check the logs for details.
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {!importDetail.file && (
|
|
|
|
|
+ <div className="text-sm text-yellow-600">
|
|
|
|
|
+ No file attached. Please upload a file before starting import.
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <CardTitle>Cintas Summaries</CardTitle>
|
|
|
|
|
+ </CardHeader>
|
|
|
<CardContent>
|
|
<CardContent>
|
|
|
{importDetail.cintasSummaries.length === 0 ? (
|
|
{importDetail.cintasSummaries.length === 0 ? (
|
|
|
<p className="text-muted-foreground">No summaries calculated yet</p>
|
|
<p className="text-muted-foreground">No summaries calculated yet</p>
|