Bladeren bron

docs: update implementation plan for Cintas Install Calendar Summary feature

vtugulan 6 maanden geleden
bovenliggende
commit
b4803eacbd
2 gewijzigde bestanden met toevoegingen van 323 en 249 verwijderingen
  1. 156 249
      IMPLEMENTATION_PLAN.md
  2. 167 0
      public/Cintas-Blue.svg

+ 156 - 249
IMPLEMENTATION_PLAN.md

@@ -1,275 +1,182 @@
-# Excel Import Implementation Plan - Next.js
+# Cintas Install Calendar Summary - Implementation Plan
 
 ## 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
 
-### 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
-// 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
-
-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

+ 167 - 0
public/Cintas-Blue.svg

@@ -0,0 +1,167 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+
+
+<svg
+   version="1.0"
+   width="513.68011"
+   height="194.72844"
+   id="svg3260"
+   sodipodi:docname="Cintas_logo.svg"
+   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview17"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     showgrid="false"
+     inkscape:zoom="2.4996101"
+     inkscape:cx="146.62287"
+     inkscape:cy="103.01607"
+     inkscape:window-width="1920"
+     inkscape:window-height="1044"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1" />
+  <defs
+     id="defs3262">
+    <inkscape:path-effect
+       effect="powerclip"
+       id="path-effect1233"
+       is_visible="true"
+       lpeversion="1"
+       inverse="true"
+       flatten="false"
+       hide_clip="false"
+       message="Use fill-rule evenodd on &lt;b&gt;fill and stroke&lt;/b&gt; dialog if no flatten result after convert clip to paths." />
+    <inkscape:path-effect
+       effect="powerclip"
+       id="path-effect1218"
+       is_visible="true"
+       lpeversion="1"
+       inverse="true"
+       flatten="false"
+       hide_clip="false"
+       message="Use fill-rule evenodd on &lt;b&gt;fill and stroke&lt;/b&gt; dialog if no flatten result after convert clip to paths." />
+    <filter
+       id="mask-powermask-path-effect1134_inverse"
+       inkscape:label="filtermask-powermask-path-effect1134"
+       style="color-interpolation-filters:sRGB"
+       height="100"
+       width="100"
+       x="-50"
+       y="-50">
+      <feColorMatrix
+         id="mask-powermask-path-effect1134_primitive1"
+         values="1"
+         type="saturate"
+         result="fbSourceGraphic" />
+      <feColorMatrix
+         id="mask-powermask-path-effect1134_primitive2"
+         values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0 "
+         in="fbSourceGraphic" />
+    </filter>
+    <clipPath
+       clipPathUnits="userSpaceOnUse"
+       id="clipPath1214">
+      <path
+         d="m 182.92662,559.07467 a 39.106595,41.847119 0 1 1 -78.21319,0 39.106595,41.847119 0 1 1 78.21319,0 z"
+         id="path1216"
+         style="display:none;fill:#000000;fill-opacity:1;stroke:#ff0000;stroke-width:0;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
+      <path
+         id="lpe_path-effect1218"
+         style="fill:#000000;fill-opacity:1;stroke:#ff0000;stroke-width:0;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
+         class="powerclip"
+         d="M 61.074241,475.21934 H 211.73228 V 640.39369 H 61.074241 Z m 121.852379,83.85533 a 39.106595,41.847119 0 1 0 -78.21319,0 39.106595,41.847119 0 1 0 78.21319,0 z" />
+    </clipPath>
+    <clipPath
+       clipPathUnits="userSpaceOnUse"
+       id="clipPath1229">
+      <rect
+         width="73.013077"
+         height="124.12222"
+         x="152.65424"
+         y="475.79471"
+         id="rect1231"
+         style="display:none;fill:#000000;fill-opacity:1;stroke:#ff0000;stroke-width:0;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
+      <path
+         id="lpe_path-effect1233"
+         style="fill:#000000;fill-opacity:1;stroke:#ff0000;stroke-width:0;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
+         class="powerclip"
+         d="M 54.375895,465.13206 H 191.30522 V 599.22197 H 54.375895 Z m 98.278345,10.66265 v 124.12222 h 73.01307 V 475.79471 Z" />
+    </clipPath>
+  </defs>
+  <g
+     transform="translate(-43.503162,-420.53055)"
+     id="layer1">
+    <g
+       id="g1223"
+       clip-path="url(#clipPath1229)"
+       inkscape:path-effect="#path-effect1233">
+      <path
+         d="m 206.73228,557.80652 a 70.329019,77.587174 0 1 1 -140.658038,0 70.329019,77.587174 0 1 1 140.658038,0 z"
+         transform="matrix(0.9023965,0,0,0.7996806,-0.2492693,86.109964)"
+         id="path3295"
+         style="fill:#003ec7;fill-opacity:1;stroke:#ff0000;stroke-width:0;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
+         clip-path="url(#clipPath1214)"
+         inkscape:path-effect="#path-effect1218"
+         inkscape:original-d="m 206.73228,557.80652 a 70.329019,77.587174 0 1 1 -140.658038,0 70.329019,77.587174 0 1 1 140.658038,0 z" />
+    </g>
+    <rect
+       width="33.261509"
+       height="118.44343"
+       ry="0"
+       x="159.54991"
+       y="473.36096"
+       id="rect3283"
+       style="fill:#003ec7;fill-opacity:1;stroke:#ff0000;stroke-width:0;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
+    <path
+       d="m 232.76153,461.94879 a 23.526435,23.776717 0 1 1 -47.05287,0 23.526435,23.776717 0 1 1 47.05287,0 z"
+       transform="matrix(0.8189655,0,0,0.8103448,6.2440348,79.755793)"
+       id="path3285"
+       style="fill:#003ec7;fill-opacity:1;stroke:#ff0000;stroke-width:0;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
+    <rect
+       width="30.82774"
+       height="118.0378"
+       x="198.08458"
+       y="474.17221"
+       id="rect3287"
+       style="fill:#003ec7;fill-opacity:1;stroke:#ff0000;stroke-width:0;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
+    <rect
+       width="30.82774"
+       height="118.0378"
+       x="282.04962"
+       y="473.96939"
+       id="rect3289"
+       style="fill:#003ec7;fill-opacity:1;stroke:#ff0000;stroke-width:0;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
+    <rect
+       width="100.19016"
+       height="28.799597"
+       x="286.10593"
+       y="473.96942"
+       id="rect3291"
+       style="fill:#003ec7;fill-opacity:1;stroke:#ff0000;stroke-width:0;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
+    <rect
+       width="33.261509"
+       height="117.22654"
+       x="328.29123"
+       y="474.37503"
+       id="rect3293"
+       style="fill:#003ec7;fill-opacity:1;stroke:#ff0000;stroke-width:0;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
+    <path
+       d="m 229.31796,474.17221 55.16542,73.01306 -2.43376,45.02473 -55.57106,-73.82432 -18.25327,-36.91217 z"
+       id="path3811"
+       style="fill:#003ec7;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    <path
+       d="m 360.33586,581.66367 58.81608,-150.89368 55.97669,126.96162 c 9.73099,13.45118 29.8371,9.36027 31.639,1.21688 4.73233,-24.87852 -45.549,-5.89797 -49.08101,-46.64723 0.18749,-41.1727 49.25887,-48.73754 66.92865,-34.4784 v 27.17709 c -8.44881,-7.19547 -24.42744,-12.52842 -29.61086,-1.21689 -6.83898,12.02285 20.19147,11.59948 32.04463,19.47015 24.7997,18.89879 17.72151,55.79833 -11.76322,70.57931 -30.18277,11.38449 -58.94574,-5.17506 -67.33428,-20.28141 l -24.33769,-63.278 -13.38573,34.88403 h 21.4983 l 9.73507,24.33769 -38.9403,1.21689 -8.92382,20.68703 -38.12905,0.40563 z"
+       id="path3813"
+       style="fill:#003ec7;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+  </g>
+</svg>