diff --git a/resources/js/components/transactions/import-step-mapping.tsx b/resources/js/components/transactions/import-step-mapping.tsx index 7d6d0e30..81ad185b 100644 --- a/resources/js/components/transactions/import-step-mapping.tsx +++ b/resources/js/components/transactions/import-step-mapping.tsx @@ -23,7 +23,7 @@ interface ImportStepMappingProps { dateFormatDetected: boolean; parsedData: ParsedRow[]; currencyCode: string; - onMappingChange: (field: keyof ColumnMapping, value: string) => void; + onMappingChange: (field: keyof ColumnMapping, value: string | string[]) => void; onDateFormatChange: (format: DateFormat) => void; onNext: () => void; onBack: () => void; @@ -41,11 +41,61 @@ export function ImportStepMapping({ onNext, onBack, }: ImportStepMappingProps) { + const descriptionColumns = Array.isArray(columnMapping.description) + ? columnMapping.description + : columnMapping.description + ? [columnMapping.description] + : []; + const isValid = columnMapping.transaction_date && columnMapping.description && columnMapping.amount; + const getDescriptionFromRow = (row: ParsedRow): string => { + if (!columnMapping.description) { + return ''; + } + + const columns = Array.isArray(columnMapping.description) + ? columnMapping.description + : [columnMapping.description]; + + return columns + .map((col) => String(row[col] || '').trim()) + .filter((val) => val.length > 0) + .join('\n'); + }; + + const handleDescriptionChange = (index: number, value: string) => { + const newColumns = [...descriptionColumns]; + + if (value === '__none__') { + newColumns.splice(index, 1); + } else { + newColumns[index] = value; + } + + if (newColumns.length === 0) { + onMappingChange('description', ''); + } else if (newColumns.length === 1) { + onMappingChange('description', newColumns[0]); + } else { + onMappingChange('description', newColumns); + } + }; + + const addDescriptionColumn = () => { + if (descriptionColumns.length < 3) { + const newColumns = [...descriptionColumns, '']; + if (newColumns.length === 1) { + onMappingChange('description', ''); + } else { + onMappingChange('description', newColumns); + } + } + }; + const previewTransactions = parsedData.slice(0, 3).map((row) => { const date = columnMapping.transaction_date ? parseDate( @@ -53,9 +103,7 @@ export function ImportStepMapping({ dateFormat, ) : null; - const description = columnMapping.description - ? String(row[columnMapping.description] || '') - : ''; + const description = getDescriptionFromRow(row); const amount = columnMapping.amount ? parseAmount(row[columnMapping.amount] as string | number) : null; @@ -112,31 +160,89 @@ export function ImportStepMapping({
-
@@ -201,17 +307,17 @@ export function ImportStepMapping({ {previewTransactions.map((transaction, index) => (
-
- +
+ {transaction.date} - + {transaction.description}
- + {transaction.amount}
diff --git a/resources/js/components/transactions/import-transactions-drawer.tsx b/resources/js/components/transactions/import-transactions-drawer.tsx index 5c053d6a..393385d2 100644 --- a/resources/js/components/transactions/import-transactions-drawer.tsx +++ b/resources/js/components/transactions/import-transactions-drawer.tsx @@ -200,9 +200,14 @@ export function ImportTransactionsDrawer({ const values = Object.values(mapping).filter( (v) => v !== null, ); - return values.every((value) => - headers.includes(value as string), - ); + return values.every((value) => { + if (Array.isArray(value)) { + return value.every((v) => + headers.includes(v as string), + ); + } + return headers.includes(value as string); + }); }; if (isValidMapping(savedConfig.columnMapping)) { @@ -230,7 +235,10 @@ export function ImportTransactionsDrawer({ } }; - const handleMappingChange = (field: keyof ColumnMapping, value: string) => { + const handleMappingChange = ( + field: keyof ColumnMapping, + value: string | string[], + ) => { setState((prev) => ({ ...prev, columnMapping: { @@ -397,6 +405,18 @@ export function ImportTransactionsDrawer({ if (result.status === 'fulfilled') { createdTransactions.push(result.value.transaction); } else { + const errorMessage = + result.reason instanceof Error + ? result.reason.message + : 'Unknown error'; + + console.error(`Transaction ${rowNumber} failed:`, { + transaction, + error: result.reason, + errorMessage, + stack: result.reason instanceof Error ? result.reason.stack : undefined, + }); + errors.push({ rowNumber, transaction: { @@ -404,10 +424,7 @@ export function ImportTransactionsDrawer({ description: transaction.description, amount: transaction.amount.toString(), }, - error: - result.reason instanceof Error - ? result.reason.message - : 'Unknown error', + error: errorMessage, }); } }); diff --git a/resources/js/lib/file-parser.ts b/resources/js/lib/file-parser.ts index 13930aa1..3fc90ec4 100644 --- a/resources/js/lib/file-parser.ts +++ b/resources/js/lib/file-parser.ts @@ -436,6 +436,21 @@ export function parseAmount(amountStr: string | number): number | null { return amount; } +function getDescriptionFromRow(row: ParsedRow, mapping: ColumnMapping): string { + if (!mapping.description) { + return ''; + } + + const columns = Array.isArray(mapping.description) + ? mapping.description + : [mapping.description]; + + return columns + .map((col) => String(row[col] || '').trim()) + .filter((val) => val.length > 0) + .join('\n'); +} + export function validateTransaction( row: ParsedRow, mapping: ColumnMapping, @@ -455,8 +470,13 @@ export function validateTransaction( } } - if (!mapping.description || !row[mapping.description]) { + if (!mapping.description) { errors.push('Missing description'); + } else { + const description = getDescriptionFromRow(row, mapping); + if (!description) { + errors.push('Missing description'); + } } if ( @@ -497,7 +517,7 @@ export function convertRowsToTransactions( dateFormat, ); const amount = parseAmount(row[mapping.amount!] as string | number); - const description = String(row[mapping.description!] || '').trim(); + const description = getDescriptionFromRow(row, mapping); if (!date || amount === null || !description) { continue; diff --git a/resources/js/types/import.ts b/resources/js/types/import.ts index 1cc42e1d..71af7a26 100644 --- a/resources/js/types/import.ts +++ b/resources/js/types/import.ts @@ -13,7 +13,7 @@ export enum DateFormat { export interface ColumnMapping { transaction_date: string | null; - description: string | null; + description: string | string[] | null; amount: string | null; balance: string | null; } diff --git a/tests/Browser/assets/test-multi-description.csv b/tests/Browser/assets/test-multi-description.csv new file mode 100644 index 00000000..82902c89 --- /dev/null +++ b/tests/Browser/assets/test-multi-description.csv @@ -0,0 +1,8 @@ +Date,Description1,Description2,Description3,Amount +2024-01-15,Grocery Store,Walmart,Food Shopping,-50.25 +2024-01-16,Coffee Shop,Starbucks,Morning Coffee,-5.50 +2024-01-17,Salary,Monthly Payment,Direct Deposit,2500.00 +2024-01-18,Utilities,Electric Bill,January 2024,-125.00 +2024-01-19,Restaurant,Italian Place,Dinner with Friends,-45.75 + +