| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623 |
- "use client";
- import { useState, useEffect } from 'react';
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
- import { Button } from '@/components/ui/button';
- import { Upload, FileText, Database, BarChart3, CheckCircle, Loader2, History, PlusCircle } from 'lucide-react';
- import { UploadForm } from '@/app/components/uploadForm';
- import { createCintasImportRecord, processCintasImportData } from '@/app/actions/cintas-workflow';
- import { getImports } from '@/app/actions/imports';
- import { useKindeBrowserClient } from "@kinde-oss/kinde-auth-nextjs";
- interface FileData {
- id: string;
- filename: string;
- mimetype: string;
- size: number;
- createdAt: string;
- updatedAt: string;
- }
- interface CintasSummary {
- id: number;
- week: string;
- trrTotal: number;
- fourWkAverages: number;
- trrPlus4Wk: number;
- powerAdds: 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() {
- const { user } = useKindeBrowserClient();
- const [viewMode, setViewMode] = useState<'imports' | 'new-import' | 'summary'>('imports');
- const [currentStep, setCurrentStep] = useState(1);
- const [uploadedFile, setUploadedFile] = useState<FileData | null>(null);
- const [isProcessing, setIsProcessing] = useState(false);
- const [importRecord, setImportRecord] = useState<any>(null);
- const [summaryData, setSummaryData] = useState<CintasSummary[]>([]);
- const [error, setError] = useState<string | null>(null);
- const [summaryExists, setSummaryExists] = useState(false);
- const [priorImports, setPriorImports] = useState<ImportRecord[]>([]);
- const [selectedImport, setSelectedImport] = useState<ImportRecord | null>(null);
- const [loadingImports, setLoadingImports] = useState(true);
- useEffect(() => {
- if (user) {
- loadPriorImports();
- }
- }, [user]);
- const loadPriorImports = async () => {
- try {
- setLoadingImports(true);
- setError(null); // Clear any previous errors
- if (!user?.id) {
- // Don't set error here, just return - user might still be loading
- return;
- }
- const result = await getImports(user.id);
- 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) => {
- setUploadedFile(file);
- setCurrentStep(2);
- setError(null);
- };
- const handleCreateImportRecord = async () => {
- if (!uploadedFile) return;
- setIsProcessing(true);
- setError(null);
- try {
- const result = await createCintasImportRecord(uploadedFile.id, uploadedFile.filename);
- if (result.success) {
- setImportRecord(result.data);
- setCurrentStep(3);
- } else {
- setError(result.error || 'Failed to create import record');
- }
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Unknown error occurred');
- } finally {
- setIsProcessing(false);
- }
- };
- const handleProcessImportData = async () => {
- if (!importRecord) return;
- setIsProcessing(true);
- setError(null);
- try {
- const result = await processCintasImportData(importRecord.id);
- if (result.success) {
- setCurrentStep(4);
- } else {
- setError(result.error || 'Failed to process import data');
- }
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Unknown error occurred');
- } finally {
- setIsProcessing(false);
- }
- };
- const handleGenerateSummary = async () => {
- if (!importRecord) return;
- setIsProcessing(true);
- setError(null);
- try {
- const response = await fetch(`/api/imports/${importRecord.id}/summary`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- });
- const data = await response.json();
- if (response.ok) {
- // Handle both possible data structures from the API
- let summaries = [];
- if (data.summary && data.summary.cintasSummaries) {
- summaries = data.summary.cintasSummaries;
- } else if (Array.isArray(data.summary)) {
- summaries = data.summary;
- } else if (data.summary) {
- summaries = data.summary;
- }
- setSummaryData(Array.isArray(summaries) ? summaries : []);
- setSummaryExists(summaries.length > 0);
- if (data.summaryGenerated || summaries.length > 0) {
- setCurrentStep(4);
- setViewMode('summary');
- }
- } else {
- throw new Error(data.error || 'Failed to generate summary');
- }
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Failed to generate summary');
- } finally {
- setIsProcessing(false);
- }
- };
- 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);
- // Handle both possible data structures
- let summaries = [];
- if (data.summary?.cintasSummaries) {
- summaries = data.summary.cintasSummaries;
- } else if (Array.isArray(data.summary)) {
- summaries = data.summary;
- } else if (data.summary && data.summary.cintasSummaries) {
- summaries = data.summary.cintasSummaries;
- }
- setSummaryData(summaries);
- setSummaryExists(summaries.length > 0);
- 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 (
- <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="mb-8">
- <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">
- View prior imports or create new ones to process installation calendar data
- </p>
- </div>
- {error && (
- <div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
- <p className="text-sm text-red-800">{error}</p>
- </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>
- {/* 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">
- {user ? 'No prior imports found' : 'Please sign in to view imports'}
- </p>
- {user && (
- <Button onClick={handleStartNewImport}>
- Create First Import
- </Button>
- )}
- </div>
- ) : (
- <div className="space-y-4">
- {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>
- <h3 className="font-semibold">{importRecord.name}</h3>
- <p className="text-sm text-muted-foreground">
- {formatDate(importRecord.importDate)}
- </p>
- {importRecord.file && (
- <p className="text-sm text-muted-foreground">
- File: {importRecord.file.filename}
- </p>
- )}
- </div>
- </div>
- </div>
- ))}
- </div>
- )}
- </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)}`}
- >
- <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>
- <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>
- ) : 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>
- </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>
- );
- }
|