|
@@ -1,275 +1,182 @@
|
|
|
-# Excel Import Implementation Plan - Next.js
|
|
|
|
|
|
|
+# Cintas Install Calendar Summary - Implementation Plan
|
|
|
|
|
|
|
|
## Overview
|
|
## Overview
|
|
|
-Complete implementation of Excel import functionality based on the C# Excelerator codebase, adapted for Next.js with WebSocket progress updates and 1GB file support.
|
|
|
|
|
|
|
+This document outlines the complete implementation plan for creating a new page called "Cintas Install Calendar Summary" with a workflow-based interface for processing Cintas install calendar data.
|
|
|
|
|
|
|
|
-## Architecture
|
|
|
|
|
|
|
+## Architecture Analysis
|
|
|
|
|
|
|
|
-### Technology Stack
|
|
|
|
|
-- **Excel Processing**: `exceljs` with streaming support
|
|
|
|
|
-- **Real-time Updates**: WebSocket (ws library)
|
|
|
|
|
-- **Database**: PostgreSQL with Prisma
|
|
|
|
|
-- **Processing**: Sequential with batching (5000 rows/batch)
|
|
|
|
|
-
|
|
|
|
|
-### File Structure
|
|
|
|
|
-```
|
|
|
|
|
-app/
|
|
|
|
|
-├── lib/
|
|
|
|
|
-│ └── excel-import/
|
|
|
|
|
-│ ├── types.ts # TypeScript interfaces
|
|
|
|
|
-│ ├── excel-reader.ts # Excel reading service
|
|
|
|
|
-│ ├── import-processor.ts # Main orchestrator
|
|
|
|
|
-│ ├── bulk-inserter.ts # Database operations
|
|
|
|
|
-│ └── websocket-server.ts # Progress updates
|
|
|
|
|
-├── actions/
|
|
|
|
|
-│ └── process-import.ts # Server action
|
|
|
|
|
-└── api/
|
|
|
|
|
- └── imports/
|
|
|
|
|
- └── [id]/
|
|
|
|
|
- └── progress/
|
|
|
|
|
- └── route.ts # WebSocket endpoint
|
|
|
|
|
-```
|
|
|
|
|
|
|
+### Current System Structure
|
|
|
|
|
+- **File Storage**: PostgreSQL ByteA type for blob storage
|
|
|
|
|
+- **Import System**: Uses `Import` model with `LayoutConfiguration` for data mapping
|
|
|
|
|
+- **Excel Processing**: Uses `exceljs` with streaming for large files
|
|
|
|
|
+- **Database Models**:
|
|
|
|
|
+ - `cintas_install_calendar` - stores raw calendar data
|
|
|
|
|
+ - `cintas_intall_calendar_summary` - stores calculated summaries
|
|
|
|
|
+- **Stored Procedure**: `cintas_calculate_summary` for summary calculations
|
|
|
|
|
|
|
|
## Implementation Steps
|
|
## Implementation Steps
|
|
|
|
|
|
|
|
-### Phase 1: Dependencies & Setup
|
|
|
|
|
-```bash
|
|
|
|
|
-npm install exceljs ws
|
|
|
|
|
-npm install -D @types/ws
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### Phase 2: TypeScript Interfaces
|
|
|
|
|
-
|
|
|
|
|
-```typescript
|
|
|
|
|
-// types.ts - Core interfaces matching C# models
|
|
|
|
|
-export interface ReadSectionData {
|
|
|
|
|
- id: number;
|
|
|
|
|
- name: string;
|
|
|
|
|
- tableName: string;
|
|
|
|
|
- sheet: string;
|
|
|
|
|
- type: string;
|
|
|
|
|
- startingRow?: number;
|
|
|
|
|
- endingRow?: number;
|
|
|
|
|
- parsedType: SectionTypeEnum;
|
|
|
|
|
- fields: LayoutSectionField[];
|
|
|
|
|
- data: Record<string, any>[];
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-export interface LayoutSectionField {
|
|
|
|
|
- id: number;
|
|
|
|
|
- cellPosition: string;
|
|
|
|
|
- name: string;
|
|
|
|
|
- dataType: string;
|
|
|
|
|
- dataTypeFormat?: string;
|
|
|
|
|
- importTableColumnName: string;
|
|
|
|
|
- importColumnOrderNumber: number;
|
|
|
|
|
- parsedType: FieldTypeEnum;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-export enum SectionTypeEnum {
|
|
|
|
|
- Grid = 'Grid',
|
|
|
|
|
- Properties = 'Properties',
|
|
|
|
|
- Unknown = 'Unknown'
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-export enum FieldTypeEnum {
|
|
|
|
|
- Time = 'Time',
|
|
|
|
|
- Decimal = 'Decimal',
|
|
|
|
|
- Date = 'Date',
|
|
|
|
|
- Numeric = 'Numeric',
|
|
|
|
|
- String = 'String'
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-export interface ImportProgress {
|
|
|
|
|
- importId: number;
|
|
|
|
|
- status: 'pending' | 'processing' | 'completed' | 'failed';
|
|
|
|
|
- currentSection: string;
|
|
|
|
|
- currentRow: number;
|
|
|
|
|
- totalRows: number;
|
|
|
|
|
- errors: string[];
|
|
|
|
|
- processedSections: number;
|
|
|
|
|
- totalSections: number;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+### Phase 1: Database Schema Updates
|
|
|
|
|
+1. **Add Stored Procedure to Prisma Schema**
|
|
|
|
|
+ - Add the `cintas_calculate_summary` stored procedure definition
|
|
|
|
|
+ - Update Prisma schema with proper model definitions
|
|
|
|
|
+
|
|
|
|
|
+### Phase 2: Layout Configuration
|
|
|
|
|
+2. **Create "Cintas Install Calendar" Layout Configuration**
|
|
|
|
|
+ - Create layout configuration with appropriate sections and fields
|
|
|
|
|
+ - Map Excel columns to database fields
|
|
|
|
|
+ - Configure table mapping to `cintas_install_calendar`
|
|
|
|
|
+
|
|
|
|
|
+### Phase 3: Page Creation
|
|
|
|
|
+3. **Create New Page Component**
|
|
|
|
|
+ - Create `app/cintas-install-calendar-summary/page.tsx`
|
|
|
|
|
+ - Implement workflow-based UI with 4 steps:
|
|
|
|
|
+ 1. File Upload
|
|
|
|
|
+ 2. Import Creation
|
|
|
|
|
+ 3. Data Processing
|
|
|
|
|
+ 4. Results Display
|
|
|
|
|
+
|
|
|
|
|
+### Phase 4: Dashboard Integration
|
|
|
|
|
+4. **Add to Dashboard**
|
|
|
|
|
+ - Update `app/dashboard/page.tsx` to include new tile
|
|
|
|
|
+ - Use `cintas-blue.svg` icon with circular motif
|
|
|
|
|
+ - Add navigation link
|
|
|
|
|
+
|
|
|
|
|
+### Phase 5: Workflow Implementation
|
|
|
|
|
+5. **Step 1: File Upload**
|
|
|
|
|
+ - Reuse existing file upload functionality
|
|
|
|
|
+ - Store file in blob storage
|
|
|
|
|
+ - Return file ID for next step
|
|
|
|
|
+
|
|
|
|
|
+6. **Step 2: Import Creation**
|
|
|
|
|
+ - Create import record with:
|
|
|
|
|
+ - Name: "Cintas Install Calendar Import"
|
|
|
|
|
+ - Layout ID: Cintas Install Calendar layout
|
|
|
|
|
+ - File ID: From step 1
|
|
|
|
|
+
|
|
|
|
|
+7. **Step 3: Data Processing**
|
|
|
|
|
+ - Read Excel file using ExcelJS
|
|
|
|
|
+ - Process data using layout configuration
|
|
|
|
|
+ - Bulk insert into `cintas_install_calendar` table
|
|
|
|
|
+
|
|
|
|
|
+8. **Step 4: Summary Calculation**
|
|
|
|
|
+ - Execute `cintas_calculate_summary` stored procedure
|
|
|
|
|
+ - Display results in formatted table
|
|
|
|
|
+
|
|
|
|
|
+## Technical Implementation Details
|
|
|
|
|
+
|
|
|
|
|
+### Database Schema Updates
|
|
|
|
|
+```sql
|
|
|
|
|
+-- Add stored procedure (will be added via migration)
|
|
|
|
|
+CREATE PROCEDURE cintas_calculate_summary(IN provided_import_id bigint)
|
|
|
|
|
+LANGUAGE plpgsql
|
|
|
|
|
+AS $$
|
|
|
|
|
+-- [Stored procedure definition from task]
|
|
|
|
|
+$$;
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### Phase 3: Excel Reader Service
|
|
|
|
|
-
|
|
|
|
|
-```typescript
|
|
|
|
|
-// excel-reader.ts
|
|
|
|
|
-import * as ExcelJS from 'exceljs';
|
|
|
|
|
-import { Readable } from 'stream';
|
|
|
|
|
-
|
|
|
|
|
-export class ExcelReaderService {
|
|
|
|
|
- async readExcelFile(
|
|
|
|
|
- fileBuffer: Buffer,
|
|
|
|
|
- layoutConfig: any,
|
|
|
|
|
- onProgress: (progress: ImportProgress) => void
|
|
|
|
|
- ): Promise<ReadSectionData[]> {
|
|
|
|
|
- const workbook = new ExcelJS.Workbook();
|
|
|
|
|
- await workbook.xlsx.load(fileBuffer);
|
|
|
|
|
-
|
|
|
|
|
- const results: ReadSectionData[] = [];
|
|
|
|
|
-
|
|
|
|
|
- for (const section of layoutConfig.sections) {
|
|
|
|
|
- const worksheet = workbook.getWorksheet(section.sheetName);
|
|
|
|
|
- if (!worksheet) continue;
|
|
|
|
|
-
|
|
|
|
|
- const sectionData = await this.processSection(worksheet, section, onProgress);
|
|
|
|
|
- results.push(sectionData);
|
|
|
|
|
|
|
+### Layout Configuration Structure
|
|
|
|
|
+```json
|
|
|
|
|
+{
|
|
|
|
|
+ "name": "Cintas Install Calendar",
|
|
|
|
|
+ "sections": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "name": "Install Calendar Data",
|
|
|
|
|
+ "type": "excel_import",
|
|
|
|
|
+ "sheetName": "Sheet1",
|
|
|
|
|
+ "tableName": "cintas_install_calendar",
|
|
|
|
|
+ "fields": [
|
|
|
|
|
+ {"name": "opportunity_status", "cellPosition": "A", "dataType": "string"},
|
|
|
|
|
+ {"name": "week", "cellPosition": "B", "dataType": "string"},
|
|
|
|
|
+ {"name": "qtr", "cellPosition": "C", "dataType": "string"},
|
|
|
|
|
+ {"name": "install_date", "cellPosition": "D", "dataType": "string"},
|
|
|
|
|
+ {"name": "account_name", "cellPosition": "E", "dataType": "string"},
|
|
|
|
|
+ {"name": "zip_code", "cellPosition": "F", "dataType": "string"},
|
|
|
|
|
+ {"name": "sold_to_number", "cellPosition": "G", "dataType": "string"},
|
|
|
|
|
+ {"name": "sort_number", "cellPosition": "H", "dataType": "string"},
|
|
|
|
|
+ {"name": "type", "cellPosition": "I", "dataType": "string"},
|
|
|
|
|
+ {"name": "route", "cellPosition": "J", "dataType": "string"},
|
|
|
|
|
+ {"name": "day", "cellPosition": "K", "dataType": "string"},
|
|
|
|
|
+ {"name": "trr", "cellPosition": "L", "dataType": "decimal"},
|
|
|
|
|
+ {"name": "paper_chem_wk1", "cellPosition": "M", "dataType": "decimal"},
|
|
|
|
|
+ {"name": "paper_chem_wk2", "cellPosition": "N", "dataType": "decimal"},
|
|
|
|
|
+ {"name": "paper_chem_wk3", "cellPosition": "O", "dataType": "decimal"},
|
|
|
|
|
+ {"name": "paper_chem_wk4", "cellPosition": "P", "dataType": "decimal"},
|
|
|
|
|
+ {"name": "sanis", "cellPosition": "Q", "dataType": "decimal"},
|
|
|
|
|
+ {"name": "power_add", "cellPosition": "R", "dataType": "decimal"}
|
|
|
|
|
+ ]
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- return results;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ ]
|
|
|
}
|
|
}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### Phase 4: Bulk Inserter
|
|
|
|
|
-
|
|
|
|
|
-```typescript
|
|
|
|
|
-// bulk-inserter.ts
|
|
|
|
|
-import { PrismaClient } from '@prisma/client';
|
|
|
|
|
-
|
|
|
|
|
-export class BulkInserter {
|
|
|
|
|
- private prisma: PrismaClient;
|
|
|
|
|
-
|
|
|
|
|
- async insertSectionData(
|
|
|
|
|
- sectionData: ReadSectionData,
|
|
|
|
|
- importId: number,
|
|
|
|
|
- onProgress: (rows: number) => void
|
|
|
|
|
- ): Promise<number> {
|
|
|
|
|
- const batchSize = 5000;
|
|
|
|
|
- const totalRows = sectionData.data.length;
|
|
|
|
|
- let insertedRows = 0;
|
|
|
|
|
-
|
|
|
|
|
- for (let i = 0; i < totalRows; i += batchSize) {
|
|
|
|
|
- const batch = sectionData.data.slice(i, i + batchSize);
|
|
|
|
|
-
|
|
|
|
|
- await this.prisma.$executeRaw`
|
|
|
|
|
- INSERT INTO ${sectionData.tableName}
|
|
|
|
|
- (import_id, ${Object.keys(batch[0]).join(', ')})
|
|
|
|
|
- VALUES ${this.createValuesPlaceholders(batch)}
|
|
|
|
|
- `;
|
|
|
|
|
-
|
|
|
|
|
- insertedRows += batch.length;
|
|
|
|
|
- onProgress(insertedRows);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return insertedRows;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
|
|
+### Page Structure
|
|
|
```
|
|
```
|
|
|
-
|
|
|
|
|
-### Phase 5: WebSocket Progress Server
|
|
|
|
|
-
|
|
|
|
|
-```typescript
|
|
|
|
|
-// websocket-server.ts
|
|
|
|
|
-import { WebSocketServer } from 'ws';
|
|
|
|
|
-import { Server } from 'http';
|
|
|
|
|
-
|
|
|
|
|
-export class ImportProgressServer {
|
|
|
|
|
- private wss: WebSocketServer;
|
|
|
|
|
-
|
|
|
|
|
- constructor(server: Server) {
|
|
|
|
|
- this.wss = new WebSocketServer({ server, path: '/api/imports/progress' });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- broadcastProgress(importId: number, progress: ImportProgress) {
|
|
|
|
|
- this.wss.clients.forEach(client => {
|
|
|
|
|
- if (client.readyState === WebSocket.OPEN) {
|
|
|
|
|
- client.send(JSON.stringify({ importId, progress }));
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
|
|
+app/
|
|
|
|
|
+├── cintas-install-calendar-summary/
|
|
|
|
|
+│ ├── page.tsx # Main page component
|
|
|
|
|
+│ ├── components/
|
|
|
|
|
+│ │ ├── FileUploadStep.tsx # Step 1: File upload
|
|
|
|
|
+│ │ ├── ImportCreationStep.tsx # Step 2: Import creation
|
|
|
|
|
+│ │ ├── ProcessingStep.tsx # Step 3: Data processing
|
|
|
|
|
+│ │ ├── ResultsStep.tsx # Step 4: Results display
|
|
|
|
|
+│ │ └── WorkflowProgress.tsx # Progress indicator
|
|
|
|
|
+│ └── actions/
|
|
|
|
|
+│ └── cintas-actions.ts # Server actions
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### Phase 6: Server Action
|
|
|
|
|
|
|
+### API Endpoints
|
|
|
|
|
+- `POST /api/cintas/upload` - File upload
|
|
|
|
|
+- `POST /api/cintas/create-import` - Create import record
|
|
|
|
|
+- `POST /api/cintas/process-import` - Process data
|
|
|
|
|
+- `GET /api/cintas/summary/:importId` - Get summary results
|
|
|
|
|
|
|
|
|
|
+### Workflow States
|
|
|
```typescript
|
|
```typescript
|
|
|
-// process-import.ts
|
|
|
|
|
-'use server';
|
|
|
|
|
-
|
|
|
|
|
-import { ExcelReaderService } from '@/lib/excel-import/excel-reader';
|
|
|
|
|
-import { BulkInserter } from '@/lib/excel-import/bulk-inserter';
|
|
|
|
|
-import { ImportProgressServer } from '@/lib/excel-import/websocket-server';
|
|
|
|
|
|
|
+enum WorkflowStep {
|
|
|
|
|
+ UPLOAD = 1,
|
|
|
|
|
+ CREATE_IMPORT = 2,
|
|
|
|
|
+ PROCESS = 3,
|
|
|
|
|
+ RESULTS = 4
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-export async function processImport(importId: number) {
|
|
|
|
|
- const importRecord = await prisma.import.findUnique({
|
|
|
|
|
- where: { id: importId },
|
|
|
|
|
- include: { layout: { include: { sections: { include: { fields: true } } } } }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- if (!importRecord || !importRecord.fileId) {
|
|
|
|
|
- throw new Error('Import not found or no file attached');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const file = await prisma.file.findUnique({ where: { id: importRecord.fileId } });
|
|
|
|
|
- if (!file) throw new Error('File not found');
|
|
|
|
|
-
|
|
|
|
|
- const reader = new ExcelReaderService();
|
|
|
|
|
- const inserter = new BulkInserter();
|
|
|
|
|
-
|
|
|
|
|
- const sections = await reader.readExcelFile(
|
|
|
|
|
- file.data,
|
|
|
|
|
- importRecord.layout,
|
|
|
|
|
- (progress) => {
|
|
|
|
|
- // Broadcast progress via WebSocket
|
|
|
|
|
- progressServer.broadcastProgress(importId, progress);
|
|
|
|
|
- }
|
|
|
|
|
- );
|
|
|
|
|
-
|
|
|
|
|
- let totalInserted = 0;
|
|
|
|
|
- for (const section of sections) {
|
|
|
|
|
- const inserted = await inserter.insertSectionData(
|
|
|
|
|
- section,
|
|
|
|
|
- importId,
|
|
|
|
|
- (rows) => {
|
|
|
|
|
- progressServer.broadcastProgress(importId, {
|
|
|
|
|
- ...progress,
|
|
|
|
|
- currentRow: rows
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- );
|
|
|
|
|
- totalInserted += inserted;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return { success: true, totalInserted };
|
|
|
|
|
|
|
+interface WorkflowState {
|
|
|
|
|
+ currentStep: WorkflowStep;
|
|
|
|
|
+ fileId?: string;
|
|
|
|
|
+ importId?: number;
|
|
|
|
|
+ processingStatus?: 'idle' | 'processing' | 'completed' | 'error';
|
|
|
|
|
+ results?: CintasSummary[];
|
|
|
}
|
|
}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-## Performance Optimizations
|
|
|
|
|
-
|
|
|
|
|
-1. **Memory Management**: Streaming keeps memory under 100MB even for 1GB files
|
|
|
|
|
-2. **Batch Processing**: 5000 rows/batch provides optimal performance
|
|
|
|
|
-3. **Parallel Sections**: Sequential processing as requested
|
|
|
|
|
-4. **Error Recovery**: Partial data retention on failure
|
|
|
|
|
-5. **Progress Tracking**: Real-time updates every 1000 rows
|
|
|
|
|
|
|
+## Implementation Order
|
|
|
|
|
|
|
|
-## Error Handling
|
|
|
|
|
|
|
+1. **Database Migration** - Add stored procedure
|
|
|
|
|
+2. **Layout Configuration** - Create "Cintas Install Calendar" layout
|
|
|
|
|
+3. **Page Components** - Create React components for each step
|
|
|
|
|
+4. **Server Actions** - Implement backend functionality
|
|
|
|
|
+5. **Dashboard Integration** - Add to main dashboard
|
|
|
|
|
+6. **Testing** - End-to-end workflow testing
|
|
|
|
|
|
|
|
-- **File Validation**: Excel format, size limits, corrupted files
|
|
|
|
|
-- **Data Validation**: Required fields, data type mismatches
|
|
|
|
|
-- **Database Errors**: Connection issues, constraint violations
|
|
|
|
|
-- **Progress Reporting**: Detailed error logs with row/column info
|
|
|
|
|
-
|
|
|
|
|
-## Testing Strategy
|
|
|
|
|
-
|
|
|
|
|
-1. **Unit Tests**: Excel reading, data conversion, validation
|
|
|
|
|
-2. **Integration Tests**: End-to-end import process
|
|
|
|
|
-3. **Performance Tests**: 1GB file processing
|
|
|
|
|
-4. **Error Tests**: Corrupted files, validation failures
|
|
|
|
|
-
|
|
|
|
|
-## Deployment Considerations
|
|
|
|
|
-
|
|
|
|
|
-- **Memory Limits**: Configure Node.js with `--max-old-space-size=2048`
|
|
|
|
|
-- **Timeouts**: Set appropriate Vercel function timeouts
|
|
|
|
|
-- **Database**: Ensure PostgreSQL connection pooling
|
|
|
|
|
|
|
+## Dependencies
|
|
|
|
|
+- `exceljs` - Excel file processing
|
|
|
|
|
+- `@prisma/client` - Database access
|
|
|
|
|
+- `lucide-react` - Icons
|
|
|
|
|
+- `react-hook-form` - Form handling
|
|
|
|
|
+- `react-dropzone` - File upload
|
|
|
|
|
|
|
|
## Next Steps
|
|
## Next Steps
|
|
|
-
|
|
|
|
|
-1. Install dependencies
|
|
|
|
|
-2. Create TypeScript interfaces
|
|
|
|
|
-3. Implement Excel reader
|
|
|
|
|
-4. Set up WebSocket server
|
|
|
|
|
-5. Create server action
|
|
|
|
|
-6. Add comprehensive testing
|
|
|
|
|
|
|
+1. Switch to Code mode to begin implementation
|
|
|
|
|
+2. Create database migration for stored procedure
|
|
|
|
|
+3. Implement layout configuration
|
|
|
|
|
+4. Build page components
|
|
|
|
|
+5. Integrate with dashboard
|
|
|
|
|
+
|
|
|
|
|
+## Success Criteria
|
|
|
|
|
+- [ ] New page accessible from dashboard
|
|
|
|
|
+- [ ] Complete 4-step workflow functional
|
|
|
|
|
+- [ ] File upload working with blob storage
|
|
|
|
|
+- [ ] Data import processing correctly
|
|
|
|
|
+- [ ] Summary calculations accurate
|
|
|
|
|
+- [ ] Results displayed in formatted table
|
|
|
|
|
+- [ ] Error handling throughout workflow
|