diff --git a/resources/js/components/onboarding/step-import-transactions.tsx b/resources/js/components/onboarding/step-import-transactions.tsx index 63b8721e..f8b6ce4d 100644 --- a/resources/js/components/onboarding/step-import-transactions.tsx +++ b/resources/js/components/onboarding/step-import-transactions.tsx @@ -150,6 +150,7 @@ export function StepImportTransactions({ categories={categories} banks={banks} automationRules={automationRules} + autoSelectSingleAccount /> ); diff --git a/resources/js/components/transactions/import-step-account.test.tsx b/resources/js/components/transactions/import-step-account.test.tsx new file mode 100644 index 00000000..3aaa2bb2 --- /dev/null +++ b/resources/js/components/transactions/import-step-account.test.tsx @@ -0,0 +1,64 @@ +import type { Account } from '@/types/account'; +import { render, screen, waitFor } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { ImportStepAccount } from './import-step-account'; + +const account: Account = { + id: 'account-1', + name: 'Checking', + name_iv: null, + encrypted: false, + bank: null, + type: 'checking', + currency_code: 'USD', + banking_connection_id: null, + external_account_id: null, + linked_at: null, +}; + +describe('ImportStepAccount', () => { + it('shows a single account without auto-selecting by default', () => { + const onAccountSelect = vi.fn(); + const onNext = vi.fn(); + + render( + , + ); + + expect(screen.getByText('Checking')).not.toBeNull(); + expect(screen.getByRole('button', { name: 'Next' })).toHaveProperty( + 'disabled', + true, + ); + expect(onAccountSelect).not.toHaveBeenCalled(); + expect(onNext).not.toHaveBeenCalled(); + }); + + it('auto-selects a single account when enabled for onboarding', async () => { + const onAutoSelect = vi.fn(); + const onAccountSelect = vi.fn(); + const onNext = vi.fn(); + + render( + , + ); + + await waitFor(() => { + expect(onAutoSelect).toHaveBeenCalledWith(account.id); + expect(onNext).toHaveBeenCalled(); + }); + expect(onAccountSelect).not.toHaveBeenCalled(); + }); +}); diff --git a/resources/js/components/transactions/import-step-account.tsx b/resources/js/components/transactions/import-step-account.tsx index e12226fd..8058bb62 100644 --- a/resources/js/components/transactions/import-step-account.tsx +++ b/resources/js/components/transactions/import-step-account.tsx @@ -17,6 +17,8 @@ interface ImportStepAccountProps { selectedAccountId: UUID | null; onAccountSelect: (accountId: UUID) => void; onNext: () => void; + autoSelectSingleAccount?: boolean; + onAutoSelect?: (accountId: UUID) => void; } export function ImportStepAccount({ @@ -24,16 +26,35 @@ export function ImportStepAccount({ selectedAccountId, onAccountSelect, onNext, + autoSelectSingleAccount = false, + onAutoSelect, }: ImportStepAccountProps) { const accounts = filterTransactionalAccounts(rawAccounts); - // If there is only one account, auto-select it, and proceed to next step useEffect(() => { - if (accounts.length === 1) { - onAccountSelect(accounts[0].id); + if ( + autoSelectSingleAccount && + accounts.length === 1 && + !selectedAccountId + ) { + const accountId = accounts[0].id; + + if (onAutoSelect) { + onAutoSelect(accountId); + } else { + onAccountSelect(accountId); + } + onNext(); } - }, [accounts, onAccountSelect, onNext]); + }, [ + accounts, + autoSelectSingleAccount, + onAccountSelect, + onAutoSelect, + onNext, + selectedAccountId, + ]); if (accounts.length === 0) { return ( diff --git a/resources/js/components/transactions/import-step-upload.test.tsx b/resources/js/components/transactions/import-step-upload.test.tsx new file mode 100644 index 00000000..22db2f55 --- /dev/null +++ b/resources/js/components/transactions/import-step-upload.test.tsx @@ -0,0 +1,20 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { ImportStepUpload } from './import-step-upload'; + +describe('ImportStepUpload', () => { + it('hides the back button when requested', () => { + render( + , + ); + + expect(screen.queryByRole('button', { name: 'Back' })).toBeNull(); + expect(screen.getByRole('button', { name: 'Next' })).not.toBeNull(); + }); +}); diff --git a/resources/js/components/transactions/import-step-upload.tsx b/resources/js/components/transactions/import-step-upload.tsx index f00496ab..794a8c8d 100644 --- a/resources/js/components/transactions/import-step-upload.tsx +++ b/resources/js/components/transactions/import-step-upload.tsx @@ -9,6 +9,7 @@ interface ImportStepUploadProps { onFileSelect: (file: File) => void; onNext: () => void; onBack: () => void; + showBackButton?: boolean; } export function ImportStepUpload({ @@ -16,6 +17,7 @@ export function ImportStepUpload({ onFileSelect, onNext, onBack, + showBackButton = true, }: ImportStepUploadProps) { const [isDragging, setIsDragging] = useState(false); @@ -135,10 +137,17 @@ export function ImportStepUpload({ )} -
- +
+ {showBackButton && ( + + )} diff --git a/resources/js/components/transactions/import-transactions-drawer.tsx b/resources/js/components/transactions/import-transactions-drawer.tsx index 40b10b0a..4e0e3549 100644 --- a/resources/js/components/transactions/import-transactions-drawer.tsx +++ b/resources/js/components/transactions/import-transactions-drawer.tsx @@ -31,6 +31,7 @@ import { type ColumnMapping, type ImportState, } from '@/types/import'; +import { type UUID } from '@/types/uuid'; import { __ } from '@/utils/i18n'; import { router, usePage } from '@inertiajs/react'; import { useEffect, useState } from 'react'; @@ -48,6 +49,7 @@ interface ImportTransactionsDrawerProps { open: boolean; onOpenChange: (open: boolean) => void; onImportComplete?: () => void; + autoSelectSingleAccount?: boolean; } interface ImportError { @@ -68,6 +70,7 @@ export function ImportTransactionsDrawer({ open, onOpenChange, onImportComplete, + autoSelectSingleAccount = false, }: ImportTransactionsDrawerProps) { const { locale } = usePage().props; const [isImporting, setIsImporting] = useState(false); @@ -75,6 +78,8 @@ export function ImportTransactionsDrawer({ const [importTotal, setImportTotal] = useState(0); const [importErrors, setImportErrors] = useState([]); const [error, setError] = useState(null); + const [wasSingleAccountAutoSelected, setWasSingleAccountAutoSelected] = + useState(false); const [selectedAccount, setSelectedAccount] = useState( null, ); @@ -89,6 +94,7 @@ export function ImportTransactionsDrawer({ transaction_date: null, description: null, amount: null, + balance: null, }, dateFormat: DateFormat.YearMonthDay, dateFormatDetected: false, @@ -119,6 +125,7 @@ export function ImportTransactionsDrawer({ transaction_date: null, description: null, amount: null, + balance: null, }, dateFormat: DateFormat.YearMonthDay, dateFormatDetected: false, @@ -126,14 +133,20 @@ export function ImportTransactionsDrawer({ }); setIsImporting(false); setError(null); + setWasSingleAccountAutoSelected(false); setSelectedAccount(null); } }, [open]); - const handleAccountSelect = (accountId: number) => { + const handleAccountSelect = (accountId: UUID) => { setState((prev) => ({ ...prev, selectedAccountId: accountId })); }; + const handleSingleAccountAutoSelect = (accountId: UUID) => { + setWasSingleAccountAutoSelected(true); + handleAccountSelect(accountId); + }; + const handleFileSelect = async (file: File) => { if (!file) { setState((prev) => ({ @@ -653,6 +666,8 @@ export function ImportTransactionsDrawer({ onNext={() => { moveToStep(ImportStep.UploadFile); }} + autoSelectSingleAccount={autoSelectSingleAccount} + onAutoSelect={handleSingleAccountAutoSelect} /> ); @@ -665,6 +680,7 @@ export function ImportTransactionsDrawer({ moveToStep(ImportStep.MapColumns); }} onBack={() => moveToStep(ImportStep.SelectAccount)} + showBackButton={!wasSingleAccountAutoSelected} /> ); diff --git a/resources/js/lib/import-config-storage.ts b/resources/js/lib/import-config-storage.ts index 52dfe1d6..5a85a811 100644 --- a/resources/js/lib/import-config-storage.ts +++ b/resources/js/lib/import-config-storage.ts @@ -1,4 +1,5 @@ import { type ColumnMapping, DateFormat } from '@/types/import'; +import { type UUID } from '@/types/uuid'; interface ImportConfig { columnMapping: ColumnMapping; @@ -7,10 +8,7 @@ interface ImportConfig { const STORAGE_KEY_PREFIX = 'import_config_account_'; -export function saveImportConfig( - accountId: number, - config: ImportConfig, -): void { +export function saveImportConfig(accountId: UUID, config: ImportConfig): void { if (typeof window === 'undefined') return; try { @@ -21,7 +19,7 @@ export function saveImportConfig( } } -export function loadImportConfig(accountId: number): ImportConfig | null { +export function loadImportConfig(accountId: UUID): ImportConfig | null { if (typeof window === 'undefined') return null; try { @@ -45,7 +43,7 @@ export function loadImportConfig(accountId: number): ImportConfig | null { } } -export function clearImportConfig(accountId: number): void { +export function clearImportConfig(accountId: UUID): void { if (typeof window === 'undefined') return; try { diff --git a/resources/js/types/import.ts b/resources/js/types/import.ts index fe1af2a0..b32d678f 100644 --- a/resources/js/types/import.ts +++ b/resources/js/types/import.ts @@ -1,3 +1,5 @@ +import { UUID } from './uuid'; + export enum ImportStep { SelectAccount = 'select-account', UploadFile = 'upload-file', @@ -40,7 +42,7 @@ export interface ColumnOption { export interface ImportState { step: ImportStep; - selectedAccountId: number | null; + selectedAccountId: UUID | null; file: File | null; parsedData: ParsedRow[]; columnHeaders: string[];