diff --git a/app/Features/CalculateBalancesOnImport.php b/app/Features/CalculateBalancesOnImport.php
new file mode 100644
index 00000000..74e2ec6e
--- /dev/null
+++ b/app/Features/CalculateBalancesOnImport.php
@@ -0,0 +1,19 @@
+user();
+
return [
'cashflow' => true,
+ 'calculateBalancesOnImport' => $user
+ ? Feature::for($user)->active(CalculateBalancesOnImport::class)
+ : false,
];
}
diff --git a/lang/es.json b/lang/es.json
index 2055e679..b8c2ab00 100644
--- a/lang/es.json
+++ b/lang/es.json
@@ -225,6 +225,7 @@
"Balance History": "Historial de Balance",
"Balance Tracking": "Seguimiento de Balance",
"Balance evolution": "Evolución del balance",
+ "Balance on": "Balance del",
"Balances": "Balances",
"Bank": "Banco",
"Bank Account": "Cuenta Bancaria",
@@ -271,6 +272,7 @@
"Can I export or delete my data?": "¿Puedo exportar o eliminar mis datos?",
"Canadian Dollar": "Dólar canadiense",
"Cancel": "Cancelar",
+ "Calculate balances from transactions": "Calcular balances a partir de transacciones",
"Carry Over": "Acumular",
"Cash inflow": "Entrada de efectivo",
"Cash outflow": "Salida de efectivo",
@@ -1005,6 +1007,7 @@
"Prefer to Connect Your Bank?": "¿Prefieres Conectar Tu Banco?",
"Prefer to Start Fresh?": "¿Prefieres Empezar de Cero?",
"Prev": "Ant",
+ "Pre-filled from an existing balance for this date.": "Rellenado con un balance existente para esta fecha.",
"Preview (first 3 rows)": "Vista previa (primeras 3 filas)",
"Preview Balances": "Vista Previa de Balances",
"Preview Owed Amounts": "Vista Previa de Montos Adeudados",
@@ -1453,6 +1456,7 @@
"Uruguayan Peso": "Peso uruguayo",
"Usage Information:": "Información de Uso:",
"Use Defaults": "Usar Valores Predeterminados",
+ "Use the balance on the latest transaction date as a reference to compute balances for older dates.": "Usa el balance de la fecha de la última transacción como referencia para calcular los balances de fechas anteriores.",
"Use a view-only key.": "Usa una clave de solo lectura.",
"Use a strong password (minimum 12 characters). This password will encrypt your data.": "Usa una contraseña fuerte (mínimo 12 caracteres). Esta contraseña encriptará tus datos.",
"Use a strong password (minimum 12 characters). This\\n password will encrypt your data.": "Usa una contraseña segura (mínimo 12 caracteres). Esta contraseña cifrará tus datos.",
@@ -1587,6 +1591,7 @@
"Year": "Año",
"Yearly": "Anual",
"Yearly code": "Código anual",
+ "Yesterday": "Ayer",
"Yes! Whisper Money is fully open source. You can review the code, suggest improvements, or even self-host it. Transparency is a core part of our privacy commitment.": "¡Sí! Whisper Money es completamente de código abierto. Puedes revisar el código, sugerir mejoras o incluso alojarlo tú mismo. La transparencia es una parte fundamental de nuestro compromiso con la privacidad.",
"Yes. Bank connections use secure Open Banking and never require your bank credentials, so transactions can sync automatically. This is a Pro feature; free users can import everything, but only through the CSV/Excel importer.": "Sí. Las conexiones bancarias usan Open Banking seguro y nunca requieren tus credenciales bancarias, por lo que las transacciones se sincronizan automáticamente. Es una función Pro; los usuarios gratuitos pueden importarlo todo, pero solo mediante el importador CSV/Excel.",
"You agree to indemnify, defend, and hold harmless Whisper Money and its officers, directors, employees, and agents from any claims, liabilities, damages, losses, and expenses, including reasonable legal fees, arising out of or related to your use of the service, violation of these Terms, or violation of any rights of another party.": "Aceptas indemnizar, defender y eximir de responsabilidad a Whisper Money y sus funcionarios, directores, empleados y agentes de cualquier reclamo, responsabilidad, daño, pérdida y gasto, incluidos los honorarios legales razonables, que surjan de o estén relacionados con tu uso del servicio, violación de estos Términos o violación de los derechos de otra parte.",
diff --git a/resources/js/components/transactions/import-step-mapping.tsx b/resources/js/components/transactions/import-step-mapping.tsx
index 57ada1b9..58048ff8 100644
--- a/resources/js/components/transactions/import-step-mapping.tsx
+++ b/resources/js/components/transactions/import-step-mapping.tsx
@@ -1,4 +1,6 @@
+import { AmountInput } from '@/components/ui/amount-input';
import { Button } from '@/components/ui/button';
+import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import {
@@ -9,14 +11,20 @@ import {
SelectValue,
} from '@/components/ui/select';
import { useLocale } from '@/hooks/use-locale';
-import { parseAmount, parseDate } from '@/lib/file-parser';
+import {
+ getLatestTransactionDate,
+ parseAmount,
+ parseDate,
+} from '@/lib/file-parser';
import {
DateFormat,
type ColumnMapping,
type ColumnOption,
type ParsedRow,
} from '@/types/import';
+import { formatRelativeDate } from '@/utils/date';
import { __ } from '@/utils/i18n';
+import { useEffect, useMemo } from 'react';
interface ImportStepMappingProps {
columnOptions: ColumnOption[];
@@ -25,11 +33,18 @@ interface ImportStepMappingProps {
dateFormatDetected: boolean;
parsedData: ParsedRow[];
currencyCode: string;
+ calculateBalances: boolean;
+ referenceBalance: number | null;
+ referenceBalancePrefilled: boolean;
+ calculateBalancesAvailable: boolean;
onMappingChange: (
field: keyof ColumnMapping,
value: string | string[],
) => void;
onDateFormatChange: (format: DateFormat) => void;
+ onCalculateBalancesChange: (enabled: boolean) => void;
+ onReferenceBalanceChange: (balanceInCents: number) => void;
+ onLatestDateChange: (date: string | null) => void;
onNext: () => void;
onBack: () => void;
}
@@ -41,8 +56,15 @@ export function ImportStepMapping({
dateFormatDetected,
parsedData,
currencyCode,
+ calculateBalances,
+ referenceBalance,
+ referenceBalancePrefilled,
+ calculateBalancesAvailable,
onMappingChange,
onDateFormatChange,
+ onCalculateBalancesChange,
+ onReferenceBalanceChange,
+ onLatestDateChange,
onNext,
onBack,
}: ImportStepMappingProps) {
@@ -53,10 +75,36 @@ export function ImportStepMapping({
: [];
const locale = useLocale();
+ const balanceColumnSet = !!columnMapping.balance;
+ const checkboxDisabled = balanceColumnSet;
+ const effectiveCalculate = calculateBalances && !balanceColumnSet;
+
+ const latestDate = useMemo(() => {
+ if (!effectiveCalculate) {
+ return null;
+ }
+ return getLatestTransactionDate(
+ parsedData,
+ columnMapping,
+ dateFormat,
+ );
+ }, [effectiveCalculate, parsedData, columnMapping, dateFormat]);
+
+ useEffect(() => {
+ onLatestDateChange(latestDate);
+ }, [latestDate, onLatestDateChange]);
+
+ const baseMappingValid =
+ !!columnMapping.transaction_date &&
+ !!columnMapping.description &&
+ !!columnMapping.amount;
+
const isValid =
- columnMapping.transaction_date &&
- columnMapping.description &&
- columnMapping.amount;
+ baseMappingValid &&
+ (!effectiveCalculate ||
+ (latestDate !== null &&
+ referenceBalance !== null &&
+ referenceBalance !== undefined));
const getDescriptionFromRow = (row: ParsedRow): string => {
if (!columnMapping.description) {
@@ -334,9 +382,75 @@ export function ImportStepMapping({
))}
+
+ {calculateBalancesAvailable && (
+