refactor(import): merge storage helpers, keep file preview off the network path

- Merge the two ~identical import-config storage modules into one
  (transaction + balance share the same HTTP logic).
- Show the parsed file immediately and load the saved per-account config
  off the critical path, so a slow or hanging request never blocks the
  preview or Next button; a stale load can't clobber a newer file.
- Drop the dead $hidden on AccountImportConfig (the controller returns
  the raw config blob, never the serialized model).
This commit is contained in:
Víctor Falcón 2026-07-18 13:00:33 +02:00
parent b6c759cfd6
commit ceef9cf26f
5 changed files with 109 additions and 117 deletions

View File

@ -20,14 +20,6 @@ class AccountImportConfig extends Model
'config',
];
/** @var list<string> */
protected $hidden = [
'id',
'account_id',
'created_at',
'updated_at',
];
protected function casts(): array
{
return [

View File

@ -8,10 +8,6 @@ import {
DrawerTitle,
} from '@/components/ui/drawer';
import { Progress } from '@/components/ui/progress';
import {
loadBalanceImportConfig,
saveBalanceImportConfig,
} from '@/lib/balance-import-config-storage';
import { getCsrfToken } from '@/lib/csrf';
import {
detectDateFormat,
@ -19,6 +15,10 @@ import {
parseDate,
parseFile,
} from '@/lib/file-parser';
import {
loadBalanceImportConfig,
saveBalanceImportConfig,
} from '@/lib/import-config-storage';
import { type SharedData } from '@/types';
import { supportsInvestedAmount, type Account } from '@/types/account';
import {
@ -279,13 +279,23 @@ export function ImportBalancesDrawer({
}
}
let finalMapping = autoMapping;
let finalDateFormat = detectedFormat;
// Show the parsed file immediately with auto-detected columns; the
// saved per-account config is fetched off the critical path so a
// slow or hanging network never blocks the preview or Next button.
setState((prev) => ({
...prev,
file,
parsedData: data,
columnHeaders: headers,
columnOptions,
columnMapping: autoMapping,
dateFormat: detectedFormat,
dateFormatDetected: formatDetected,
}));
if (state.selectedAccountId) {
const savedConfig = await loadBalanceImportConfig(
state.selectedAccountId,
);
const accountId = state.selectedAccountId;
if (accountId) {
const savedConfig = await loadBalanceImportConfig(accountId);
if (savedConfig) {
const isValidMapping = (
@ -300,26 +310,24 @@ export function ImportBalancesDrawer({
};
if (isValidMapping(savedConfig.columnMapping)) {
finalMapping = savedConfig.columnMapping;
finalDateFormat = savedConfig.dateFormat;
// Keep the saved format as the default, but still show
// the selector when the dates are ambiguous so a
// Only apply if this file is still the selected one, so a
// slow load can't clobber a file picked afterwards. Keep
// the saved format as the default, but still show the
// selector when the dates are ambiguous so a
// previously-saved wrong format can be corrected.
formatDetected = !formatAmbiguous;
setState((prev) =>
prev.file === file
? {
...prev,
columnMapping: savedConfig.columnMapping,
dateFormat: savedConfig.dateFormat,
dateFormatDetected: !formatAmbiguous,
}
: prev,
);
}
}
}
setState((prev) => ({
...prev,
file,
parsedData: data,
columnHeaders: headers,
columnOptions,
columnMapping: finalMapping,
dateFormat: finalDateFormat,
dateFormatDetected: formatDetected,
}));
} catch (err) {
setError(
err instanceof Error ? err.message : 'Failed to parse file',

View File

@ -223,13 +223,23 @@ export function ImportTransactionsDrawer({
}
}
let finalMapping = autoMapping;
let finalDateFormat = detectedFormat;
// Show the parsed file immediately with auto-detected columns; the
// saved per-account config is fetched off the critical path so a
// slow or hanging network never blocks the preview or Next button.
setState((prev) => ({
...prev,
file,
parsedData: data,
columnHeaders: headers,
columnOptions,
columnMapping: autoMapping,
dateFormat: detectedFormat,
dateFormatDetected: formatDetected,
}));
if (state.selectedAccountId) {
const savedConfig = await loadImportConfig(
state.selectedAccountId,
);
const accountId = state.selectedAccountId;
if (accountId) {
const savedConfig = await loadImportConfig(accountId);
if (savedConfig) {
const isValidMapping = (
@ -249,26 +259,24 @@ export function ImportTransactionsDrawer({
};
if (isValidMapping(savedConfig.columnMapping)) {
finalMapping = savedConfig.columnMapping;
finalDateFormat = savedConfig.dateFormat;
// Keep the saved format as the default, but still show
// the selector when the dates are ambiguous so a
// Only apply if this file is still the selected one, so a
// slow load can't clobber a file picked afterwards. Keep
// the saved format as the default, but still show the
// selector when the dates are ambiguous so a
// previously-saved wrong format can be corrected.
formatDetected = !formatAmbiguous;
setState((prev) =>
prev.file === file
? {
...prev,
columnMapping: savedConfig.columnMapping,
dateFormat: savedConfig.dateFormat,
dateFormatDetected: !formatAmbiguous,
}
: prev,
);
}
}
}
setState((prev) => ({
...prev,
file,
parsedData: data,
columnHeaders: headers,
columnOptions,
columnMapping: finalMapping,
dateFormat: finalDateFormat,
dateFormatDetected: formatDetected,
}));
} catch (err) {
setError(
err instanceof Error ? err.message : 'Failed to parse file',

View File

@ -1,49 +0,0 @@
import type { BalanceColumnMapping } from '@/types/balance-import';
import { DateFormat } from '@/types/import';
import type { UUID } from '@/types/uuid';
import axios from 'axios';
interface BalanceImportConfig {
columnMapping: BalanceColumnMapping;
dateFormat: DateFormat;
}
function configUrl(accountId: UUID): string {
return `/api/accounts/${accountId}/import-config`;
}
export async function saveBalanceImportConfig(
accountId: UUID,
config: BalanceImportConfig,
): Promise<void> {
try {
await axios.put(configUrl(accountId), {
type: 'balance',
config,
});
} catch (error) {
console.error('Failed to save balance import configuration:', error);
}
}
export async function loadBalanceImportConfig(
accountId: UUID,
): Promise<BalanceImportConfig | null> {
try {
const { data } = await axios.get<{ data: BalanceImportConfig | null }>(
configUrl(accountId),
{ params: { type: 'balance' } },
);
const config = data.data;
if (!config || !config.columnMapping || !config.dateFormat) {
return null;
}
return config;
} catch (error) {
console.error('Failed to load balance import configuration:', error);
return null;
}
}

View File

@ -1,3 +1,4 @@
import type { BalanceColumnMapping } from '@/types/balance-import';
import { type ColumnMapping, DateFormat } from '@/types/import';
import { type UUID } from '@/types/uuid';
import axios from 'axios';
@ -7,31 +8,37 @@ interface ImportConfig {
dateFormat: DateFormat;
}
interface BalanceImportConfig {
columnMapping: BalanceColumnMapping;
dateFormat: DateFormat;
}
type ImportConfigType = 'transaction' | 'balance';
function configUrl(accountId: UUID): string {
return `/api/accounts/${accountId}/import-config`;
}
export async function saveImportConfig(
async function saveConfig(
accountId: UUID,
config: ImportConfig,
type: ImportConfigType,
config: ImportConfig | BalanceImportConfig,
): Promise<void> {
try {
await axios.put(configUrl(accountId), {
type: 'transaction',
config,
});
await axios.put(configUrl(accountId), { type, config });
} catch (error) {
console.error('Failed to save import configuration:', error);
console.error(`Failed to save ${type} import configuration:`, error);
}
}
export async function loadImportConfig(
async function loadConfig<T extends ImportConfig | BalanceImportConfig>(
accountId: UUID,
): Promise<ImportConfig | null> {
type: ImportConfigType,
): Promise<T | null> {
try {
const { data } = await axios.get<{ data: ImportConfig | null }>(
const { data } = await axios.get<{ data: T | null }>(
configUrl(accountId),
{ params: { type: 'transaction' } },
{ params: { type } },
);
const config = data.data;
@ -42,7 +49,33 @@ export async function loadImportConfig(
return config;
} catch (error) {
console.error('Failed to load import configuration:', error);
console.error(`Failed to load ${type} import configuration:`, error);
return null;
}
}
export function saveImportConfig(
accountId: UUID,
config: ImportConfig,
): Promise<void> {
return saveConfig(accountId, 'transaction', config);
}
export function loadImportConfig(
accountId: UUID,
): Promise<ImportConfig | null> {
return loadConfig<ImportConfig>(accountId, 'transaction');
}
export function saveBalanceImportConfig(
accountId: UUID,
config: BalanceImportConfig,
): Promise<void> {
return saveConfig(accountId, 'balance', config);
}
export function loadBalanceImportConfig(
accountId: UUID,
): Promise<BalanceImportConfig | null> {
return loadConfig<BalanceImportConfig>(accountId, 'balance');
}