Переглянути джерело

feat(imports): add trigger import functionality with API endpoint and UI integration

- Add triggerImport action to validate and initiate import processing
- Create POST /api/imports/[id]/trigger endpoint for background processing
- Update ImportDetailDialog with trigger import button and progress tracking
- Add file validation and error handling for import triggers
- Implement WebSocket connection for real-time import progress updates
vtugulan 6 місяців тому
батько
коміт
096f1b6f02

+ 29 - 0
app/actions/imports.ts

@@ -215,4 +215,33 @@ export async function getLayoutConfigurations() {
     console.error('Error fetching layout configurations:', error);
     return { success: false, error: 'Failed to fetch layout configurations' };
   }
+}
+
+// Trigger import process
+export async function triggerImport(importId: number) {
+  try {
+    // Validate import exists
+    const importRecord = await prisma.import.findUnique({
+      where: { id: importId },
+      include: { file: true, layout: true }
+    });
+
+    if (!importRecord) {
+      return { success: false, error: 'Import not found' };
+    }
+
+    if (!importRecord.file) {
+      return { success: false, error: 'No file attached to import' };
+    }
+
+    if (!importRecord.layout) {
+      return { success: false, error: 'No layout configuration found' };
+    }
+
+    // Return success - the actual processing will be handled by the API endpoint
+    return { success: true, message: 'Import process triggered successfully' };
+  } catch (error) {
+    console.error('Error triggering import:', error);
+    return { success: false, error: 'Failed to trigger import' };
+  }
 }

+ 48 - 0
app/api/imports/[id]/trigger/route.ts

@@ -0,0 +1,48 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { ImportProcessor } from '@/app/lib/excel-import/import-processor';
+
+export async function POST(
+  request: NextRequest,
+  { params }: { params: { id: string } }
+) {
+  try {
+    const importId = parseInt(params.id);
+    
+    if (isNaN(importId)) {
+      return NextResponse.json(
+        { success: false, error: 'Invalid import ID' },
+        { status: 400 }
+      );
+    }
+
+    // Initialize import processor
+    const processor = new ImportProcessor();
+    
+    // Validate import before processing
+    const validation = await processor.validateImport(importId);
+    if (!validation.valid) {
+      return NextResponse.json(
+        { success: false, error: validation.errors.join(', ') },
+        { status: 400 }
+      );
+    }
+
+    // Start processing in background
+    processor.processImport(importId).catch(error => {
+      console.error('Import processing failed:', error);
+    });
+
+    return NextResponse.json({
+      success: true,
+      message: 'Import process started successfully',
+      importId
+    });
+
+  } catch (error) {
+    console.error('Error triggering import:', error);
+    return NextResponse.json(
+      { success: false, error: 'Failed to trigger import' },
+      { status: 500 }
+    );
+  }
+}

+ 144 - 8
app/components/imports/ImportDetailDialog.tsx

@@ -45,6 +45,12 @@ interface ImportDetail {
     }>;
   };
   cintasSummaries: CintasSummary[];
+  file?: {
+    id: string;
+    filename: string;
+    size: number;
+    contentType: string;
+  };
 }
 
 interface ImportDetailDialogProps {
@@ -57,6 +63,8 @@ export function ImportDetailDialog({ open, onOpenChange, importId }: ImportDetai
   const [importDetail, setImportDetail] = useState<ImportDetail | null>(null);
   const [loading, setLoading] = useState(true);
   const [calculating, setCalculating] = useState(false);
+  const [processing, setProcessing] = useState(false);
+  const [importStatus, setImportStatus] = useState<'idle' | 'processing' | 'completed' | 'failed'>('idle');
   const { toast } = useToast();
 
   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) {
     return (
       <Dialog open={open} onOpenChange={onOpenChange}>
@@ -166,6 +256,12 @@ export function ImportDetailDialog({ open, onOpenChange, importId }: ImportDetai
                 <span className="font-medium">Import Date:</span>
                 <span>{format(importDetail.importDate, 'PPpp')}</span>
               </div>
+              {importDetail.file && (
+                <div className="flex justify-between">
+                  <span className="font-medium">File:</span>
+                  <span>{importDetail.file.filename}</span>
+                </div>
+              )}
             </CardContent>
           </Card>
 
@@ -200,16 +296,56 @@ export function ImportDetailDialog({ open, onOpenChange, importId }: ImportDetai
           <Card>
             <CardHeader>
               <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>
             </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>
               {importDetail.cintasSummaries.length === 0 ? (
                 <p className="text-muted-foreground">No summaries calculated yet</p>