diff --git a/app/Enums/AccountType.php b/app/Enums/AccountType.php index 2bcc9a36..e8c27abf 100644 --- a/app/Enums/AccountType.php +++ b/app/Enums/AccountType.php @@ -19,4 +19,9 @@ enum AccountType: string { return in_array($this, [self::Investment, self::Retirement, self::Savings], true); } + + public function reducesNetWorth(): bool + { + return in_array($this, [self::CreditCard, self::Loan], true); + } } diff --git a/app/Http/Controllers/Api/DashboardAnalyticsController.php b/app/Http/Controllers/Api/DashboardAnalyticsController.php index dd44c8c0..b9beeca3 100644 --- a/app/Http/Controllers/Api/DashboardAnalyticsController.php +++ b/app/Http/Controllers/Api/DashboardAnalyticsController.php @@ -282,12 +282,16 @@ class DashboardAnalyticsController extends Controller ->orderBy('balance_date', 'desc') ->value('balance') ?? 0; - $total += $this->exchangeRateService->convert( + $convertedBalance = $this->exchangeRateService->convert( $account->currency_code, $userCurrency, $balance, $date->toDateString(), ); + + $total += $account->type->reducesNetWorth() + ? -abs($convertedBalance) + : $convertedBalance; } return $total; diff --git a/app/Http/Controllers/Settings/NetWorthChartLoanPreferenceController.php b/app/Http/Controllers/Settings/NetWorthChartLoanPreferenceController.php new file mode 100644 index 00000000..f7df2edc --- /dev/null +++ b/app/Http/Controllers/Settings/NetWorthChartLoanPreferenceController.php @@ -0,0 +1,20 @@ +user()->setting()->updateOrCreate( + ['user_id' => $request->user()->id], + ['include_loans_in_net_worth_chart' => $request->boolean('include_loans_in_net_worth_chart')] + ); + + return back(); + } +} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index a349ffea..9f045937 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -85,6 +85,7 @@ class HandleInertiaRequests extends Middleware 'currency' => strtoupper(config('cashier.currency', 'eur')), ], 'chartColorScheme' => $user?->setting?->chart_color_scheme->value ?? 'colorful', + 'includeLoansInNetWorthChart' => $user?->setting->include_loans_in_net_worth_chart ?? true, 'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true', 'features' => [ 'cashflow' => true, diff --git a/app/Http/Requests/Settings/UpdateNetWorthChartLoanPreferenceRequest.php b/app/Http/Requests/Settings/UpdateNetWorthChartLoanPreferenceRequest.php new file mode 100644 index 00000000..ab8b15f9 --- /dev/null +++ b/app/Http/Requests/Settings/UpdateNetWorthChartLoanPreferenceRequest.php @@ -0,0 +1,28 @@ +|string> + */ + public function rules(): array + { + return [ + 'include_loans_in_net_worth_chart' => ['required', 'boolean'], + ]; + } +} diff --git a/app/Models/UserSetting.php b/app/Models/UserSetting.php index 27450425..63d1e318 100644 --- a/app/Models/UserSetting.php +++ b/app/Models/UserSetting.php @@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; /** * @property \App\Enums\ChartColorScheme $chart_color_scheme + * @property bool $include_loans_in_net_worth_chart */ class UserSetting extends Model { @@ -19,12 +20,14 @@ class UserSetting extends Model protected $fillable = [ 'user_id', 'chart_color_scheme', + 'include_loans_in_net_worth_chart', ]; protected function casts(): array { return [ 'chart_color_scheme' => ChartColorScheme::class, + 'include_loans_in_net_worth_chart' => 'boolean', ]; } diff --git a/database/factories/UserSettingFactory.php b/database/factories/UserSettingFactory.php index 6deb02fb..d38859d0 100644 --- a/database/factories/UserSettingFactory.php +++ b/database/factories/UserSettingFactory.php @@ -21,6 +21,7 @@ class UserSettingFactory extends Factory return [ 'user_id' => User::factory(), 'chart_color_scheme' => ChartColorScheme::Colorful, + 'include_loans_in_net_worth_chart' => true, ]; } diff --git a/database/migrations/2026_03_19_151039_add_include_loans_in_net_worth_chart_to_user_settings_table.php b/database/migrations/2026_03_19_151039_add_include_loans_in_net_worth_chart_to_user_settings_table.php new file mode 100644 index 00000000..062732a2 --- /dev/null +++ b/database/migrations/2026_03_19_151039_add_include_loans_in_net_worth_chart_to_user_settings_table.php @@ -0,0 +1,28 @@ +boolean('include_loans_in_net_worth_chart')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('user_settings', function (Blueprint $table) { + $table->dropColumn('include_loans_in_net_worth_chart'); + }); + } +}; diff --git a/lang/es.json b/lang/es.json index 4df444e7..43413393 100644 --- a/lang/es.json +++ b/lang/es.json @@ -163,6 +163,7 @@ "Are you overspending? Know exactly where you stand.": "\u00bfEst\u00e1s gastando de m\u00e1s? Conoce exactamente d\u00f3nde est\u00e1s parado.", "Are you sure you want to delete \"": "\u00bfEst\u00e1s seguro de que deseas eliminar \"", "Are you sure you want to delete this balance record? This action cannot be undone.": "\u00bfEst\u00e1s seguro de que deseas eliminar este registro de balance? Esta acci\u00f3n no se puede deshacer.", + "Are you sure you want to delete this owed amount record? This action cannot be undone.": "\u00bfEst\u00e1s seguro de que deseas eliminar este registro de monto adeudado? Esta acci\u00f3n no se puede deshacer.", "Are you sure you want to delete this transaction? This action cannot be undone.": "\u00bfEst\u00e1s seguro de que quieres eliminar esta transacci\u00f3n? Esta acci\u00f3n no se puede deshacer.", "Are you sure you want to delete your account?": "\u00bfEst\u00e1s seguro de que deseas eliminar tu cuenta?", "As a developer, I appreciate the security architecture. This is how finance apps should be built.": "Como desarrollador, aprecio la arquitectura de seguridad. As\u00ed es como deber\u00edan construirse las apps de finanzas.", @@ -196,6 +197,7 @@ "Balance Date": "Fecha del Balance", "Balance History": "Historial de Balance", "Balance Tracking": "Seguimiento de Balance", + "Balances": "Balances", "Balance evolution": "Evoluci\u00f3n del balance", "Bank": "Banco", "Bank Account": "Cuenta Bancaria", @@ -394,6 +396,7 @@ "Delete account": "Eliminar cuenta", "Delete accounts": "Eliminar cuentas", "Delete balance": "Eliminar balance", + "Delete owed amount": "Eliminar monto adeudado", "Delete budget": "Eliminar presupuesto", "Delete your account and all of its resources": "Elimina tu cuenta y todos sus recursos", "Deleting...": "Eliminando...", @@ -426,6 +429,7 @@ "Edit Balance": "Editar Balance", "Edit Budget": "Editar Presupuesto", "Edit Category": "Editar Categor\u00eda", + "Edit Owed Amount": "Editar Monto Adeudado", "Edit Label": "Editar Etiqueta", "Edit Transaction": "Editar transacci\u00f3n", "Edit account": "Editar cuenta", @@ -623,6 +627,7 @@ "Import a year's worth of transactions in under 10 seconds. Simply export a CSV or XLS file from your bank and drag it into Whisper Money. All data is encrypted locally before upload.": "Importa transacciones de un a\u00f1o completo en menos de 10 segundos. Simplemente exporta un archivo CSV o XLS desde tu banco y arr\u00e1stralo a Whisper Money. Todos los datos se encriptan localmente antes de subirse.", "Import balance history from CSV files": "Importar historial de balances desde archivos CSV", "Import balances": "Importar balances", + "Import owed amounts": "Importar montos adeudados", "Import in Seconds": "Importa en Segundos", "Import in seconds": "Importa en segundos", "Import transactions": "Importar transacciones", @@ -635,6 +640,8 @@ "Importing Transactions": "Importando Transacciones", "Importing your balances...": "Importando tus saldos...", "Imported": "Importado", + "Include loan balances in the net worth totals and chart": "Incluir los saldos de pr\u00e9stamos en los totales y el gr\u00e1fico del patrimonio neto", + "Include loans": "Incluir pr\u00e9stamos", "Income": "Ingresos", "Income (Salary, Freelance, Investments)": "Ingresos (Salario, Freelance, Inversiones)", "Income Sources": "Fuentes de Ingresos", @@ -697,9 +704,11 @@ "Load more": "Cargar m\u00e1s", "Loading": "Cargando", "Loading last balance...": "Cargando \u00faltimo balance...", + "Loading last owed amount...": "Cargando \u00faltimo monto adeudado...", "Loading recovery codes": "Cargando c\u00f3digos de recuperaci\u00f3n", "Loading...": "Cargando...", "Loan": "Pr\u00e9stamo", + "Loans": "Pr\u00e9stamos", "Lock encryption key": "Bloquear clave de encriptaci\u00f3n", "Log in": "Iniciar sesi\u00f3n", "Log in to your account": "Inicia sesi\u00f3n en tu cuenta", @@ -802,11 +811,15 @@ "No income this period": "Sin ingresos en este per\u00edodo", "No labels found.": "No se encontraron etiquetas.", "No limits on bank accounts, transactions, or categories.": "Sin l\u00edmites en cuentas bancarias, transacciones o categor\u00edas.", + "No owed amount data available": "No hay datos de monto adeudado disponibles", + "No owed amount records found.": "No se encontraron registros de monto adeudado.", "No results found": "No se encontraron resultados", "No spending data this month": "Sin datos de gasto este mes", "No third-party sharing, no AI snooping. Your financial data belongs to you and only you.": "Sin compartir con terceros, sin espionaje de IA. Tus datos financieros te pertenecen a ti y solo a ti.", "No tracking": "Sin seguimiento", "No transactions found.": "No se encontraron transacciones.", + "No valid balances found": "No se encontraron balances v\u00e1lidos", + "No valid owed amounts found": "No se encontraron montos adeudados v\u00e1lidos", "No worries! Your encryption key is automatically generated and stored securely on your device. You don't need to remember anything - I've made it as simple as possible.": "\u00a1No te preocupes! Tu clave de encriptaci\u00f3n se genera autom\u00e1ticamente y se almacena de forma segura en tu dispositivo. No necesitas recordar nada, lo he hecho lo m\u00e1s simple posible.", "No. We never ask for your bank credentials. You import transactions by exporting a CSV or XLS file from your bank and uploading it to Whisper Money. This keeps your bank account secure.": "No. Nunca pedimos tus credenciales bancarias. Importas transacciones exportando un archivo CSV o XLS de tu banco y subi\u00e9ndolo a Whisper Money. Esto mantiene tu cuenta bancaria segura.", "None": "Ninguno", @@ -835,6 +848,10 @@ "Our service is not intended for users under the age of 16. We do not knowingly collect personal information from children. If you believe we have collected information from a child, please contact us immediately, and we will delete it.": "Nuestro servicio no est\u00e1 destinado a usuarios menores de 16 a\u00f1os. No recopilamos intencionalmente informaci\u00f3n personal de ni\u00f1os. Si crees que hemos recopilado informaci\u00f3n de un ni\u00f1o, por favor cont\u00e1ctanos inmediatamente y la eliminaremos.", "Our service is not intended for users under the\\n age of 16. We do not knowingly collect personal\\n information from children. If you believe we\\n have collected information from a child, please\\n contact us immediately, and we will delete it.": "Nuestro servicio no est\u00e1 destinado a usuarios menores de 16 a\u00f1os. No recopilamos intencionalmente informaci\u00f3n personal de ni\u00f1os. Si crees que hemos recopilado informaci\u00f3n de un ni\u00f1o, cont\u00e1ctanos de inmediato y la eliminaremos.", "Overview of your financial health": "Vista general de tu salud financiera", + "Owed Amount": "Monto Adeudado", + "Owed Amount History": "Historial de Monto Adeudado", + "Owed Amounts": "Montos Adeudados", + "Owed amount evolution": "Evoluci\u00f3n del monto adeudado", "Page": "P\u00e1gina", "Password": "Contrase\u00f1a", "Password changes are disabled on the demo account.": "Los cambios de contrase\u00f1a est\u00e1n deshabilitados en la cuenta demo.", @@ -879,6 +896,7 @@ "Prev": "Ant", "Preview (first 3 rows)": "Vista previa (primeras 3 filas)", "Preview Balances": "Vista Previa de Balances", + "Preview Owed Amounts": "Vista Previa de Montos Adeudados", "Preview Transactions": "Vista Previa de Transacciones", "Previous": "Anterior", "Previous month": "Mes anterior", @@ -980,6 +998,7 @@ "Secure upload": "Carga segura", "See balances": "Ver balances", "See every account in one place. Track balances, monitor changes, and always know where you stand.": "Ve todas tus cuentas en un solo lugar. Sigue los saldos, monitoriza los cambios y sabe siempre c\u00f3mo est\u00e1s.", + "See owed amounts": "Ver montos adeudados", "See where you spend": "Ve en qu\u00e9 gastas", "See where you stand in real-time. Visual progress bars show spending vs. budget.": "Ve tu situaci\u00f3n en tiempo real. Las barras de progreso visuales muestran el gasto frente al presupuesto.", "Select Account": "Seleccionar Cuenta", @@ -1000,6 +1019,7 @@ "Select balance column": "Seleccionar columna de balance", "Select balance column (optional)": "Seleccionar columna de balance (opcional)", "Select bank...": "Seleccionar banco...", + "Select owed amount column": "Seleccionar columna de monto adeudado", "Select categories...": "Seleccionar categor\u00edas...", "Select country": "Seleccionar pa\u00eds", "Select currency": "Seleccionar moneda", @@ -1027,6 +1047,7 @@ "Set spending limits and get real-time updates on your financial goals.": "Establece l\u00edmites de gasto y obt\u00e9n actualizaciones en tiempo real sobre tus metas financieras.", "Set the balance for this account on a specific date.": "Actualiza el balance de esta cuenta.", "Set the current balance for this account to start tracking.": "Establece el balance actual de esta cuenta para empezar a rastrear.", + "Set the owed amount for this account on a specific date.": "Establece el monto adeudado de esta cuenta en una fecha espec\u00edfica.", "Set up a spending limit for a category or label.": "Configura un l\u00edmite de gasto para un categor\u00eda o etiqueta.", "Set your budget goals": "Establece tus objetivos de presupuesto", "Setting things up...": "Configurando...", @@ -1245,12 +1266,14 @@ "Update account balance": "Actualizar balance de cuenta", "Update balance": "Actualizar balance", "Update balances periodically to track growth": "Actualiza los balances peri\u00f3dicamente para rastrear el crecimiento", + "Update owed amount": "Actualizar monto adeudado", "Update password": "Actualizar contrase\u00f1a", "Update the account information.": "Actualiza la informaci\u00f3n de la cuenta.", "Update the balance record.": "Actualiza el registro de balance.", "Update the category and notes for this transaction.": "Actualiza la categor\u00eda y notas de esta transacci\u00f3n.", "Update the category information.": "Actualiza la informaci\u00f3n de la categor\u00eda.", "Update the label information.": "Actualiza la informaci\u00f3n de la etiqueta.", + "Update the owed amount record.": "Actualiza el registro de monto adeudado.", "Update the rule to automatically categorize transactions and add labels.": "Actualiza la regla para categorizar autom\u00e1ticamente transacciones y agregar etiquetas.", "Update the rule to automatically categorize transactions.": "Actualiza la regla para categorizar transacciones autom\u00e1ticamente.", "Update your account's appearance settings": "Actualiza la configuraci\u00f3n de apariencia de tu cuenta", @@ -1282,6 +1305,7 @@ "View Details": "Ver Detalles", "View Transactions": "Ver Transacciones", "View and manage balance records for this account.": "Ver y gestionar registros de balance para esta cuenta.", + "View and manage owed amount records for this account.": "Ver y gestionar registros de monto adeudado para esta cuenta.", "View and manage your bank accounts": "Ver y administrar tus cuentas bancarias", "View and manage your transactions": "Ver y gestionar tus transacciones", "View balance evolution over time": "Ver evoluci\u00f3n del balance a lo largo del tiempo", @@ -1504,8 +1528,10 @@ "accounts": "cuentas", "after 3 months with Whisper Money": "despu\u00e9s de 3 meses con Whisper Money", "amber": "\u00e1mbar", + "balance": "balance", "balance record": "registro de balance", "balance records": "registros de balance", + "balances": "balances", "balances imported": "balances importados", "blue": "azul", "button in your browser toolbar": "en la barra de herramientas de tu navegador", @@ -1542,6 +1568,11 @@ "or you can": "o puedes", "or, enter the code manually": "o, ingresa el c\u00f3digo manualmente", "orange": "naranja", + "owed amount": "monto adeudado", + "owed amount record": "registro de monto adeudado", + "owed amount records": "registros de monto adeudado", + "owed amounts": "montos adeudados", + "owed amounts imported": "montos adeudados importados", "per month": "por mes", "per year": "por a\u00f1o", "pink": "rosa", @@ -1570,7 +1601,6 @@ "vs last month": "vs mes anterior", "vs last period": "vs per\u00edodo anterior", "will be updated or created.": "ser\u00e1 actualizado o creado.", - "will be updated or\\n created.": "ser\u00e1n actualizadas o creadas.", "yellow": "amarillo", "\u2014 $9/month": "\u2014 $9/mes", "\u2190 Back to home": "\u2190 Volver al inicio", diff --git a/resources/js/components/accounts/account-balance-chart.tsx b/resources/js/components/accounts/account-balance-chart.tsx index 98b6d7fd..fd1adbf7 100644 --- a/resources/js/components/accounts/account-balance-chart.tsx +++ b/resources/js/components/accounts/account-balance-chart.tsx @@ -227,6 +227,7 @@ export function AccountBalanceChart({ }: AccountBalanceChartProps) { const locale = useLocale(); const isMobile = useIsMobile(); + const isLoan = account.type === 'loan'; const [granularity, setGranularity] = useState('monthly'); const [balanceData, setBalanceData] = useState( null, @@ -384,7 +385,11 @@ export function AccountBalanceChart({ return ( - {__('Balance evolution')} + + {isLoan + ? __('Owed amount evolution') + : __('Balance evolution')} +
@@ -400,11 +405,17 @@ export function AccountBalanceChart({ return ( - {__('Balance evolution')} + + {isLoan + ? __('Owed amount evolution') + : __('Balance evolution')} +
- {__('No balance data available')} + {isLoan + ? __('No owed amount data available') + : __('No balance data available')}
@@ -416,7 +427,11 @@ export function AccountBalanceChart({
- {__('Balance evolution')} + + {isLoan + ? __('Owed amount evolution') + : __('Balance evolution')} + )} diff --git a/resources/js/components/accounts/account-name.tsx b/resources/js/components/accounts/account-name.tsx index 3c4cba57..1199a400 100644 --- a/resources/js/components/accounts/account-name.tsx +++ b/resources/js/components/accounts/account-name.tsx @@ -10,9 +10,6 @@ interface AccountNameProps { length?: Length; } -export function AccountName({ - account, - className = '', -}: Omit) { +export function AccountName({ account, className = '' }: AccountNameProps) { return {account.name}; } diff --git a/resources/js/components/accounts/balances-modal.tsx b/resources/js/components/accounts/balances-modal.tsx index 13482f37..13ca481f 100644 --- a/resources/js/components/accounts/balances-modal.tsx +++ b/resources/js/components/accounts/balances-modal.tsx @@ -35,7 +35,10 @@ import { } from '@/components/ui/table'; import { useLocale } from '@/hooks/use-locale'; import type { Account, AccountBalance } from '@/types/account'; -import { supportsInvestedAmount } from '@/types/account'; +import { + balanceTermCapitalized, + supportsInvestedAmount, +} from '@/types/account'; import { formatCurrency } from '@/utils/currency'; import { __ } from '@/utils/i18n'; import { Pencil, Trash2 } from 'lucide-react'; @@ -87,6 +90,7 @@ export function BalancesModal({ formatCurrency(valueInCents, account.currency_code, locale); const showInvestedAmount = supportsInvestedAmount(account); + const isLoan = account.type === 'loan'; const fetchBalances = useCallback( async (page: number) => { @@ -236,11 +240,19 @@ export function BalancesModal({ } > - {__('Balance History')} + + {isLoan + ? __('Owed Amount History') + : __('Balance History')} + - {__( - 'View and manage balance records for this account.', - )} + {isLoan + ? __( + 'View and manage owed amount records for this account.', + ) + : __( + 'View and manage balance records for this account.', + )} @@ -257,7 +269,9 @@ export function BalancesModal({ {__('Date')} - {__('Balance')} + {balanceTermCapitalized( + account.type, + )} {showInvestedAmount && ( @@ -289,9 +303,13 @@ export function BalancesModal({ } className="h-24 text-center" > - {__( - 'No balance records found.', - )} + {isLoan + ? __( + 'No owed amount records found.', + ) + : __( + 'No balance records found.', + )} ) : ( @@ -356,8 +374,12 @@ export function BalancesModal({ {total}{' '} {total === 1 - ? __('balance record') - : __('balance records')} + ? isLoan + ? __('owed amount record') + : __('balance record') + : isLoan + ? __('owed amount records') + : __('balance records')}