|
@@ -1,12 +1,12 @@
|
|
|
-
|
|
|
|
|
"use client";
|
|
"use client";
|
|
|
|
|
|
|
|
-import { useState } from 'react';
|
|
|
|
|
|
|
+import { useState, useEffect } from 'react';
|
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
|
import { Button } from '@/components/ui/button';
|
|
import { Button } from '@/components/ui/button';
|
|
|
-import { Upload, FileText, Database, BarChart3, CheckCircle, Loader2 } from 'lucide-react';
|
|
|
|
|
|
|
+import { Upload, FileText, Database, BarChart3, CheckCircle, Loader2, History, PlusCircle } from 'lucide-react';
|
|
|
import { UploadForm } from '@/app/components/uploadForm';
|
|
import { UploadForm } from '@/app/components/uploadForm';
|
|
|
import { createCintasImportRecord, processCintasImportData } from '@/app/actions/cintas-workflow';
|
|
import { createCintasImportRecord, processCintasImportData } from '@/app/actions/cintas-workflow';
|
|
|
|
|
+import { getImports } from '@/app/actions/imports';
|
|
|
|
|
|
|
|
interface FileData {
|
|
interface FileData {
|
|
|
id: string;
|
|
id: string;
|
|
@@ -27,7 +27,23 @@ interface CintasSummary {
|
|
|
weekId: number;
|
|
weekId: number;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+interface ImportRecord {
|
|
|
|
|
+ id: number;
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ importDate: string;
|
|
|
|
|
+ fileId: string | null;
|
|
|
|
|
+ file?: {
|
|
|
|
|
+ filename: string;
|
|
|
|
|
+ createdAt: string;
|
|
|
|
|
+ };
|
|
|
|
|
+ layout?: {
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ };
|
|
|
|
|
+ cintasSummaries?: CintasSummary[];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
export default function CintasCalendarSummaryPage() {
|
|
export default function CintasCalendarSummaryPage() {
|
|
|
|
|
+ const [viewMode, setViewMode] = useState<'imports' | 'new-import' | 'summary'>('imports');
|
|
|
const [currentStep, setCurrentStep] = useState(1);
|
|
const [currentStep, setCurrentStep] = useState(1);
|
|
|
const [uploadedFile, setUploadedFile] = useState<FileData | null>(null);
|
|
const [uploadedFile, setUploadedFile] = useState<FileData | null>(null);
|
|
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
@@ -35,6 +51,43 @@ export default function CintasCalendarSummaryPage() {
|
|
|
const [summaryData, setSummaryData] = useState<CintasSummary[]>([]);
|
|
const [summaryData, setSummaryData] = useState<CintasSummary[]>([]);
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
const [summaryExists, setSummaryExists] = useState(false);
|
|
const [summaryExists, setSummaryExists] = useState(false);
|
|
|
|
|
+ const [priorImports, setPriorImports] = useState<ImportRecord[]>([]);
|
|
|
|
|
+ const [selectedImport, setSelectedImport] = useState<ImportRecord | null>(null);
|
|
|
|
|
+ const [loadingImports, setLoadingImports] = useState(true);
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ loadPriorImports();
|
|
|
|
|
+ }, []);
|
|
|
|
|
+
|
|
|
|
|
+ const loadPriorImports = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ setLoadingImports(true);
|
|
|
|
|
+ const result = await getImports();
|
|
|
|
|
+ if (result.success && result.data) {
|
|
|
|
|
+ // Map the data to match our ImportRecord interface
|
|
|
|
|
+ const mappedData = result.data.map((item: any) => ({
|
|
|
|
|
+ id: item.id,
|
|
|
|
|
+ name: item.name,
|
|
|
|
|
+ importDate: item.importDate,
|
|
|
|
|
+ fileId: item.fileId,
|
|
|
|
|
+ file: item.file ? {
|
|
|
|
|
+ filename: item.file.filename,
|
|
|
|
|
+ createdAt: item.file.createdAt
|
|
|
|
|
+ } : undefined,
|
|
|
|
|
+ cintasSummaries: item.cintasSummaries || []
|
|
|
|
|
+ }));
|
|
|
|
|
+ setPriorImports(mappedData);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ setError(result.error || 'Failed to load prior imports');
|
|
|
|
|
+ setPriorImports([]);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ setError(err instanceof Error ? err.message : 'Failed to load prior imports');
|
|
|
|
|
+ setPriorImports([]);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setLoadingImports(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
const handleFileUploaded = (file: FileData) => {
|
|
const handleFileUploaded = (file: FileData) => {
|
|
|
setUploadedFile(file);
|
|
setUploadedFile(file);
|
|
@@ -92,7 +145,6 @@ export default function CintasCalendarSummaryPage() {
|
|
|
setError(null);
|
|
setError(null);
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- // Call the new POST endpoint to generate summary
|
|
|
|
|
const response = await fetch(`/api/imports/${importRecord.id}/summary`, {
|
|
const response = await fetch(`/api/imports/${importRecord.id}/summary`, {
|
|
|
method: 'POST',
|
|
method: 'POST',
|
|
|
headers: {
|
|
headers: {
|
|
@@ -106,9 +158,9 @@ export default function CintasCalendarSummaryPage() {
|
|
|
setSummaryData(data.summary || []);
|
|
setSummaryData(data.summary || []);
|
|
|
setSummaryExists(true);
|
|
setSummaryExists(true);
|
|
|
|
|
|
|
|
- // If summary was just generated, mark as completed
|
|
|
|
|
if (data.summaryGenerated) {
|
|
if (data.summaryGenerated) {
|
|
|
setCurrentStep(4);
|
|
setCurrentStep(4);
|
|
|
|
|
+ setViewMode('summary');
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
throw new Error(data.error || 'Failed to generate summary');
|
|
throw new Error(data.error || 'Failed to generate summary');
|
|
@@ -120,57 +172,63 @@ export default function CintasCalendarSummaryPage() {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const steps = [
|
|
|
|
|
- {
|
|
|
|
|
- id: 1,
|
|
|
|
|
- title: 'Upload Excel File',
|
|
|
|
|
- description: 'Upload the Cintas Install Calendar Excel file to blob storage',
|
|
|
|
|
- icon: Upload,
|
|
|
|
|
- status: currentStep >= 1 ? (uploadedFile ? 'completed' : 'pending') : 'pending',
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 2,
|
|
|
|
|
- title: 'Create Import Record',
|
|
|
|
|
- description: 'Create an import record with Cintas Install Calendar layout configuration',
|
|
|
|
|
- icon: FileText,
|
|
|
|
|
- status: currentStep >= 2 ? (importRecord ? 'completed' : 'pending') : 'disabled',
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 3,
|
|
|
|
|
- title: 'Import Data',
|
|
|
|
|
- description: 'Read the Excel file and import data into PostgreSQL database',
|
|
|
|
|
- icon: Database,
|
|
|
|
|
- status: currentStep > 3 ? 'completed' : (currentStep === 3 ? 'pending' : 'disabled'),
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 4,
|
|
|
|
|
- title: 'Generate Summary',
|
|
|
|
|
- description: 'Run summary calculations and display results',
|
|
|
|
|
- icon: BarChart3,
|
|
|
|
|
- status: currentStep >= 4 ? (summaryData.length > 0 ? 'completed' : 'pending') : 'disabled',
|
|
|
|
|
- },
|
|
|
|
|
- ];
|
|
|
|
|
-
|
|
|
|
|
- const getStepStatusColor = (status: string) => {
|
|
|
|
|
- switch (status) {
|
|
|
|
|
- case 'completed':
|
|
|
|
|
- return 'text-green-600 bg-green-50 border-green-200';
|
|
|
|
|
- case 'pending':
|
|
|
|
|
- return 'text-blue-600 bg-blue-50 border-blue-200';
|
|
|
|
|
- case 'disabled':
|
|
|
|
|
- return 'text-gray-400 bg-gray-50 border-gray-200';
|
|
|
|
|
- default:
|
|
|
|
|
- return 'text-gray-600 bg-gray-50 border-gray-200';
|
|
|
|
|
|
|
+ const handleLoadImportSummary = async (importRecord: ImportRecord) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ setIsProcessing(true);
|
|
|
|
|
+ setError(null);
|
|
|
|
|
+
|
|
|
|
|
+ const response = await fetch(`/api/imports/${importRecord.id}/summary`);
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ setSelectedImport(importRecord);
|
|
|
|
|
+ setSummaryData(data.summary?.cintasSummaries || []);
|
|
|
|
|
+ setSummaryExists(data.summaryExists);
|
|
|
|
|
+ setViewMode('summary');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ throw new Error(data.error || 'Failed to load summary');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ setError(err instanceof Error ? err.message : 'Failed to load summary');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setIsProcessing(false);
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ const handleStartNewImport = () => {
|
|
|
|
|
+ setViewMode('new-import');
|
|
|
|
|
+ setCurrentStep(1);
|
|
|
|
|
+ setUploadedFile(null);
|
|
|
|
|
+ setImportRecord(null);
|
|
|
|
|
+ setSummaryData([]);
|
|
|
|
|
+ setSummaryExists(false);
|
|
|
|
|
+ setError(null);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleBackToImports = () => {
|
|
|
|
|
+ setViewMode('imports');
|
|
|
|
|
+ setSelectedImport(null);
|
|
|
|
|
+ setSummaryData([]);
|
|
|
|
|
+ loadPriorImports();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const formatDate = (dateString: string) => {
|
|
|
|
|
+ return new Date(dateString).toLocaleDateString('en-US', {
|
|
|
|
|
+ year: 'numeric',
|
|
|
|
|
+ month: 'short',
|
|
|
|
|
+ day: 'numeric',
|
|
|
|
|
+ hour: '2-digit',
|
|
|
|
|
+ minute: '2-digit'
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
|
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
|
|
<div className="container mx-auto py-6 px-4 max-w-6xl">
|
|
<div className="container mx-auto py-6 px-4 max-w-6xl">
|
|
|
<div className="mb-8">
|
|
<div className="mb-8">
|
|
|
<h1 className="text-3xl font-bold tracking-tight text-gray-900 dark:text-white">Cintas Install Calendar Summary</h1>
|
|
<h1 className="text-3xl font-bold tracking-tight text-gray-900 dark:text-white">Cintas Install Calendar Summary</h1>
|
|
|
<p className="text-muted-foreground dark:text-gray-300">
|
|
<p className="text-muted-foreground dark:text-gray-300">
|
|
|
- Follow the workflow steps to upload and process the installation calendar data
|
|
|
|
|
|
|
+ View prior imports or create new ones to process installation calendar data
|
|
|
</p>
|
|
</p>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -180,218 +238,353 @@ export default function CintasCalendarSummaryPage() {
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
- {/* Workflow Steps */}
|
|
|
|
|
- <div className="mb-8">
|
|
|
|
|
- <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
|
|
|
- {steps.map((step) => {
|
|
|
|
|
- const Icon = step.icon;
|
|
|
|
|
- return (
|
|
|
|
|
- <Card
|
|
|
|
|
- key={step.id}
|
|
|
|
|
- className={`border-2 ${getStepStatusColor(step.status)}`}
|
|
|
|
|
- >
|
|
|
|
|
- <CardHeader className="pb-3">
|
|
|
|
|
- <div className="flex items-center space-x-2">
|
|
|
|
|
- <Icon className="h-5 w-5" />
|
|
|
|
|
- <div>
|
|
|
|
|
- <CardTitle className="text-sm font-medium">
|
|
|
|
|
- Step {step.id}: {step.title}
|
|
|
|
|
- </CardTitle>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </CardHeader>
|
|
|
|
|
- <CardContent>
|
|
|
|
|
- <CardDescription className="text-xs">
|
|
|
|
|
- {step.description}
|
|
|
|
|
- </CardDescription>
|
|
|
|
|
- </CardContent>
|
|
|
|
|
- </Card>
|
|
|
|
|
- );
|
|
|
|
|
- })}
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ {/* Navigation Buttons */}
|
|
|
|
|
+ <div className="mb-6 flex gap-4">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant={viewMode === 'imports' ? 'default' : 'outline'}
|
|
|
|
|
+ onClick={handleBackToImports}
|
|
|
|
|
+ className="flex items-center gap-2"
|
|
|
|
|
+ >
|
|
|
|
|
+ <History className="h-4 w-4" />
|
|
|
|
|
+ Prior Imports
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant={viewMode === 'new-import' ? 'default' : 'outline'}
|
|
|
|
|
+ onClick={handleStartNewImport}
|
|
|
|
|
+ className="flex items-center gap-2"
|
|
|
|
|
+ >
|
|
|
|
|
+ <PlusCircle className="h-4 w-4" />
|
|
|
|
|
+ New Import
|
|
|
|
|
+ </Button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- {/* Step Content */}
|
|
|
|
|
- <div className="grid gap-6">
|
|
|
|
|
- {currentStep === 1 && (
|
|
|
|
|
- <Card>
|
|
|
|
|
- <CardHeader>
|
|
|
|
|
- <CardTitle>Step 1: Upload Excel File</CardTitle>
|
|
|
|
|
- <CardDescription>
|
|
|
|
|
- Upload your Cintas Install Calendar Excel file to begin processing
|
|
|
|
|
- </CardDescription>
|
|
|
|
|
- </CardHeader>
|
|
|
|
|
- <CardContent>
|
|
|
|
|
|
|
+ {/* Prior Imports View */}
|
|
|
|
|
+ {viewMode === 'imports' && (
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <CardTitle>Prior Imports</CardTitle>
|
|
|
|
|
+ <CardDescription>
|
|
|
|
|
+ Select an import to view its summary results
|
|
|
|
|
+ </CardDescription>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ {loadingImports ? (
|
|
|
|
|
+ <div className="flex justify-center py-8">
|
|
|
|
|
+ <Loader2 className="h-8 w-8 animate-spin" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : priorImports.length === 0 ? (
|
|
|
|
|
+ <div className="text-center py-8">
|
|
|
|
|
+ <p className="text-muted-foreground mb-4">No prior imports found</p>
|
|
|
|
|
+ <Button onClick={handleStartNewImport}>
|
|
|
|
|
+ Create First Import
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : (
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-4">
|
|
|
- <UploadForm onFileUploaded={handleFileUploaded} />
|
|
|
|
|
-
|
|
|
|
|
- {uploadedFile && (
|
|
|
|
|
- <div className="mt-4 p-4 bg-green-50 border border-green-200 rounded-lg">
|
|
|
|
|
- <div className="flex items-center space-x-2">
|
|
|
|
|
- <CheckCircle className="h-5 w-5 text-green-600" />
|
|
|
|
|
|
|
+ {priorImports.map((importRecord) => (
|
|
|
|
|
+ <div
|
|
|
|
|
+ key={importRecord.id}
|
|
|
|
|
+ className="border rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer"
|
|
|
|
|
+ onClick={() => handleLoadImportSummary(importRecord)}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div className="flex justify-between items-start">
|
|
|
<div>
|
|
<div>
|
|
|
- <p className="text-sm font-medium text-green-800">File uploaded successfully!</p>
|
|
|
|
|
- <p className="text-sm text-green-600">
|
|
|
|
|
- {uploadedFile.filename} ({(uploadedFile.size / 1024 / 1024).toFixed(2)} MB)
|
|
|
|
|
|
|
+ <h3 className="font-semibold">{importRecord.name}</h3>
|
|
|
|
|
+ <p className="text-sm text-muted-foreground">
|
|
|
|
|
+ {formatDate(importRecord.importDate)}
|
|
|
</p>
|
|
</p>
|
|
|
|
|
+ {importRecord.file && (
|
|
|
|
|
+ <p className="text-sm text-muted-foreground">
|
|
|
|
|
+ File: {importRecord.file.filename}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="text-right">
|
|
|
|
|
+ <span className="text-sm text-muted-foreground">
|
|
|
|
|
+ {importRecord.cintasSummaries?.length || 0} weeks
|
|
|
|
|
+ </span>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- )}
|
|
|
|
|
|
|
+ ))}
|
|
|
</div>
|
|
</div>
|
|
|
- </CardContent>
|
|
|
|
|
- </Card>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {currentStep === 2 && (
|
|
|
|
|
- <Card>
|
|
|
|
|
- <CardHeader>
|
|
|
|
|
- <CardTitle>Step 2: Create Import Record</CardTitle>
|
|
|
|
|
- <CardDescription>
|
|
|
|
|
- Creating import record with Cintas Install Calendar layout configuration
|
|
|
|
|
- </CardDescription>
|
|
|
|
|
- </CardHeader>
|
|
|
|
|
- <CardContent>
|
|
|
|
|
- <div className="space-y-4">
|
|
|
|
|
- <p className="text-sm text-muted-foreground">
|
|
|
|
|
- File: {uploadedFile?.filename}
|
|
|
|
|
- </p>
|
|
|
|
|
- <Button
|
|
|
|
|
- onClick={handleCreateImportRecord}
|
|
|
|
|
- disabled={isProcessing}
|
|
|
|
|
- className="w-full"
|
|
|
|
|
|
|
+ )}
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* New Import Workflow */}
|
|
|
|
|
+ {viewMode === 'new-import' && (
|
|
|
|
|
+ <div className="space-y-6">
|
|
|
|
|
+ {/* Workflow Steps */}
|
|
|
|
|
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
|
|
|
+ {[
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 1,
|
|
|
|
|
+ title: 'Upload Excel File',
|
|
|
|
|
+ description: 'Upload the Cintas Install Calendar Excel file',
|
|
|
|
|
+ icon: Upload,
|
|
|
|
|
+ status: currentStep >= 1 ? (uploadedFile ? 'completed' : 'pending') : 'pending',
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 2,
|
|
|
|
|
+ title: 'Create Import Record',
|
|
|
|
|
+ description: 'Create import record with layout configuration',
|
|
|
|
|
+ icon: FileText,
|
|
|
|
|
+ status: currentStep >= 2 ? (importRecord ? 'completed' : 'pending') : 'disabled',
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 3,
|
|
|
|
|
+ title: 'Import Data',
|
|
|
|
|
+ description: 'Process Excel file and import data',
|
|
|
|
|
+ icon: Database,
|
|
|
|
|
+ status: currentStep > 3 ? 'completed' : (currentStep === 3 ? 'pending' : 'disabled'),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 4,
|
|
|
|
|
+ title: 'Generate Summary',
|
|
|
|
|
+ description: 'Run summary calculations and display results',
|
|
|
|
|
+ icon: BarChart3,
|
|
|
|
|
+ status: currentStep >= 4 ? (summaryData.length > 0 ? 'completed' : 'pending') : 'disabled',
|
|
|
|
|
+ },
|
|
|
|
|
+ ].map((step) => {
|
|
|
|
|
+ const Icon = step.icon;
|
|
|
|
|
+ const getStatusColor = (status: string) => {
|
|
|
|
|
+ switch (status) {
|
|
|
|
|
+ case 'completed':
|
|
|
|
|
+ return 'text-green-600 bg-green-50 border-green-200';
|
|
|
|
|
+ case 'pending':
|
|
|
|
|
+ return 'text-blue-600 bg-blue-50 border-blue-200';
|
|
|
|
|
+ case 'disabled':
|
|
|
|
|
+ return 'text-gray-400 bg-gray-50 border-gray-200';
|
|
|
|
|
+ default:
|
|
|
|
|
+ return 'text-gray-600 bg-gray-50 border-gray-200';
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Card
|
|
|
|
|
+ key={step.id}
|
|
|
|
|
+ className={`border-2 ${getStatusColor(step.status)}`}
|
|
|
>
|
|
>
|
|
|
- {isProcessing ? (
|
|
|
|
|
- <>
|
|
|
|
|
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
|
|
|
- Creating Import Record...
|
|
|
|
|
- </>
|
|
|
|
|
- ) : (
|
|
|
|
|
- 'Create Import Record'
|
|
|
|
|
- )}
|
|
|
|
|
- </Button>
|
|
|
|
|
|
|
+ <CardHeader className="pb-3">
|
|
|
|
|
+ <div className="flex items-center space-x-2">
|
|
|
|
|
+ <Icon className="h-5 w-5" />
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <CardTitle className="text-sm font-medium">
|
|
|
|
|
+ Step {step.id}: {step.title}
|
|
|
|
|
+ </CardTitle>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ <CardDescription className="text-xs">
|
|
|
|
|
+ {step.description}
|
|
|
|
|
+ </CardDescription>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ );
|
|
|
|
|
+ })}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Step Content */}
|
|
|
|
|
+ <div className="space-y-6">
|
|
|
|
|
+ {currentStep === 1 && (
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <CardTitle>Step 1: Upload Excel File</CardTitle>
|
|
|
|
|
+ <CardDescription>
|
|
|
|
|
+ Upload your Cintas Install Calendar Excel file to begin processing
|
|
|
|
|
+ </CardDescription>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ <div className="space-y-4">
|
|
|
|
|
+ <UploadForm onFileUploaded={handleFileUploaded} />
|
|
|
|
|
+ {uploadedFile && (
|
|
|
|
|
+ <div className="mt-4 p-4 bg-green-50 border border-green-200 rounded-lg">
|
|
|
|
|
+ <div className="flex items-center space-x-2">
|
|
|
|
|
+ <CheckCircle className="h-5 w-5 text-green-600" />
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p className="text-sm font-medium text-green-800">File uploaded successfully!</p>
|
|
|
|
|
+ <p className="text-sm text-green-600">
|
|
|
|
|
+ {uploadedFile.filename} ({(uploadedFile.size / 1024 / 1024).toFixed(2)} MB)
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {currentStep === 2 && (
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <CardTitle>Step 2: Create Import Record</CardTitle>
|
|
|
|
|
+ <CardDescription>
|
|
|
|
|
+ Creating import record with Cintas Install Calendar layout configuration
|
|
|
|
|
+ </CardDescription>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ <div className="space-y-4">
|
|
|
|
|
+ <p className="text-sm text-muted-foreground">
|
|
|
|
|
+ File: {uploadedFile?.filename}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ onClick={handleCreateImportRecord}
|
|
|
|
|
+ disabled={isProcessing}
|
|
|
|
|
+ className="w-full"
|
|
|
|
|
+ >
|
|
|
|
|
+ {isProcessing ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
|
|
|
+ Creating Import Record...
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ 'Create Import Record'
|
|
|
|
|
+ )}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {currentStep === 3 && (
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <CardTitle>Step 3: Import Data</CardTitle>
|
|
|
|
|
+ <CardDescription>
|
|
|
|
|
+ Processing Excel file and importing data into database
|
|
|
|
|
+ </CardDescription>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ <div className="space-y-4">
|
|
|
|
|
+ <p className="text-sm text-muted-foreground">
|
|
|
|
|
+ Import ID: {importRecord?.id}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ onClick={handleProcessImportData}
|
|
|
|
|
+ disabled={isProcessing}
|
|
|
|
|
+ className="w-full"
|
|
|
|
|
+ >
|
|
|
|
|
+ {isProcessing ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
|
|
|
+ Processing Import...
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ 'Process Import'
|
|
|
|
|
+ )}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {currentStep === 4 && (
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <CardTitle>Step 4: Generate Summary</CardTitle>
|
|
|
|
|
+ <CardDescription>
|
|
|
|
|
+ Running summary calculations and displaying results
|
|
|
|
|
+ </CardDescription>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ <div className="space-y-4">
|
|
|
|
|
+ <p className="text-sm text-muted-foreground">
|
|
|
|
|
+ Import ID: {importRecord?.id}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ onClick={handleGenerateSummary}
|
|
|
|
|
+ disabled={isProcessing}
|
|
|
|
|
+ className="w-full"
|
|
|
|
|
+ >
|
|
|
|
|
+ {isProcessing ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
|
|
|
+ {summaryExists ? 'Loading Summary...' : 'Generating Summary...'}
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ summaryExists ? 'Load Summary' : 'Generate Summary'
|
|
|
|
|
+ )}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* Summary Results View */}
|
|
|
|
|
+ {viewMode === 'summary' && selectedImport && (
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <div className="flex justify-between items-start">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <CardTitle>Summary Results</CardTitle>
|
|
|
|
|
+ <CardDescription>
|
|
|
|
|
+ {selectedImport.name} - {formatDate(selectedImport.importDate)}
|
|
|
|
|
+ </CardDescription>
|
|
|
</div>
|
|
</div>
|
|
|
- </CardContent>
|
|
|
|
|
- </Card>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {currentStep === 3 && (
|
|
|
|
|
- <Card>
|
|
|
|
|
- <CardHeader>
|
|
|
|
|
- <CardTitle>Step 3: Import Data</CardTitle>
|
|
|
|
|
- <CardDescription>
|
|
|
|
|
- Processing Excel file and importing data into database
|
|
|
|
|
- </CardDescription>
|
|
|
|
|
- </CardHeader>
|
|
|
|
|
- <CardContent>
|
|
|
|
|
- <div className="space-y-4">
|
|
|
|
|
- <p className="text-sm text-muted-foreground">
|
|
|
|
|
- Import ID: {importRecord?.id}
|
|
|
|
|
- </p>
|
|
|
|
|
- <Button
|
|
|
|
|
- onClick={handleProcessImportData}
|
|
|
|
|
- disabled={isProcessing}
|
|
|
|
|
- className="w-full"
|
|
|
|
|
- >
|
|
|
|
|
- {isProcessing ? (
|
|
|
|
|
- <>
|
|
|
|
|
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
|
|
|
- Processing Import...
|
|
|
|
|
- </>
|
|
|
|
|
- ) : (
|
|
|
|
|
- 'Process Import'
|
|
|
|
|
- )}
|
|
|
|
|
- </Button>
|
|
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ onClick={handleBackToImports}
|
|
|
|
|
+ className="flex items-center gap-2"
|
|
|
|
|
+ >
|
|
|
|
|
+ <History className="h-4 w-4" />
|
|
|
|
|
+ Back to Imports
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ {isProcessing ? (
|
|
|
|
|
+ <div className="flex justify-center py-8">
|
|
|
|
|
+ <Loader2 className="h-8 w-8 animate-spin" />
|
|
|
</div>
|
|
</div>
|
|
|
- </CardContent>
|
|
|
|
|
- </Card>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {currentStep === 4 && (
|
|
|
|
|
- <Card>
|
|
|
|
|
- <CardHeader>
|
|
|
|
|
- <CardTitle>Step 4: Generate Summary</CardTitle>
|
|
|
|
|
- <CardDescription>
|
|
|
|
|
- Running summary calculations and displaying results
|
|
|
|
|
- </CardDescription>
|
|
|
|
|
- </CardHeader>
|
|
|
|
|
- <CardContent>
|
|
|
|
|
- <div className="space-y-4">
|
|
|
|
|
- <p className="text-sm text-muted-foreground">
|
|
|
|
|
- Import ID: {importRecord?.id}
|
|
|
|
|
- </p>
|
|
|
|
|
- <Button
|
|
|
|
|
- onClick={handleGenerateSummary}
|
|
|
|
|
- disabled={isProcessing}
|
|
|
|
|
- className="w-full"
|
|
|
|
|
- >
|
|
|
|
|
- {isProcessing ? (
|
|
|
|
|
- <>
|
|
|
|
|
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
|
|
|
- {summaryExists ? 'Loading Summary...' : 'Generating Summary...'}
|
|
|
|
|
- </>
|
|
|
|
|
- ) : (
|
|
|
|
|
- summaryExists ? 'Load Summary' : 'Generate Summary'
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ ) : summaryData.length === 0 ? (
|
|
|
|
|
+ <div className="text-center py-8">
|
|
|
|
|
+ <p className="text-muted-foreground mb-4">No summary data available</p>
|
|
|
|
|
+ <Button onClick={() => handleGenerateSummary()}>
|
|
|
|
|
+ Generate Summary
|
|
|
</Button>
|
|
</Button>
|
|
|
-
|
|
|
|
|
- {summaryData.length > 0 && (
|
|
|
|
|
- <div className="mt-6">
|
|
|
|
|
- <h3 className="text-lg font-semibold mb-4">Summary Results</h3>
|
|
|
|
|
- <div className="overflow-x-auto">
|
|
|
|
|
- <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
|
|
|
- <thead className="bg-gray-50 dark:bg-gray-800">
|
|
|
|
|
- <tr>
|
|
|
|
|
- <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Week</th>
|
|
|
|
|
- <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">TRR Total</th>
|
|
|
|
|
- <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">4 Week Avg</th>
|
|
|
|
|
- <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">TRR + 4Wk</th>
|
|
|
|
|
- <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Power Adds</th>
|
|
|
|
|
- </tr>
|
|
|
|
|
- </thead>
|
|
|
|
|
- <tbody className="bg-white divide-y divide-gray-200 dark:bg-gray-800 dark:divide-gray-700">
|
|
|
|
|
- {summaryData.map((item, index) => (
|
|
|
|
|
- <tr
|
|
|
|
|
- key={`${item.id}-${index}`}
|
|
|
|
|
- className={index % 2 === 0
|
|
|
|
|
- ? 'bg-white dark:bg-gray-800'
|
|
|
|
|
- : 'bg-gray-50 dark:bg-gray-700/50'
|
|
|
|
|
- }
|
|
|
|
|
- >
|
|
|
|
|
- <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{item.week}</td>
|
|
|
|
|
- <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{item.trrTotal}</td>
|
|
|
|
|
- <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{item.fourWkAverages}</td>
|
|
|
|
|
- <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{item.trrPlus4Wk}</td>
|
|
|
|
|
- <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{item.powerAdds}</td>
|
|
|
|
|
- </tr>
|
|
|
|
|
- ))}
|
|
|
|
|
- </tbody>
|
|
|
|
|
- </table>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
</div>
|
|
</div>
|
|
|
- </CardContent>
|
|
|
|
|
- </Card>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {/* Navigation */}
|
|
|
|
|
- <div className="flex justify-between">
|
|
|
|
|
- <Button
|
|
|
|
|
- variant="outline"
|
|
|
|
|
- onClick={() => setCurrentStep(Math.max(1, currentStep - 1))}
|
|
|
|
|
- disabled={currentStep === 1}
|
|
|
|
|
- >
|
|
|
|
|
- Previous
|
|
|
|
|
- </Button>
|
|
|
|
|
- <Button
|
|
|
|
|
- onClick={() => setCurrentStep(Math.min(4, currentStep + 1))}
|
|
|
|
|
- disabled={currentStep === 4 || (currentStep === 1 && !uploadedFile)}
|
|
|
|
|
- >
|
|
|
|
|
- {currentStep === 4 ? 'Complete' : 'Next'}
|
|
|
|
|
- </Button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <div className="overflow-x-auto">
|
|
|
|
|
+ <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
|
|
|
+ <thead className="bg-gray-50 dark:bg-gray-800">
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Week</th>
|
|
|
|
|
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">TRR Total</th>
|
|
|
|
|
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">4 Week Avg</th>
|
|
|
|
|
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">TRR + 4Wk</th>
|
|
|
|
|
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Power Adds</th>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </thead>
|
|
|
|
|
+ <tbody className="bg-white divide-y divide-gray-200 dark:bg-gray-800 dark:divide-gray-700">
|
|
|
|
|
+ {summaryData.map((item, index) => (
|
|
|
|
|
+ <tr
|
|
|
|
|
+ key={`${item.id}-${index}`}
|
|
|
|
|
+ className={index % 2 === 0
|
|
|
|
|
+ ? 'bg-white dark:bg-gray-800'
|
|
|
|
|
+ : 'bg-gray-50 dark:bg-gray-700/50'
|
|
|
|
|
+ }
|
|
|
|
|
+ >
|
|
|
|
|
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{item.week}</td>
|
|
|
|
|
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{item.trrTotal}</td>
|
|
|
|
|
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{item.fourWkAverages}</td>
|
|
|
|
|
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{item.trrPlus4Wk}</td>
|
|
|
|
|
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{item.powerAdds}</td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </tbody>
|
|
|
|
|
+ </table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|