diff --git a/backend/src/controllers/bulkImportController.ts b/backend/src/controllers/bulkImportController.ts index 529feca4..58e163d3 100644 --- a/backend/src/controllers/bulkImportController.ts +++ b/backend/src/controllers/bulkImportController.ts @@ -6,21 +6,18 @@ import { sendInternalError } from '../utils/internalError.js'; export class BulkImportController { async import(req: Request, res: Response) { try { - const { organization_id } = req.body; - const csvContent = req.body.csv; // Assuming the CSV is sent as a string in the 'csv' field + const organizationId = req.tenantId ?? req.user?.organizationId; + const csvContent = req.body?.csv; - if (!organization_id) { - return res.status(400).json({ error: 'Missing organization_id' }); + if (!organizationId) { + return res.status(403).json({ error: 'Authenticated organization context required' }); } - if (!csvContent) { + if (typeof csvContent !== 'string' || csvContent.trim().length === 0) { return res.status(400).json({ error: 'Missing csv content' }); } - const result = await csvPayrollImportService.processCsv( - parseInt(organization_id), - csvContent - ); + const result = await csvPayrollImportService.processCsv(organizationId, csvContent); // Return 207 Multi-Status if there were any errors, otherwise 200/201 const statusCode = result.errorCount > 0 ? 207 : result.successCount > 0 ? 201 : 200; diff --git a/frontend/src/components/CSVUploader.tsx b/frontend/src/components/CSVUploader.tsx index 26b62ac2..2da291af 100644 --- a/frontend/src/components/CSVUploader.tsx +++ b/frontend/src/components/CSVUploader.tsx @@ -1,16 +1,13 @@ -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { Upload, AlertCircle, CheckCircle } from 'lucide-react'; +import { parseCSVPreview } from '../utils/csvPreview'; +import type { CSVRow } from '../utils/csvPreview'; -export interface CSVRow { - rowNumber: number; - data: Record; - errors: string[]; - isValid: boolean; -} +export type { CSVRow } from '../utils/csvPreview'; interface CSVUploaderProps { requiredColumns: string[]; - onDataParsed: (data: CSVRow[]) => void; + onDataParsed: (data: CSVRow[], csvContent: string) => void; validators?: Record string | null>; } @@ -23,60 +20,21 @@ export const CSVUploader: React.FC = ({ const [parsedData, setParsedData] = useState([]); const [fileName, setFileName] = useState(null); const fileInputRef = useRef(null); + const readGeneration = useRef(0); - const parseCSV = (content: string): CSVRow[] => { - const lines = content.trim().split('\n'); - if (lines.length < 2) return []; - - const headers = lines[0].split(',').map((h) => h.trim()); - - // Validate headers - const missingColumns = requiredColumns.filter((col) => !headers.includes(col)); - if (missingColumns.length > 0) { - alert(`Missing required columns: ${missingColumns.join(', ')}`); - return []; - } - - const rows: CSVRow[] = []; - - for (let i = 1; i < lines.length; i++) { - const values = lines[i].split(',').map((v) => v.trim()); - const row: Record = {}; - const errors: string[] = []; - - headers.forEach((header, idx) => { - row[header] = values[idx] || ''; - }); - - // Validate each field - requiredColumns.forEach((col) => { - if (!row[col]) { - errors.push(`Missing required field: ${col}`); - } - }); - - // Run custom validators - Object.entries(validators).forEach(([field, validator]) => { - if (row[field]) { - const error = validator(row[field]); - if (error) { - errors.push(error); - } - } - }); - - rows.push({ - rowNumber: i + 1, - data: row, - errors, - isValid: errors.length === 0, - }); - } - - return rows; - }; + // Modified 2026-10-05: prevent replaced or unmounted reads from publishing CSV data. + useEffect(() => () => { + readGeneration.current += 1; + }, []); const handleFileParse = (file: File) => { + const generation = ++readGeneration.current; + if (fileInputRef.current) fileInputRef.current.value = ''; + setParsedData([]); + setFileName(null); + onDataParsed([], ''); + if (generation !== readGeneration.current) return; + if (!file.name.endsWith('.csv')) { alert('Please upload a CSV file'); return; @@ -84,15 +42,38 @@ export const CSVUploader: React.FC = ({ setFileName(file.name); const reader = new FileReader(); + const handleReadError = () => { + if (generation === readGeneration.current) { + alert('Unable to read the CSV file. Please select it again.'); + } + }; reader.onload = (e) => { - const content = e.target?.result as string; - const rows = parseCSV(content); + if (generation !== readGeneration.current) return; + const content = e.target?.result; + if (typeof content !== 'string') { + handleReadError(); + return; + } + let rows: CSVRow[]; + try { + rows = parseCSVPreview(content, requiredColumns, validators); + } catch (error) { + alert(error instanceof Error ? error.message : 'Unable to parse the CSV file'); + return; + } + if (generation !== readGeneration.current) return; setParsedData(rows); - onDataParsed(rows); + onDataParsed(rows, content); }; + reader.onerror = handleReadError; + reader.onabort = handleReadError; - reader.readAsText(file); + try { + reader.readAsText(file); + } catch { + handleReadError(); + } }; const handleDragEnter = (e: React.DragEvent) => { @@ -130,6 +111,7 @@ export const CSVUploader: React.FC = ({ {/* Upload Zone */}
e.preventDefault()} onDragLeave={handleDragLeave} onDrop={handleDrop} className={`relative border-2 border-dashed rounded-lg p-8 text-center transition cursor-pointer ${ @@ -146,7 +128,7 @@ export const CSVUploader: React.FC = ({ className="hidden" /> -