diff --git a/app/Console/Commands/Concerns/ResolvesFeatures.php b/app/Console/Commands/Concerns/ResolvesFeatures.php index 353a449e..ab94c744 100644 --- a/app/Console/Commands/Concerns/ResolvesFeatures.php +++ b/app/Console/Commands/Concerns/ResolvesFeatures.php @@ -49,6 +49,6 @@ trait ResolvesFeatures private function getStringBasedFeatures(): array { - return ['open-banking', 'account-mapping']; + return ['open-banking', 'account-mapping', 'real-estate']; } } diff --git a/app/Enums/AccountType.php b/app/Enums/AccountType.php index e8c27abf..08502244 100644 --- a/app/Enums/AccountType.php +++ b/app/Enums/AccountType.php @@ -9,6 +9,7 @@ enum AccountType: string case Investment = 'investment'; case Loan = 'loan'; case Retirement = 'retirement'; + case RealEstate = 'real_estate'; case Savings = 'savings'; case Others = 'others'; @@ -24,4 +25,12 @@ enum AccountType: string { return in_array($this, [self::CreditCard, self::Loan], true); } + + /** + * Whether this account type is non-transactional (balance tracking only). + */ + public function isNonTransactional(): bool + { + return in_array($this, [self::Investment, self::Retirement, self::RealEstate], true); + } } diff --git a/app/Enums/PropertyType.php b/app/Enums/PropertyType.php new file mode 100644 index 00000000..b5e1753c --- /dev/null +++ b/app/Enums/PropertyType.php @@ -0,0 +1,12 @@ +where('user_id', $user->id) ->with('bank:id,name,logo') - ->orderByRaw("FIELD(type, 'checking', 'savings', 'investment', 'retirement', 'loan', 'credit_card', 'others')") + ->orderByRaw("FIELD(type, 'checking', 'savings', 'investment', 'retirement', 'real_estate', 'loan', 'credit_card', 'others')") ->orderBy('name') ->get(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code', 'banking_connection_id']); @@ -38,8 +38,35 @@ class AccountController extends Controller $account->load('bank:id,name,logo'); + $data = $account->only(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code', 'banking_connection_id', 'bank']); + + if ($account->type === \App\Enums\AccountType::RealEstate) { + $account->load('realEstateDetail.linkedLoanAccount.bank:id,name,logo'); + $realEstateDetail = $account->realEstateDetail; + + if ($realEstateDetail) { + $data['real_estate_detail'] = [ + ...$realEstateDetail->only([ + 'id', 'property_type', 'address', 'purchase_price', + 'purchase_date', 'area_value', 'area_unit', 'notes', + 'linked_loan_account_id', + ]), + 'linked_loan_account' => $realEstateDetail->linkedLoanAccount + ? $realEstateDetail->linkedLoanAccount->only(['id', 'name', 'name_iv', 'encrypted', 'type', 'currency_code', 'bank']) + : null, + ]; + } + + // Provide available loan accounts for linking + $data['available_loan_accounts'] = $request->user() + ->accounts() + ->where('type', \App\Enums\AccountType::Loan->value) + ->with('bank:id,name,logo') + ->get(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code']); + } + return Inertia::render('Accounts/Show', [ - 'account' => $account->only(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code', 'banking_connection_id', 'bank']), + 'account' => $data, ]); } } diff --git a/app/Http/Controllers/RealEstateDetailController.php b/app/Http/Controllers/RealEstateDetailController.php new file mode 100644 index 00000000..09638b51 --- /dev/null +++ b/app/Http/Controllers/RealEstateDetailController.php @@ -0,0 +1,31 @@ +authorize('update', $account); + + $realEstateDetail = $account->realEstateDetail; + + if (! $realEstateDetail) { + abort(404); + } + + $realEstateDetail->update($request->validated()); + + return to_route('accounts.show', $account); + } +} diff --git a/app/Http/Controllers/Settings/AccountController.php b/app/Http/Controllers/Settings/AccountController.php index dea20f62..b0d8b814 100644 --- a/app/Http/Controllers/Settings/AccountController.php +++ b/app/Http/Controllers/Settings/AccountController.php @@ -40,10 +40,13 @@ class AccountController extends Controller $user = auth()->user(); $validated = $request->validated(); $balance = $validated['balance'] ?? null; - unset($validated['balance']); + + $accountData = collect($validated)->only([ + 'name', 'bank_id', 'currency_code', 'type', + ])->toArray(); $account = $user->accounts()->create([ - ...$validated, + ...$accountData, 'encrypted' => false, 'name_iv' => null, ]); @@ -55,6 +58,18 @@ class AccountController extends Controller ]); } + // Create real estate detail if account type is real_estate + if ($account->type === \App\Enums\AccountType::RealEstate) { + $realEstateData = collect($validated)->only([ + 'property_type', 'address', 'purchase_price', 'purchase_date', + 'area_value', 'area_unit', 'linked_loan_account_id', 'notes', + ])->filter(fn ($value) => $value !== null)->toArray(); + + if (! empty($realEstateData)) { + $account->realEstateDetail()->create($realEstateData); + } + } + // Set user's currency_code from first account if ($user->accounts()->count() === 1) { $user->update(['currency_code' => $account->currency_code]); diff --git a/app/Http/Controllers/Settings/NetWorthChartRealEstatePreferenceController.php b/app/Http/Controllers/Settings/NetWorthChartRealEstatePreferenceController.php new file mode 100644 index 00000000..92f10dd6 --- /dev/null +++ b/app/Http/Controllers/Settings/NetWorthChartRealEstatePreferenceController.php @@ -0,0 +1,20 @@ +user()->setting()->updateOrCreate( + ['user_id' => $request->user()->id], + ['include_real_estate_in_net_worth_chart' => $request->boolean('include_real_estate_in_net_worth_chart')] + ); + + return back(); + } +} diff --git a/app/Http/Middleware/ActivateDevelopmentFeatures.php b/app/Http/Middleware/ActivateDevelopmentFeatures.php index bbb76957..4352031f 100644 --- a/app/Http/Middleware/ActivateDevelopmentFeatures.php +++ b/app/Http/Middleware/ActivateDevelopmentFeatures.php @@ -19,6 +19,7 @@ class ActivateDevelopmentFeatures if (app()->isLocal() && $request->user()) { Feature::for($request->user())->activate('open-banking'); Feature::for($request->user())->activate('account-mapping'); + Feature::for($request->user())->activate('real-estate'); } return $next($request); diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 9f045937..bac993ac 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -86,11 +86,13 @@ class HandleInertiaRequests extends Middleware ], 'chartColorScheme' => $user?->setting?->chart_color_scheme->value ?? 'colorful', 'includeLoansInNetWorthChart' => $user?->setting->include_loans_in_net_worth_chart ?? true, + 'includeRealEstateInNetWorthChart' => $user?->setting->include_real_estate_in_net_worth_chart ?? true, 'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true', 'features' => [ 'cashflow' => true, 'open-banking' => $user ? Feature::for($user)->active('open-banking') : false, 'account-mapping' => $user ? Feature::for($user)->active('account-mapping') : false, + 'real-estate' => $user ? Feature::for($user)->active('real-estate') : false, ], 'accounts' => fn () => $user ? $user->accounts() ->with('bank:id,name,logo') diff --git a/app/Http/Requests/Settings/StoreAccountRequest.php b/app/Http/Requests/Settings/StoreAccountRequest.php index 4b529b0e..a9d4ef46 100644 --- a/app/Http/Requests/Settings/StoreAccountRequest.php +++ b/app/Http/Requests/Settings/StoreAccountRequest.php @@ -3,9 +3,11 @@ namespace App\Http\Requests\Settings; use App\Enums\AccountType; +use App\Enums\PropertyType; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; +use Laravel\Pennant\Feature; class StoreAccountRequest extends FormRequest { @@ -14,6 +16,10 @@ class StoreAccountRequest extends FormRequest */ public function authorize(): bool { + if ($this->input('type') === AccountType::RealEstate->value) { + return Feature::for($this->user())->active('real-estate'); + } + return true; } @@ -24,9 +30,13 @@ class StoreAccountRequest extends FormRequest */ public function rules(): array { - return [ + $isRealEstate = $this->input('type') === AccountType::RealEstate->value; + + $rules = [ 'name' => ['required', 'string'], - 'bank_id' => ['required', 'exists:banks,id'], + 'bank_id' => $isRealEstate + ? ['nullable', 'exists:banks,id'] + : ['required', 'exists:banks,id'], 'currency_code' => [ 'required', 'string', @@ -39,5 +49,31 @@ class StoreAccountRequest extends FormRequest ], 'balance' => ['nullable', 'integer'], ]; + + if ($isRealEstate) { + $rules = array_merge($rules, [ + 'property_type' => [ + 'required', + 'string', + Rule::in(array_map(fn ($type) => $type->value, PropertyType::cases())), + ], + 'address' => ['nullable', 'string', 'max:500'], + 'purchase_price' => ['nullable', 'integer', 'min:0'], + 'purchase_date' => ['nullable', 'date', 'before_or_equal:today'], + 'area_value' => ['nullable', 'numeric', 'min:0', 'max:99999999.99'], + 'area_unit' => ['nullable', 'string', Rule::in(['sqm', 'sqft', 'acres', 'hectares'])], + 'linked_loan_account_id' => [ + 'nullable', + 'string', + Rule::exists('accounts', 'id')->where(function ($query) { + $query->where('user_id', $this->user()->id) + ->where('type', AccountType::Loan->value); + }), + ], + 'notes' => ['nullable', 'string', 'max:2000'], + ]); + } + + return $rules; } } diff --git a/app/Http/Requests/Settings/UpdateNetWorthChartRealEstatePreferenceRequest.php b/app/Http/Requests/Settings/UpdateNetWorthChartRealEstatePreferenceRequest.php new file mode 100644 index 00000000..83deb8e1 --- /dev/null +++ b/app/Http/Requests/Settings/UpdateNetWorthChartRealEstatePreferenceRequest.php @@ -0,0 +1,28 @@ +|string> + */ + public function rules(): array + { + return [ + 'include_real_estate_in_net_worth_chart' => ['required', 'boolean'], + ]; + } +} diff --git a/app/Http/Requests/StoreRealEstateDetailRequest.php b/app/Http/Requests/StoreRealEstateDetailRequest.php new file mode 100644 index 00000000..08bd60dc --- /dev/null +++ b/app/Http/Requests/StoreRealEstateDetailRequest.php @@ -0,0 +1,50 @@ +|string> + */ + public function rules(): array + { + return [ + 'property_type' => [ + 'required', + 'string', + Rule::in(array_map(fn ($type) => $type->value, PropertyType::cases())), + ], + 'address' => ['nullable', 'string', 'max:500'], + 'purchase_price' => ['nullable', 'integer', 'min:0'], + 'purchase_date' => ['nullable', 'date', 'before_or_equal:today'], + 'area_value' => ['nullable', 'numeric', 'min:0', 'max:99999999.99'], + 'area_unit' => ['nullable', 'string', Rule::in(['sqm', 'sqft', 'acres', 'hectares'])], + 'linked_loan_account_id' => [ + 'nullable', + 'string', + Rule::exists('accounts', 'id')->where(function ($query) { + $query->where('user_id', $this->user()->id) + ->where('type', AccountType::Loan->value); + }), + ], + 'notes' => ['nullable', 'string', 'max:2000'], + ]; + } +} diff --git a/app/Http/Requests/UpdateRealEstateDetailRequest.php b/app/Http/Requests/UpdateRealEstateDetailRequest.php new file mode 100644 index 00000000..49afbeb5 --- /dev/null +++ b/app/Http/Requests/UpdateRealEstateDetailRequest.php @@ -0,0 +1,51 @@ +|string> + */ + public function rules(): array + { + return [ + 'property_type' => [ + 'sometimes', + 'required', + 'string', + Rule::in(array_map(fn ($type) => $type->value, PropertyType::cases())), + ], + 'address' => ['nullable', 'string', 'max:500'], + 'purchase_price' => ['nullable', 'integer', 'min:0'], + 'purchase_date' => ['nullable', 'date', 'before_or_equal:today'], + 'area_value' => ['nullable', 'numeric', 'min:0', 'max:99999999.99'], + 'area_unit' => ['nullable', 'string', Rule::in(['sqm', 'sqft', 'acres', 'hectares'])], + 'linked_loan_account_id' => [ + 'nullable', + 'string', + Rule::exists('accounts', 'id')->where(function ($query) { + $query->where('user_id', $this->user()->id) + ->where('type', AccountType::Loan->value); + }), + ], + 'notes' => ['nullable', 'string', 'max:2000'], + ]; + } +} diff --git a/app/Models/Account.php b/app/Models/Account.php index e4ce7854..e33622c7 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\SoftDeletes; /** @@ -71,6 +72,12 @@ class Account extends Model return $this->belongsTo(BankingConnection::class); } + /** @return HasOne */ + public function realEstateDetail(): HasOne + { + return $this->hasOne(RealEstateDetail::class); + } + public function isConnected(): bool { return $this->banking_connection_id !== null; diff --git a/app/Models/RealEstateDetail.php b/app/Models/RealEstateDetail.php new file mode 100644 index 00000000..8da3d96b --- /dev/null +++ b/app/Models/RealEstateDetail.php @@ -0,0 +1,53 @@ + */ + use HasFactory, HasUuids; + + protected $fillable = [ + 'account_id', + 'linked_loan_account_id', + 'property_type', + 'address', + 'purchase_price', + 'purchase_date', + 'area_value', + 'area_unit', + 'notes', + ]; + + protected function casts(): array + { + return [ + 'property_type' => PropertyType::class, + 'purchase_price' => 'integer', + 'purchase_date' => 'date', + 'area_value' => 'decimal:2', + ]; + } + + /** @return BelongsTo */ + public function account(): BelongsTo + { + return $this->belongsTo(Account::class); + } + + /** @return BelongsTo */ + public function linkedLoanAccount(): BelongsTo + { + return $this->belongsTo(Account::class, 'linked_loan_account_id'); + } +} diff --git a/app/Models/UserSetting.php b/app/Models/UserSetting.php index 63d1e318..4e2afbd5 100644 --- a/app/Models/UserSetting.php +++ b/app/Models/UserSetting.php @@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; /** * @property \App\Enums\ChartColorScheme $chart_color_scheme * @property bool $include_loans_in_net_worth_chart + * @property bool $include_real_estate_in_net_worth_chart */ class UserSetting extends Model { @@ -21,6 +22,7 @@ class UserSetting extends Model 'user_id', 'chart_color_scheme', 'include_loans_in_net_worth_chart', + 'include_real_estate_in_net_worth_chart', ]; protected function casts(): array @@ -28,6 +30,7 @@ class UserSetting extends Model return [ 'chart_color_scheme' => ChartColorScheme::class, 'include_loans_in_net_worth_chart' => 'boolean', + 'include_real_estate_in_net_worth_chart' => 'boolean', ]; } diff --git a/app/Policies/RealEstateDetailPolicy.php b/app/Policies/RealEstateDetailPolicy.php new file mode 100644 index 00000000..59af7a14 --- /dev/null +++ b/app/Policies/RealEstateDetailPolicy.php @@ -0,0 +1,33 @@ +id === $realEstateDetail->account->user_id; + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, RealEstateDetail $realEstateDetail): bool + { + return $user->id === $realEstateDetail->account->user_id; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, RealEstateDetail $realEstateDetail): bool + { + return $user->id === $realEstateDetail->account->user_id; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 89b51732..3488e137 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -52,5 +52,6 @@ class AppServiceProvider extends ServiceProvider Feature::define('open-banking', fn (User $user) => false); Feature::define('account-mapping', fn (User $user) => false); + Feature::define('real-estate', fn (User $user) => false); } } diff --git a/database/factories/AccountFactory.php b/database/factories/AccountFactory.php index 1bee139c..985d5528 100644 --- a/database/factories/AccountFactory.php +++ b/database/factories/AccountFactory.php @@ -47,4 +47,12 @@ class AccountFactory extends Factory 'linked_at' => now(), ]); } + + public function realEstate(): static + { + return $this->state(fn (array $attributes) => [ + 'type' => AccountType::RealEstate, + 'bank_id' => null, + ]); + } } diff --git a/database/factories/RealEstateDetailFactory.php b/database/factories/RealEstateDetailFactory.php new file mode 100644 index 00000000..3d8ba7bd --- /dev/null +++ b/database/factories/RealEstateDetailFactory.php @@ -0,0 +1,50 @@ + + */ +class RealEstateDetailFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'account_id' => Account::factory()->state(['type' => AccountType::RealEstate]), + 'property_type' => fake()->randomElement(PropertyType::cases()), + 'address' => fake()->address(), + 'purchase_price' => fake()->numberBetween(10000000, 100000000), + 'purchase_date' => fake()->dateTimeBetween('-10 years', 'now'), + 'area_value' => fake()->randomFloat(2, 50, 500), + 'area_unit' => fake()->randomElement(['sqm', 'sqft']), + 'notes' => fake()->optional()->sentence(), + ]; + } + + /** + * State for a property with a linked loan account. + */ + public function withLinkedLoan(?Account $loanAccount = null): static + { + return $this->state(function (array $attributes) use ($loanAccount) { + $loan = $loanAccount ?? Account::factory()->state([ + 'type' => AccountType::Loan, + 'user_id' => Account::find($attributes['account_id'])?->user_id, + ]); + + return [ + 'linked_loan_account_id' => $loan instanceof Account ? $loan->id : $loan, + ]; + }); + } +} diff --git a/database/factories/UserSettingFactory.php b/database/factories/UserSettingFactory.php index d38859d0..4d0e9a25 100644 --- a/database/factories/UserSettingFactory.php +++ b/database/factories/UserSettingFactory.php @@ -22,6 +22,7 @@ class UserSettingFactory extends Factory 'user_id' => User::factory(), 'chart_color_scheme' => ChartColorScheme::Colorful, 'include_loans_in_net_worth_chart' => true, + 'include_real_estate_in_net_worth_chart' => true, ]; } diff --git a/database/migrations/2026_03_20_110659_create_real_estate_details_table.php b/database/migrations/2026_03_20_110659_create_real_estate_details_table.php new file mode 100644 index 00000000..0baf7dbf --- /dev/null +++ b/database/migrations/2026_03_20_110659_create_real_estate_details_table.php @@ -0,0 +1,36 @@ +char('id', 36)->primary(); + $table->foreignUuid('account_id')->unique()->constrained()->onDelete('cascade'); + $table->foreignUuid('linked_loan_account_id')->nullable()->constrained('accounts')->onDelete('set null'); + $table->string('property_type'); + $table->text('address')->nullable(); + $table->bigInteger('purchase_price')->nullable(); + $table->date('purchase_date')->nullable(); + $table->decimal('area_value', 12, 2)->nullable(); + $table->string('area_unit', 20)->nullable(); + $table->text('notes')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('real_estate_details'); + } +}; diff --git a/database/migrations/2026_03_20_132058_make_bank_id_nullable_on_accounts_table.php b/database/migrations/2026_03_20_132058_make_bank_id_nullable_on_accounts_table.php new file mode 100644 index 00000000..105bd1b9 --- /dev/null +++ b/database/migrations/2026_03_20_132058_make_bank_id_nullable_on_accounts_table.php @@ -0,0 +1,32 @@ +dropForeign('accounts_bank_id_foreign'); + $table->char('bank_id', 36)->nullable()->change(); + $table->foreign('bank_id')->references('id')->on('banks')->cascadeOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('accounts', function (Blueprint $table) { + $table->dropForeign(['bank_id']); + $table->char('bank_id', 36)->nullable(false)->change(); + $table->foreign('bank_id')->references('id')->on('banks')->cascadeOnDelete(); + }); + } +}; diff --git a/database/migrations/2026_03_20_154755_add_include_real_estate_in_net_worth_chart_to_user_settings_table.php b/database/migrations/2026_03_20_154755_add_include_real_estate_in_net_worth_chart_to_user_settings_table.php new file mode 100644 index 00000000..88e6f6f9 --- /dev/null +++ b/database/migrations/2026_03_20_154755_add_include_real_estate_in_net_worth_chart_to_user_settings_table.php @@ -0,0 +1,28 @@ +boolean('include_real_estate_in_net_worth_chart')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('user_settings', function (Blueprint $table) { + $table->dropColumn('include_real_estate_in_net_worth_chart'); + }); + } +}; diff --git a/lang/es.json b/lang/es.json index 4a4183aa..238b1fb5 100644 --- a/lang/es.json +++ b/lang/es.json @@ -125,6 +125,8 @@ "Add the app to your home screen for the best experience.": "A\u00f1ade la app a tu pantalla de inicio para la mejor experiencia.", "Add to Home Screen": "A\u00f1adir a pantalla de inicio", "Add transaction": "Agregar transacci\u00f3n", + "Additional notes about this property": "Notas adicionales sobre esta propiedad", + "Address": "Direcci\u00f3n", "Address:": "Direcci\u00f3n:", "Alex T.": "Alex T.", "All": "Todo", @@ -166,6 +168,7 @@ "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?", + "Area": "Superficie", "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.", "As a solo founder, your feedback directly shapes what I build next. I personally read every response and take your suggestions seriously. When you subscribe, you're not just supporting some big corporation - you're helping me continue building something I'm passionate about.": "Como fundador en solitario, tus comentarios moldean directamente lo que construyo a continuaci\u00f3n. Leo personalmente cada respuesta y tomo tus sugerencias en serio. Cuando te suscribes, no est\u00e1s apoyando a una gran corporaci\u00f3n, est\u00e1s ayud\u00e1ndome a seguir construyendo algo que me apasiona.", "As a user in the European Union, you have the following rights regarding your personal data:": "Como usuario en la Uni\u00f3n Europea, tienes los siguientes derechos con respecto a tus datos personales:", @@ -197,13 +200,13 @@ "Balance Date": "Fecha del Balance", "Balance History": "Historial de Balance", "Balance Tracking": "Seguimiento de Balance", - "Balances": "Balances", "Balance evolution": "Evoluci\u00f3n del balance", + "Balances": "Balances", "Bank": "Banco", "Bank Account": "Cuenta Bancaria", - "Bank account reconnected successfully.": "Cuenta bancaria reconectada exitosamente.", "Bank Connections": "Conexiones Bancarias", "Bank Name": "Nombre del Banco", + "Bank account reconnected successfully.": "Cuenta bancaria reconectada exitosamente.", "Bank accounts": "Cuentas bancarias", "Bank connections automatically sync your transactions directly from your bank. This feature requires the Standard Plan.": "Las conexiones bancarias sincronizan autom\u00e1ticamente tus transacciones desde tu banco. Esta funci\u00f3n requiere el Plan Standard.", "Bank logo preview": "Vista previa del logo del banco", @@ -221,13 +224,13 @@ "Blue": "Azul", "Browse Files": "Explorar Archivos", "Budget Name": "Nombre del Presupuesto", + "Budget Spending": "Gasto del Presupuesto", + "Budget Tracking": "Seguimiento de Presupuesto", "Budget alert and limit adjustment": "Alerta de presupuesto y ajuste de l\u00edmite", "Budget goals by category": "Objetivos de presupuesto por categor\u00eda", "Budget insights and alerts": "Informes y alertas de presupuesto", "Budget progress for Groceries": "Progreso del presupuesto de Supermercado", "Budget resets to zero at the start of each period.": "El presupuesto se reinicia a cero al inicio de cada per\u00edodo.", - "Budget Spending": "Gasto del Presupuesto", - "Budget Tracking": "Seguimiento de Presupuesto", "Budgets": "Presupuestos", "Build better budgets": "Crea mejores presupuestos", "Built by two people, for real people": "Construido por dos personas, para personas reales", @@ -239,11 +242,14 @@ "Can I export or delete my data?": "\u00bfPuedo exportar o eliminar mis datos?", "Cancel": "Cancelar", "Carry Over": "Acumular", + "Cash inflow": "Entrada de efectivo", + "Cash outflow": "Salida de efectivo", "Cashflow": "Flujo de Caja", "Cashflow Trend": "Tendencia del Flujo de Caja", + "Cashflow analytics": "Analitica de flujo de caja", "Cashflow at a Glance": "Flujo de Caja de un Vistazo", - "Cashflow visualization": "Visualizaci\u00f3n del flujo de caja", "Cashflow income vs expenses bar chart": "Gr\u00e1fico de barras de ingresos frente a gastos", + "Cashflow visualization": "Visualizaci\u00f3n del flujo de caja", "Categories": "Categor\u00edas", "Categories below 5% of total": "Categor\u00edas por debajo del 5% del total", "Categories settings": "Configuraci\u00f3n de categor\u00edas", @@ -263,16 +269,17 @@ "Check your inbox \u2014 we've sent you an email.": "Revisa tu bandeja de entrada \u2014 te hemos enviado un correo.", "Checking": "Cuenta Corriente", "Choose how to handle each account from :bank. You can create new accounts, link to existing ones, or skip.": "Elige c\u00f3mo gestionar cada cuenta de :bank. Puedes crear nuevas cuentas, vincularlas a las existentes u omitirlas.", + "Choose how to report this transfer": "Elige como mostrar esta transferencia", "Choose how you want to add your account.": "Elige c\u00f3mo quieres a\u00f1adir tu cuenta.", "Choose the account where transactions will be imported": "Elige la cuenta donde se importar\u00e1n las transacciones", "Choose the color palette for your charts": "Elige la paleta de colores para tus gr\u00e1ficos", "Choose the plan that works for you": "Elige el plan que funcione para ti", "Choose the plan that works for you. No hidden fees.": "Elige el plan que mejor se adapte a ti. Sin tarifas ocultas.", "Choose your billing cycle": "Elige tu ciclo de facturaci\u00f3n", + "City Electric Co.": "City Electric Co.", "Claim Your Discount": "Reclama Tu Descuento", "Clean interface, powerful features, and zero compromise on privacy. What more could you want?": "Interfaz limpia, funciones potentes y cero compromisos con la privacidad. \u00bfQu\u00e9 m\u00e1s podr\u00edas querer?", "Clear": "Limpiar", - "City Electric Co.": "City Electric Co.", "Clear Encryption Key?": "\u00bfBorrar Clave de Encriptaci\u00f3n?", "Clear Key": "Borrar Clave", "Clear selection": "Limpiar selecci\u00f3n", @@ -286,6 +293,7 @@ "Color": "Color", "Colorful": "Colorido", "Columns": "Columnas", + "Commercial": "Comercial", "Common Questions": "Preguntas Frecuentes", "Community": "Comunidad", "Company Name:": "Nombre de la Empresa:", @@ -302,11 +310,11 @@ "Connect Bank Account": "Conectar Cuenta Bancaria", "Connect Your Bank": "Conecta tu banco", "Connect Your Banks": "Conecta tus bancos", + "Connect bank accounts": "Conectar cuentas bancarias", "Connect in seconds": "Conecta en segundos", "Connect your Binance account using your API Key and Secret.": "Conecta tu cuenta de Binance usando tu Clave API y Secreto.", "Connect your Bitpanda account using your API Key.": "Conecta tu cuenta de Bitpanda usando tu Clave API.", "Connect your Indexa Capital account using your API token.": "Conecta tu cuenta de Indexa Capital usando tu token API.", - "Connect bank accounts": "Conectar cuentas bancarias", "Connect your bank accounts": "Conecta tus cuentas bancarias", "Connect your bank accounts, savings, investments, and more \u2014 all in a single dashboard. No more switching between apps or losing track of accounts.": "Conecta tus cuentas bancarias, ahorros, inversiones y m\u00e1s, todo en un \u00fanico panel. Sin m\u00e1s cambiar entre apps ni perder el control de tus cuentas.", "Connect your bank and sync transactions automatically.": "Conecta tu banco y sincroniza las transacciones autom\u00e1ticamente.", @@ -396,8 +404,8 @@ "Delete account": "Eliminar cuenta", "Delete accounts": "Eliminar cuentas", "Delete balance": "Eliminar balance", - "Delete owed amount": "Eliminar monto adeudado", "Delete budget": "Eliminar presupuesto", + "Delete owed amount": "Eliminar monto adeudado", "Delete your account and all of its resources": "Elimina tu cuenta y todos sus recursos", "Deleting...": "Eliminando...", "Demo encryption password:": "Contrase\u00f1a de encriptaci\u00f3n de demo:", @@ -414,6 +422,7 @@ "Disconnecting...": "Desconectando...", "Discord": "Discord", "Discord for 80% off": "Discord con 80% de descuento", + "Do not show": "No mostrar", "Do you connect directly to my bank?": "\u00bfOs conect\u00e1is directamente a mi banco?", "Don't have an account?": "\u00bfNo tienes una cuenta?", "Download as CSV or Excel format": "Descargar en formato CSV o Excel", @@ -429,13 +438,13 @@ "Edit Balance": "Editar Balance", "Edit Budget": "Editar Presupuesto", "Edit Category": "Editar Categor\u00eda", - "Edit Owed Amount": "Editar Monto Adeudado", "Edit Label": "Editar Etiqueta", + "Edit Owed Amount": "Editar Monto Adeudado", + "Edit Property Details": "Editar Detalles de Propiedad", "Edit Transaction": "Editar transacci\u00f3n", "Edit account": "Editar cuenta", "Edit budget": "Editar presupuesto", "Electricity": "Electricidad", - "Emergency Fund": "Fondo de Emergencia", "Email": "Correo Electr\u00f3nico", "Email address": "Direcci\u00f3n de correo electr\u00f3nico", "Email address, name, and password (encrypted)": "Direcci\u00f3n de correo electr\u00f3nico, nombre y contrase\u00f1a (encriptados)", @@ -443,6 +452,7 @@ "Email verification": "Verificaci\u00f3n de correo electr\u00f3nico", "Email:": "Correo:", "Email\\n address, name, and password (encrypted)": "Direcci\u00f3n de correo electr\u00f3nico, nombre y contrase\u00f1a (cifrada)", + "Emergency Fund": "Fondo de Emergencia", "Emma L.": "Emma L.", "Enable 2FA": "Activar 2FA", "Enable privacy mode": "Activar modo privado", @@ -471,8 +481,8 @@ "Enter your encryption password to decrypt transactions information and accounts name.": "Ingresa tu contrase\u00f1a de encriptaci\u00f3n para descifrar la informaci\u00f3n de transacciones y nombres de cuentas.", "Enter your encryption password to decrypt your transactions information.": "Introduce tu contrase\u00f1a de cifrado para descifrar la informaci\u00f3n de tus transacciones.", "Enter your new API credentials for :provider.": "Introduce tus nuevas credenciales API para :provider.", - "Entertainment (Movies, Sports, Hobbies)": "Entretenimiento (Pel\u00edculas, Deportes, Hobbies)", "Entertainment": "Entretenimiento", + "Entertainment (Movies, Sports, Hobbies)": "Entretenimiento (Pel\u00edculas, Deportes, Hobbies)", "Entertainment \u00b7 Only $8 remaining this month": "Entretenimiento \u00b7 Solo $8 restante este mes", "Entertainment \u2192 $100": "Entretenimiento \u2192 $100", "Error": "Error", @@ -532,9 +542,9 @@ "Forgot password": "Olvid\u00e9 mi contrase\u00f1a", "Forgot password?": "\u00bfOlvidaste tu contrase\u00f1a?", "Founders of Whisper Money": "Fundadores de Whisper Money", + "Free": "Gratis", "Freelance Invoice": "Factura Freelance", "Frequently Asked Questions": "Preguntas Frecuentes", - "Free": "Gratis", "From": "Desde", "Full name": "Nombre completo", "Fully transparent and open source. Review the code yourself.": "Totalmente transparente y de c\u00f3digo abierto. Revisa el c\u00f3digo t\u00fa mismo.", @@ -549,18 +559,18 @@ "Get started at no cost. No bank connections included.": "Empieza sin coste. Sin conexiones bancarias incluidas.", "Get started quickly with your existing financial data.": "Comienza r\u00e1pidamente con tus datos financieros existentes.", "Github": "Github", - "Gold Card": "Tarjeta Oro", "Go to Dashboard": "Ir al Panel", "Go to your account's transaction history": "Ve al historial de transacciones de tu cuenta", "Go to your dashboard and click \"Import Transactions\". Select your CSV file and I'll map the columns automatically.": "Ve a tu panel y haz clic en \"Importar Transacciones\". Selecciona tu archivo CSV y mapear\u00e9 las columnas autom\u00e1ticamente.", "Go to your dashboard and click \"Import Transactions\". Select your CSV file and we'll map the columns automatically.": "Ve a tu panel y haz clic en \"Importar Transacciones\". Selecciona tu archivo CSV y mapearemos las columnas autom\u00e1ticamente.", + "Gold Card": "Tarjeta Oro", "Good": "Buena", "Good progress on your savings.": "Buen progreso en tus ahorros.", "Got it": "Entendido", "Great Progress!": "\u00a1Gran Progreso!", "Great job! You're saving well.": "\u00a1Excelente trabajo! Est\u00e1s ahorrando bien.", - "Groups joined by:": "Grupos unidos por:", "Groceries": "Supermercado", + "Groups joined by:": "Grupos unidos por:", "Health & Wellness (Medical, Pharmacy, Fitness)": "Salud y Bienestar (M\u00e9dico, Farmacia, Gimnasio)", "Health and pharmaceuticals": "Salud y farmacia", "Healthy Pharmacy": "Healthy Pharmacy", @@ -627,19 +637,20 @@ "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 market values": "Importar valores de mercado", + "Import owed amounts": "Importar montos adeudados", "Import transactions": "Importar transacciones", "Import transactions for your account. You can export transaction history from your bank's website.": "Importa transacciones para tu cuenta. Puedes exportar el historial de transacciones desde la web de tu banco.", - "Import transactions from a CSV file": "Importar transacciones desde un archivo CSV", "Import transactions from CSV or Excel files": "Importar transacciones desde archivos CSV o Excel", "Import transactions from CSV/Excel": "Importar transacciones desde CSV/Excel", + "Import transactions from a CSV file": "Importar transacciones desde un archivo CSV", "Import transactions in seconds": "Importa transacciones en segundos", "Import your transaction history to start tracking your finances.": "Importa tu historial de transacciones para empezar a rastrear tus finanzas.", + "Imported": "Importado", "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", @@ -676,11 +687,13 @@ "Keep me logged in": "Mantenerme conectado", "Keep me logged in (convenient)": "Mantenerme conectado (conveniente)", "Keep me logged in (less secure)": "Mantenerme conectado (menos seguro)", + "Keep this transfer out of cashflow analytics.": "Mantiene esta transferencia fuera de la analitica de flujo de caja.", "Key Storage": "Almacenamiento de Clave", "Label (Optional)": "Etiqueta (Opcional)", "Label name": "Nombre de etiqueta", "Labels": "Etiquetas", "Labels settings": "Configuraci\u00f3n de etiquetas", + "Land": "Terreno", "Language": "Idioma", "Last Login": "\u00daltimo acceso", "Last period:": "Per\u00edodo anterior:", @@ -699,8 +712,10 @@ "Lightning fast": "Ultrarr\u00e1pido", "Lightning-Fast CSV/XLS Import": "Importaci\u00f3n Ultrarr\u00e1pida de CSV/XLS", "Limit adjusted": "L\u00edmite ajustado", + "Link a loan account to track equity (market value minus owed amount).": "Vincula una cuenta de pr\u00e9stamo para rastrear el patrimonio (valor de mercado menos monto adeudado).", "Link to existing account": "Vincular a cuenta existente", "Link your bank accounts directly. Transactions sync automatically, giving you a real-time view of your finances.": "Vincula tus cuentas bancarias directamente. Las transacciones se sincronizan autom\u00e1ticamente, d\u00e1ndote una vista en tiempo real de tus finanzas.", + "Linked Mortgage / Loan": "Hipoteca / Pr\u00e9stamo Vinculado", "Load more": "Cargar m\u00e1s", "Loading": "Cargando", "Loading last balance...": "Cargando \u00faltimo balance...", @@ -746,6 +761,8 @@ "Map Accounts": "Mapear Cuentas", "Map Bank Accounts": "Mapear Cuentas Bancarias", "Map Columns": "Mapear Columnas", + "Market Value": "Valor de Mercado", + "Market Values": "Valores de Mercado", "Match your file columns to transaction fields": "Asocia las columnas de tu archivo con los campos de transacci\u00f3n", "Max": "M\u00e1x", "Maybe later": "Quiz\u00e1s m\u00e1s tarde", @@ -759,6 +776,7 @@ "Month": "Mes", "Monthly": "Mensual", "Monthly income, expenses, and net cashflow over the last 12 months": "Ingresos, gastos y flujo de efectivo neto mensual durante los \u00faltimos 12 meses", + "Monthly income, expenses, tracked transfers, and net cashflow over the last 12 months": "Ingresos, gastos, transferencias seguidas y flujo de caja neto mensual durante los ultimos 12 meses", "More": "M\u00e1s", "More actions": "M\u00e1s acciones", "More options": "M\u00e1s opciones", @@ -811,6 +829,7 @@ "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 linked loan": "Sin pr\u00e9stamo vinculado", "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", @@ -833,8 +852,6 @@ "Once verified, you'll be able to set up your encryption key and start tracking your finances with full privacy.": "Una vez verificado, podr\u00e1s configurar tu clave de encriptaci\u00f3n y empezar a rastrear tus finanzas con total privacidad.", "Once your account is deleted, all of its resources and data will also be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.": "Una vez que tu cuenta sea eliminada, todos sus recursos y datos tambi\u00e9n ser\u00e1n eliminados permanentemente. Por favor ingresa tu contrase\u00f1a para confirmar que deseas eliminar permanentemente tu cuenta.", "Once your account is deleted, all of its resources\\n and data will also be permanently deleted. Please\\n enter your password to confirm you would like to\\n permanently delete your account.": "Una vez que tu cuenta sea eliminada, todos sus recursos y datos tambi\u00e9n ser\u00e1n eliminados permanentemente. Por favor, introduce tu contrase\u00f1a para confirmar que deseas eliminar permanentemente tu cuenta.", - "only with you": "solo contigo", - "Quartely": "Cada 4 meses", "Only you know this password. It never leaves your device.": "Solo t\u00fa conoces esta contrase\u00f1a. Nunca sale de tu dispositivo.", "Open menu": "Abrir men\u00fa", "Open source": "C\u00f3digo abierto", @@ -874,10 +891,10 @@ "Percentage of income saved": "Porcentaje de ingresos ahorrados", "Perfect for investment portfolios and retirement accounts": "Perfecto para carteras de inversi\u00f3n y cuentas de jubilaci\u00f3n", "Perfect for investment portfolios and retirement\\n accounts": "Perfecto para carteras de inversi\u00f3n y cuentas de jubilaci\u00f3n", - "Personal transfers": "Transferencias personales", "Period": "Per\u00edodo", "Period Duration (days)": "Duraci\u00f3n del Per\u00edodo (d\u00edas)", "Period Type": "Tipo de Per\u00edodo", + "Personal transfers": "Transferencias personales", "Pink": "Rosa", "Platform": "Plataforma", "Please enter a balance": "Por favor, ingresa un balance", @@ -920,11 +937,19 @@ "Pro Yearly": "Pro Anual", "Profile Settings": "Configuraci\u00f3n del Perfil", "Profile information": "Informaci\u00f3n del perfil", + "Property Details": "Detalles de Propiedad", + "Property Type": "Tipo de Propiedad", + "Property address": "Direcci\u00f3n de la propiedad", "Protect your privacy with no tracking or third-party analytics.": "Protege tu privacidad sin rastreo ni an\u00e1lisis de terceros.", - "Update categories automatically": "Actualizar categor\u00edas autom\u00e1ticamente", + "Purchase Date": "Fecha de Compra", + "Purchase Price": "Precio de Compra", + "Quartely": "Cada 4 meses", "Re-evaluate rules": "Reevaluar reglas", "Reactivate Your Subscription": "Reactiva Tu Suscripci\u00f3n", "Ready to take control of your finances?": "\u00bfListo para tomar el control de tus finanzas?", + "Real Estate": "Bienes Ra\u00edces", + "Include real estate": "Incluir bienes ra\u00edces", + "Include real estate assets in the net worth totals and chart": "Incluir bienes ra\u00edces en los totales y gr\u00e1fico de patrimonio neto", "Reconnect": "Reconectar", "Recovery Codes": "C\u00f3digos de Recuperaci\u00f3n", "Recovery codes": "C\u00f3digos de recuperaci\u00f3n", @@ -939,11 +964,12 @@ "Remaining balance returns to available money pool": "El saldo restante vuelve al fondo de dinero disponible", "Remember me": "Recu\u00e9rdame", "Remove all labels": "Eliminar todas las etiquetas", + "Rent Payment": "Pago de Alquiler", + "Rent and maintanence": "Alquiler y mantenimiento", "Resend verification email": "Reenviar correo de verificaci\u00f3n", "Reset password": "Restablecer contrase\u00f1a", "Reset/Pool": "Reiniciar/Fondo", - "Rent Payment": "Pago de Alquiler", - "Rent and maintanence": "Alquiler y mantenimiento", + "Residential": "Residencial", "Retirement": "Jubilaci\u00f3n", "Retirement / Pension": "Jubilaci\u00f3n / Pensi\u00f3n", "Retry": "Reintentar", @@ -971,18 +997,18 @@ "Sarah M.": "Sarah M.", "Save": "Guardar", "Save & Sync": "Guardar y Sincronizar", + "Save :percent%": "Ahorra :percent%", "Save Balance": "Guardar Balance", "Save Changes": "Guardar Cambios", "Save money for goals": "Ahorra dinero para tus metas", - "Save :percent%": "Ahorra :percent%", "Save password": "Guardar contrase\u00f1a", "Saved": "Guardado", "Saving": "Ahorro", "Saving...": "Guardando...", "Savings": "Ahorros", "Savings Rate": "Tasa de Ahorro", - "Savings rate": "Tasa de ahorro", "Savings Transfer": "Transferencia a Ahorros", + "Savings rate": "Tasa de ahorro", "Search": "Buscar", "Search bank...": "Buscar banco...", "Search banks...": "Buscar bancos...", @@ -994,11 +1020,12 @@ "Search, filter, and categorize with ease. Understand exactly where your money goes.": "Busca, filtra y categoriza con facilidad. Entiende exactamente a d\u00f3nde va tu dinero.", "Search, move": "Buscar, mover", "Searching...": "Buscando...", - "Secure Storage:": "Almacenamiento Seguro:", "Secure & encrypted": "Seguro y cifrado", + "Secure Storage:": "Almacenamiento Seguro:", "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 market values": "Ver valores de mercado", "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.", @@ -1020,7 +1047,6 @@ "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", @@ -1030,8 +1056,11 @@ "Select labels (optional)": "Seleccionar etiquetas (opcional)", "Select labels...": "Seleccionar etiquetas...", "Select language": "Seleccionar idioma", + "Select owed amount column": "Seleccionar columna de monto adeudado", + "Select property type": "Seleccionar tipo de propiedad", "Select row": "Seleccionar fila", "Select the country where your bank is located.": "Selecciona el pa\u00eds donde est\u00e1 ubicado tu banco.", + "Select unit": "Seleccionar unidad", "Select your bank.": "Selecciona tu banco.", "Select your country and bank to get started.": "Selecciona tu pa\u00eds y banco para empezar.", "Selected": "Seleccionado", @@ -1065,6 +1094,8 @@ "Shared": "Compartido", "Shared with": "Compartido con", "Shopping (Clothing, Electronics, Gifts)": "Compras (Ropa, Electr\u00f3nica, Regalos)", + "Show as cash inflow": "Mostrar como entrada de efectivo", + "Show as cash outflow": "Mostrar como salida de efectivo", "Sign up": "Crear cuenta", "Simple, transparent pricing": "Precios simples y transparentes", "Skip": "Saltar", @@ -1087,6 +1118,7 @@ "Spent:": "Gastado:", "Spot savings opportunities": "Identifica oportunidades de ahorro", "Square images only (max": "Solo im\u00e1genes cuadradas (m\u00e1x", + "Standard": "Estandar", "Standard Plan required": "Plan Standard requerido", "Start Day": "D\u00eda de inicio", "Start Day of Month": "D\u00eda de inicio del mes", @@ -1153,8 +1185,8 @@ "The most secure personal finance app. Your data is never shared with third parties. Track expenses, create budgets, and manage your money privately.": "La app m\u00e1s segura de finanzas personales. Tus datos nunca se comparten con terceros. Rastrea gastos, crea presupuestos y administra tu dinero de forma privada.", "The most secure privacy-first personal finance app. Track expenses, create budgets, and manage your money privately.": "La aplicaci\u00f3n de finanzas personales m\u00e1s segura y centrada en la privacidad. Realiza un seguimiento de gastos, crea presupuestos y gestiona tu dinero de forma privada.", "The most secure way to understand your finances": "La forma m\u00e1s segura de entender tus finanzas", - "There are different account types. Some track transactions, others just track balance over time.": "Hay diferentes tipos de cuenta. Algunas rastrean transacciones, otras solo rastrean el balance a lo largo del tiempo.", "Theatre, music, cinema": "Teatro, m\u00fasica, cine", + "There are different account types. Some track transactions, others just track balance over time.": "Hay diferentes tipos de cuenta. Algunas rastrean transacciones, otras solo rastrean el balance a lo largo del tiempo.", "These Terms of Service govern your use of the Whisper Money personal finance platform.": "Estos T\u00e9rminos de Servicio rigen tu uso de la plataforma de finanzas personales Whisper Money.", "These Terms of Service govern your use of the\\n Whisper Money personal finance platform.": "Estos T\u00e9rminos de Servicio rigen tu uso de la plataforma de finanzas personales Whisper Money.", "These Terms, together with our Privacy Policy, constitute the entire agreement between you and Whisper Money regarding the use of our service and supersede any prior agreements or understandings, whether written or oral.": "Estos T\u00e9rminos, junto con nuestra Pol\u00edtica de Privacidad, constituyen el acuerdo completo entre t\u00fa y Whisper Money con respecto al uso de nuestro servicio y reemplazan cualquier acuerdo o entendimiento previo, ya sea escrito u oral.", @@ -1213,9 +1245,16 @@ "Track all your finances in one place \\u2014 checking,\\n savings, credit cards, investments, and more.": "Realiza un seguimiento de todas tus finanzas en un solo lugar: cuentas corrientes, ahorros, tarjetas de cr\u00e9dito, inversiones y m\u00e1s.", "Track all your finances in one place \u2014 checking, savings, credit cards, investments, and more.": "Rastrea todas tus finanzas en un solo lugar: cuenta corriente, ahorros, tarjetas de cr\u00e9dito, inversiones y m\u00e1s.", "Track credit card spending": "Rastrea los gastos de tarjeta de cr\u00e9dito", + "Track money entering your available cash without counting it as income.": "Sigue el dinero que entra en tu efectivo disponible sin contarlo como ingreso.", + "Track money leaving your available cash, like investments or savings transfers.": "Sigue el dinero que sale de tu efectivo disponible, como inversiones o transferencias a ahorros.", "Track your budget progress": "Sigue el progreso de tu presupuesto", "Track your income, expenses, and savings": "Rastrea tus ingresos, gastos y ahorros", + "Track your property market value over time. Transactions are not supported.": "Rastrea el valor de mercado de tu propiedad a lo largo del tiempo. Las transacciones no est\u00e1n soportadas.", "Track your spending with flexible budgets": "Rastrea tus gastos con presupuestos flexibles", + "Tracked Inflows": "Entradas seguidas", + "Tracked Outflows": "Salidas seguidas", + "Tracked transfers appear in the money flow Sankey chart. They are not counted as income or expenses.": "Las transferencias seguidas aparecen en el diagrama Sankey de flujo de dinero. No se cuentan como ingresos ni gastos.", + "Tracked transfers appear only in the monthly cashflow trend. They are not counted as income, expenses, or shown in the money flow Sankey.": "Las transferencias seguidas solo aparecen en la tendencia mensual del flujo de caja. No cuentan como ingresos ni gastos, ni se muestran en el Sankey de flujo de dinero.", "Tracking": "Seguimiento", "Tracking spending for": "Rastreando gastos para", "Tracking:": "Seguimiento:", @@ -1232,6 +1271,8 @@ "Transactions in this category will not be counted in top expenses or income. Transfer categories are mainly used for transactions between accounts.": "Estas transacciones no se contar\u00e1 como gasto ni como ingreso.", "Transactions in this category will\\n not be counted in top expenses or\\n income. Transfer categories are\\n mainly used for transactions between\\n accounts.": "Las transacciones en esta categor\u00eda no se contar\u00e1n entre los principales gastos o ingresos. Las categor\u00edas de transferencia se usan principalmente para transacciones entre cuentas.", "Transfer": "Transferencia", + "Transfer categories stay out of income and expense totals, but you can still track them in the money flow chart.": "Las categor\u00edas de transferencia quedan fuera de los totales de ingresos y gastos, pero a\u00fan puedes seguirlas en el gr\u00e1fico de flujo de dinero.", + "Transfer categories stay out of income and expense totals, but you can still track them in the monthly chart.": "Las categorias de transferencia quedan fuera de los totales de ingresos y gastos, pero aun puedes seguirlas en el grafico mensual.", "Transfers (Between accounts, Savings)": "Transferencias (Entre cuentas, Ahorros)", "Transportation": "Transporte", "Transportation (Fuel, Public Transit, Parking)": "Transporte (Combustible, Transporte p\u00fablico, Aparcamiento)", @@ -1251,6 +1292,7 @@ "Understand your finances and make better decisions without the friction. Track expenses, create budgets, and achieve your goals\u2014all in one place.": "Entiende tus finanzas para tomar mejores decisiones sin fricci\u00f3n. Rastrea gastos, crea presupuestos y alcanza tus metas, todo en un solo sitio.", "Understand your spending with beautiful charts and reports.": "Entiende tus gastos con gr\u00e1ficos y reportes hermosos.", "Understanding Categories": "Entendiendo las Categor\u00edas", + "Unit": "Unidad", "Unlimited Everything": "Sin L\u00edmites en Todo", "Unlimited accounts": "Cuentas ilimitadas", "Unlimited transaction imports": "Importaciones de transacciones ilimitadas", @@ -1267,6 +1309,8 @@ "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 categories automatically": "Actualizar categor\u00edas autom\u00e1ticamente", + "Update market value": "Actualizar valor de mercado", "Update owed amount": "Actualizar monto adeudado", "Update password": "Actualizar contrase\u00f1a", "Update the account information.": "Actualiza la informaci\u00f3n de la cuenta.", @@ -1297,6 +1341,7 @@ "Use the service only for lawful purposes and in compliance with all applicable laws": "Usa el servicio solo para fines legales y en cumplimiento de todas las leyes aplicables", "Use the service only for lawful purposes and\\n in compliance with all applicable laws": "Usa el servicio solo para fines legales y de conformidad con todas las leyes aplicables", "User account": "Cuenta de usuario", + "Vacation": "Vacacional", "Value": "Valor", "Verify Email Address": "Verificar Correo Electr\u00f3nico", "Verify Your Email - Whisper Money": "Verifica Tu Email - Whisper Money", @@ -1504,8 +1549,8 @@ "Your financial data is encrypted on your device before being transmitted to our servers, ensuring that only you can access your information": "Tus datos financieros se encriptan en tu dispositivo antes de ser transmitidos a nuestros servidores, asegurando que solo t\u00fa puedas acceder a tu informaci\u00f3n", "Your financial data is encrypted on your device before it ever reaches our servers.": "Tus datos financieros se encriptan en tu dispositivo antes de que lleguen a nuestros servidores.", "Your financial data is encrypted on your device. Only you can access it.": "Tus datos financieros se encriptan en tu dispositivo. Solo t\u00fa puedes acceder a ellos.", - "Your financial data stays private. The most secure way to manage your personal finances.": "Tus datos financieros permanecen privados. La forma m\u00e1s segura de gestionar tus finanzas personales.", "Your financial data stays private and is never shared": "Tus datos financieros son privados y nunca se comparten", + "Your financial data stays private. The most secure way to manage your personal finances.": "Tus datos financieros permanecen privados. La forma m\u00e1s segura de gestionar tus finanzas personales.", "Your financial data stays private\u2014never shared with third parties. The most secure way to manage your personal finances.": "Tus datos financieros permanecen privados\u2014nunca se comparten con terceros. La forma m\u00e1s segura de gestionar tus finanzas personales.", "Your financial data stays private\u2014never shared with third parties. Track expenses, create budgets, and achieve your goals\u2014all while keeping your information completely secure.": "Tus datos financieros permanecen privados\u2014nunca se comparten con terceros. Rastrea gastos, crea presupuestos y alcanza tus metas, todo mientras mantienes tu informaci\u00f3n completamente segura.", "Your first account must be a": "Tu primera cuenta debe ser una", @@ -1527,6 +1572,7 @@ "account": "cuenta", "account.": "cuenta.", "accounts": "cuentas", + "acres": "acres", "after 3 months with Whisper Money": "despu\u00e9s de 3 meses con Whisper Money", "amber": "\u00e1mbar", "balance": "balance", @@ -1550,10 +1596,12 @@ "for the last 12 months": "en los \u00faltimos 12 meses", "for the last 30 days": "en los \u00faltimos 30 d\u00edas", "forever": "siempre", + "ft\u00b2": "ft\u00b2", "fuchsia": "fucsia", "gray": "gris", "greater than": "mayor que", "green": "verde", + "ha": "ha", "index, follow": "index, follow", "indigo": "\u00edndigo", "is empty": "est\u00e1 vac\u00edo", @@ -1562,10 +1610,14 @@ "less than": "menor que", "lime": "lima", "log in": "iniciar sesi\u00f3n", + "market value": "valor de mercado", + "market values": "valores de mercado", + "m\u00b2": "m\u00b2", "neutral": "neutro", "of": "de", "on the last 30 days": "en los \u00faltimos 30 d\u00edas", "one-time": "pago \u00fanico", + "only with you": "solo contigo", "or you can": "o puedes", "or, enter the code manually": "o, ingresa el c\u00f3digo manualmente", "orange": "naranja", @@ -1605,23 +1657,5 @@ "yellow": "amarillo", "\u2014 $9/month": "\u2014 $9/mes", "\u2190 Back to home": "\u2190 Volver al inicio", - "\ud83c\udf89 Get a founder discount \u2022": "\ud83c\udf89 Obt\u00e9n un descuento de fundador \u2022", - "Cash inflow": "Entrada de efectivo", - "Cash outflow": "Salida de efectivo", - "Cashflow analytics": "Analitica de flujo de caja", - "Choose how to report this transfer": "Elige como mostrar esta transferencia", - "Do not show": "No mostrar", - "Keep this transfer out of cashflow analytics.": "Mantiene esta transferencia fuera de la analitica de flujo de caja.", - "Monthly income, expenses, tracked transfers, and net cashflow over the last 12 months": "Ingresos, gastos, transferencias seguidas y flujo de caja neto mensual durante los ultimos 12 meses", - "Show as cash inflow": "Mostrar como entrada de efectivo", - "Show as cash outflow": "Mostrar como salida de efectivo", - "Standard": "Estandar", - "Track money entering your available cash without counting it as income.": "Sigue el dinero que entra en tu efectivo disponible sin contarlo como ingreso.", - "Track money leaving your available cash, like investments or savings transfers.": "Sigue el dinero que sale de tu efectivo disponible, como inversiones o transferencias a ahorros.", - "Tracked Inflows": "Entradas seguidas", - "Tracked Outflows": "Salidas seguidas", - "Tracked transfers appear only in the monthly cashflow trend. They are not counted as income, expenses, or shown in the money flow Sankey.": "Las transferencias seguidas solo aparecen en la tendencia mensual del flujo de caja. No cuentan como ingresos ni gastos, ni se muestran en el Sankey de flujo de dinero.", - "Tracked transfers appear in the money flow Sankey chart. They are not counted as income or expenses.": "Las transferencias seguidas aparecen en el diagrama Sankey de flujo de dinero. No se cuentan como ingresos ni gastos.", - "Transfer categories stay out of income and expense totals, but you can still track them in the monthly chart.": "Las categorias de transferencia quedan fuera de los totales de ingresos y gastos, pero aun puedes seguirlas en el grafico mensual.", - "Transfer categories stay out of income and expense totals, but you can still track them in the money flow chart.": "Las categorías de transferencia quedan fuera de los totales de ingresos y gastos, pero aún puedes seguirlas en el gráfico de flujo de dinero." + "\ud83c\udf89 Get a founder discount \u2022": "\ud83c\udf89 Obt\u00e9n un descuento de fundador \u2022" } diff --git a/resources/js/components/accounts/account-form.tsx b/resources/js/components/accounts/account-form.tsx index 53ca58c8..6e50bb10 100644 --- a/resources/js/components/accounts/account-form.tsx +++ b/resources/js/components/accounts/account-form.tsx @@ -8,14 +8,22 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { Textarea } from '@/components/ui/textarea'; import { ACCOUNT_TYPES, + AREA_UNITS, + Account, CURRENCY_OPTIONS, + PROPERTY_TYPES, balanceTermCapitalized, formatAccountType, + formatAreaUnit, + formatPropertyType, type AccountType, + type AreaUnit, type Bank, type CurrencyCode, + type PropertyType, } from '@/types/account'; import { __ } from '@/utils/i18n'; import { useCallback, useEffect, useState } from 'react'; @@ -29,6 +37,17 @@ const BALANCE_ACCOUNT_TYPES: AccountType[] = [ 'savings', ]; +export interface RealEstateFormData { + propertyType: PropertyType | null; + address: string; + purchasePrice: number; + purchaseDate: string; + areaValue: string; + areaUnit: AreaUnit | null; + linkedLoanAccountId: string | null; + notes: string; +} + export interface AccountFormData { displayName: string; bankId: number | null; @@ -36,16 +55,19 @@ export interface AccountFormData { currencyCode: CurrencyCode | null; customBank: CustomBankData | null; balance: number | null; + realEstate: RealEstateFormData | null; } interface AccountFormProps { initialValues?: { displayName: string; - bank: Bank; + bank: Bank | null; type: AccountType; currencyCode: CurrencyCode; }; forceAccountType?: AccountType; + hiddenAccountTypes?: AccountType[]; + availableLoanAccounts?: Account[]; onChange: (data: AccountFormData) => void; } @@ -55,16 +77,29 @@ const initialCustomBankData: CustomBankData = { logoPreview: null, }; +const initialRealEstateData: RealEstateFormData = { + propertyType: null, + address: '', + purchasePrice: 0, + purchaseDate: '', + areaValue: '', + areaUnit: null, + linkedLoanAccountId: null, + notes: '', +}; + export function AccountForm({ initialValues, forceAccountType, + hiddenAccountTypes = [], + availableLoanAccounts = [], onChange, }: AccountFormProps) { const [displayName, setDisplayName] = useState( initialValues?.displayName ?? '', ); const [selectedBankId, setSelectedBankId] = useState( - initialValues?.bank.id ?? null, + initialValues?.bank?.id ?? null, ); const [selectedType, setSelectedType] = useState( initialValues?.type ?? forceAccountType ?? null, @@ -76,9 +111,13 @@ export function AccountForm({ initialCustomBankData, ); const [balance, setBalance] = useState(null); + const [realEstateData, setRealEstateData] = useState( + initialRealEstateData, + ); const showBalanceField = selectedType !== null && BALANCE_ACCOUNT_TYPES.includes(selectedType); + const isRealEstate = selectedType === 'real_estate'; useEffect(() => { onChange({ @@ -88,6 +127,7 @@ export function AccountForm({ currencyCode: selectedCurrency, customBank: isCreatingCustomBank ? customBankData : null, balance: showBalanceField ? balance : null, + realEstate: isRealEstate ? realEstateData : null, }); }, [ displayName, @@ -98,17 +138,20 @@ export function AccountForm({ customBankData, balance, showBalanceField, + isRealEstate, + realEstateData, onChange, ]); useEffect(() => { if (initialValues) { setDisplayName(initialValues.displayName); - setSelectedBankId(initialValues.bank.id); + setSelectedBankId(initialValues.bank?.id ?? null); setSelectedType(initialValues.type); setSelectedCurrency(initialValues.currencyCode); setIsCreatingCustomBank(false); setCustomBankData(initialCustomBankData); + setRealEstateData(initialRealEstateData); } }, [initialValues]); @@ -142,36 +185,6 @@ export function AccountForm({ /> -
- -
- {isCreatingCustomBank ? ( - - ) : ( - <> - - - - - )} -
-
-
@@ -179,9 +192,15 @@ export function AccountForm({ name="type" value={selectedType ?? undefined} disabled={!!forceAccountType} - onValueChange={(value) => - setSelectedType(value as AccountType) - } + onValueChange={(value) => { + const newType = value as AccountType; + setSelectedType(newType); + if (newType === 'real_estate') { + setSelectedBankId(null); + setIsCreatingCustomBank(false); + setCustomBankData(initialCustomBankData); + } + }} required > @@ -190,7 +209,9 @@ export function AccountForm({ /> - {ACCOUNT_TYPES.map((type) => ( + {ACCOUNT_TYPES.filter( + (type) => !hiddenAccountTypes.includes(type), + ).map((type) => ( {formatAccountType(type)} @@ -206,8 +227,49 @@ export function AccountForm({ )}

)} + {isRealEstate && ( +

+ {__( + 'Track your property market value over time. Transactions are not supported.', + )} +

+ )}
+ {!isRealEstate && ( +
+ +
+ {isCreatingCustomBank ? ( + + ) : ( + <> + + + + + )} +
+
+ )} +
@@ -253,6 +315,215 @@ export function AccountForm({

)} + + {isRealEstate && ( + <> +
+ +
+ +
+
+ +
+ + + setRealEstateData((prev) => ({ + ...prev, + address: e.target.value, + })) + } + placeholder={__('Property address')} + /> +
+ +
+ + + setRealEstateData((prev) => ({ + ...prev, + purchasePrice: value, + })) + } + currencyCode={selectedCurrency ?? 'USD'} + /> +
+ +
+ + + setRealEstateData((prev) => ({ + ...prev, + purchaseDate: e.target.value, + })) + } + /> +
+ +
+
+ + + setRealEstateData((prev) => ({ + ...prev, + areaValue: e.target.value, + })) + } + placeholder="0" + min="0" + step="0.01" + /> +
+
+ +
+ +
+
+
+ + {availableLoanAccounts.length > 0 && ( +
+ +

+ {__( + 'Link a loan account to track equity (market value minus owed amount).', + )} +

+
+ +
+
+ )} + +
+ +