Merge branch 'main' into feat/goals-v2-architecture

This commit is contained in:
Guillem Arias Fauste
2026-05-18 20:51:38 +02:00
committed by GitHub
109 changed files with 2379 additions and 123 deletions

View File

@@ -7,6 +7,7 @@ class BudgetsController < ApplicationController
def show
@source_budget = @budget.most_recent_initialized_budget unless @budget.initialized?
@breadcrumbs = [ [ t("breadcrumbs.home"), root_path ], [ t("breadcrumbs.budgets"), nil ] ]
end
def edit

View File

@@ -8,6 +8,6 @@ module Breadcrumbable
private
# The default, unless specific controller or action explicitly overrides
def set_breadcrumbs
@breadcrumbs = [ [ "Home", root_path ], [ controller_name.titleize, nil ] ]
@breadcrumbs = [ [ t("breadcrumbs.home"), root_path ], [ t("breadcrumbs.#{controller_name}", default: controller_name.titleize), nil ] ]
end
end

View File

@@ -2,7 +2,7 @@ class FamilyMerchantsController < ApplicationController
before_action :set_merchant, only: %i[edit update destroy]
def index
@breadcrumbs = [ [ "Home", root_path ], [ "Merchants", nil ] ]
@breadcrumbs = [ [ t("breadcrumbs.home"), root_path ], [ t("breadcrumbs.merchants"), nil ] ]
# Show all merchants for this family
@family_merchants = Current.family.merchants.alphabetically

View File

@@ -26,11 +26,11 @@ class PagesController < ApplicationController
@dashboard_sections = build_dashboard_sections
@breadcrumbs = [ [ "Home", root_path ], [ "Dashboard", nil ] ]
@breadcrumbs = [ [ t("breadcrumbs.home"), root_path ], [ t("breadcrumbs.dashboard"), nil ] ]
end
def intro
@breadcrumbs = [ [ "Home", chats_path ], [ "Intro", nil ] ]
@breadcrumbs = [ [ t("breadcrumbs.home"), chats_path ], [ t("breadcrumbs.intro"), nil ] ]
end
def update_preferences

View File

@@ -12,7 +12,7 @@ class ReportsController < ApplicationController
# Build reports sections for collapsible/reorderable UI
@reports_sections = build_reports_sections
@breadcrumbs = [ [ "Home", root_path ], [ "Reports", nil ] ]
@breadcrumbs = [ [ t("breadcrumbs.home"), root_path ], [ t("breadcrumbs.reports"), nil ] ]
end
def print

View File

@@ -3,8 +3,8 @@ class Settings::AiPromptsController < ApplicationController
def show
@breadcrumbs = [
[ "Home", root_path ],
[ "AI Prompts", nil ]
[ t("breadcrumbs.home"), root_path ],
[ t("breadcrumbs.ai_prompts"), nil ]
]
@family = Current.family
@assistant_config = Assistant.config_for(OpenStruct.new(user: Current.user))

View File

@@ -7,8 +7,8 @@ class Settings::ApiKeysController < ApplicationController
def show
@breadcrumbs = [
[ "Home", root_path ],
[ "API Key", nil ]
[ t("breadcrumbs.home"), root_path ],
[ t("breadcrumbs.api_key"), nil ]
]
@current_api_key = @api_key
end

View File

@@ -3,8 +3,8 @@ class Settings::GuidesController < ApplicationController
def show
@breadcrumbs = [
[ "Home", root_path ],
[ "Guides", nil ]
[ t("breadcrumbs.home"), root_path ],
[ t("breadcrumbs.guides"), nil ]
]
markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML,
autolink: true,

View File

@@ -17,8 +17,8 @@ class Settings::HostingsController < ApplicationController
def show
@breadcrumbs = [
[ "Home", root_path ],
[ "Self-Hosting", nil ]
[ t("breadcrumbs.home"), root_path ],
[ t("breadcrumbs.self_hosting"), nil ]
]
# Determine which providers are currently selected

View File

@@ -3,8 +3,8 @@ class Settings::LlmUsagesController < ApplicationController
def show
@breadcrumbs = [
[ "Home", root_path ],
[ "LLM Usage", nil ]
[ t("breadcrumbs.home"), root_path ],
[ t("breadcrumbs.llm_usage"), nil ]
]
@family = Current.family

View File

@@ -6,8 +6,8 @@ class Settings::ProfilesController < ApplicationController
@users = Current.family.users.order(:created_at)
@pending_invitations = Current.family.invitations.pending
@breadcrumbs = [
[ "Home", root_path ],
[ "Profile Info", nil ]
[ t("breadcrumbs.home"), root_path ],
[ t("breadcrumbs.profile"), nil ]
]
end

View File

@@ -5,8 +5,8 @@ class Settings::ProvidersController < ApplicationController
def show
@breadcrumbs = [
[ "Home", root_path ],
[ "Bank sync", nil ]
[ t("breadcrumbs.home"), root_path ],
[ t("breadcrumbs.bank_sync"), nil ]
]
prepare_show_context

View File

@@ -3,8 +3,8 @@ class Settings::SecuritiesController < ApplicationController
def show
@breadcrumbs = [
[ "Home", root_path ],
[ "Security", nil ]
[ t("breadcrumbs.home"), root_path ],
[ t("breadcrumbs.security"), nil ]
]
@oidc_identities = Current.user.oidc_identities.order(:provider)
@webauthn_credentials = Current.user.webauthn_credentials.order(created_at: :asc)

View File

@@ -63,6 +63,8 @@ class TransactionsController < ApplicationController
10.days.from_now.to_date,
Date.current)
.includes(:merchant)
@breadcrumbs = [ [ t("breadcrumbs.home"), root_path ], [ t("breadcrumbs.transactions"), nil ] ]
end
def clear_filter

View File

@@ -13,6 +13,8 @@ export default class extends Controller {
static values = {
singularLabel: String,
pluralLabel: String,
selectedLabel: { type: String, default: "selected" },
editLabel: { type: String, default: "Edit" },
selectedIds: { type: Array, default: [] },
};
@@ -28,7 +30,7 @@ export default class extends Controller {
bulkEditDrawerHeaderTargetConnected(element) {
const headingTextEl = element.querySelector("h2");
headingTextEl.innerText = `Edit ${
headingTextEl.innerText = `${this.editLabelValue} ${
this.selectedIdsValue.length
} ${this._pluralizedResourceName()}`;
}
@@ -132,7 +134,7 @@ export default class extends Controller {
_updateSelectionBar() {
const count = this.selectedIdsValue.length;
this.selectionBarTextTarget.innerText = `${count} ${this._pluralizedResourceName()} selected`;
this.selectionBarTextTarget.innerText = `${count} ${this._pluralizedResourceName()} ${this.selectedLabelValue}`;
this.selectionBarTarget.classList.toggle("hidden", count === 0);
this.selectionBarTarget.querySelector("input[type='checkbox']").checked =
count > 0;

View File

@@ -13,7 +13,7 @@ class BalanceSheet::ClassificationGroup
end
def name
classification.titleize.pluralize
I18n.t("pages.dashboard.balance_sheet.classifications.#{classification}", default: classification.titleize.pluralize)
end
def icon
@@ -34,7 +34,7 @@ class BalanceSheet::ClassificationGroup
.transform_keys { |at| Accountable.from_type(at) }
.map do |accountable, account_rows|
BalanceSheet::AccountGroup.new(
name: I18n.t("accounts.types.#{accountable.name.underscore}", default: accountable.display_name),
name: accountable.display_name,
color: accountable.color,
accountable_type: accountable,
accounts: account_rows,

View File

@@ -197,28 +197,28 @@ class Category < ApplicationRecord
private
def default_categories
[
[ "Income", "#22c55e", "circle-dollar-sign" ],
[ "Food & Drink", "#f97316", "utensils" ],
[ "Groceries", "#407706", "shopping-bag" ],
[ "Shopping", "#3b82f6", "shopping-cart" ],
[ "Transportation", "#0ea5e9", "bus" ],
[ "Travel", "#2563eb", "plane" ],
[ "Entertainment", "#a855f7", "drama" ],
[ "Healthcare", "#4da568", "pill" ],
[ "Personal Care", "#14b8a6", "scissors" ],
[ "Home Improvement", "#d97706", "hammer" ],
[ "Mortgage / Rent", "#b45309", "home" ],
[ "Utilities", "#eab308", "lightbulb" ],
[ "Subscriptions", "#6366f1", "wifi" ],
[ "Insurance", "#0284c7", "shield" ],
[ "Sports & Fitness", "#10b981", "dumbbell" ],
[ "Gifts & Donations", "#61c9ea", "hand-helping" ],
[ "Taxes", "#dc2626", "landmark" ],
[ "Loan Payments", "#e11d48", "credit-card" ],
[ "Services", "#7c3aed", "briefcase" ],
[ "Fees", "#6b7280", "receipt" ],
[ "Savings & Investments", "#059669", "piggy-bank" ],
[ investment_contributions_name, "#0d9488", "trending-up" ]
[ I18n.t("models.category.defaults.income"), "#22c55e", "circle-dollar-sign" ],
[ I18n.t("models.category.defaults.food_and_drink"), "#f97316", "utensils" ],
[ I18n.t("models.category.defaults.groceries"), "#407706", "shopping-bag" ],
[ I18n.t("models.category.defaults.shopping"), "#3b82f6", "shopping-cart" ],
[ I18n.t("models.category.defaults.transportation"), "#0ea5e9", "bus" ],
[ I18n.t("models.category.defaults.travel"), "#2563eb", "plane" ],
[ I18n.t("models.category.defaults.entertainment"), "#a855f7", "drama" ],
[ I18n.t("models.category.defaults.healthcare"), "#4da568", "pill" ],
[ I18n.t("models.category.defaults.personal_care"), "#14b8a6", "scissors" ],
[ I18n.t("models.category.defaults.home_improvement"), "#d97706", "hammer" ],
[ I18n.t("models.category.defaults.mortgage_rent"), "#b45309", "home" ],
[ I18n.t("models.category.defaults.utilities"), "#eab308", "lightbulb" ],
[ I18n.t("models.category.defaults.subscriptions"), "#6366f1", "wifi" ],
[ I18n.t("models.category.defaults.insurance"), "#0284c7", "shield" ],
[ I18n.t("models.category.defaults.sports_and_fitness"), "#10b981", "dumbbell" ],
[ I18n.t("models.category.defaults.gifts_and_donations"), "#61c9ea", "hand-helping" ],
[ I18n.t("models.category.defaults.taxes"), "#dc2626", "landmark" ],
[ I18n.t("models.category.defaults.loan_payments"), "#e11d48", "credit-card" ],
[ I18n.t("models.category.defaults.services"), "#7c3aed", "briefcase" ],
[ I18n.t("models.category.defaults.fees"), "#6b7280", "receipt" ],
[ I18n.t("models.category.defaults.savings_and_investments"), "#059669", "piggy-bank" ],
[ investment_contributions_name, "#0d9488", "trending-up" ]
]
end
end

View File

@@ -58,8 +58,29 @@ module Accountable
classification == "asset" ? "up" : "down"
end
def singular_display_name
I18n.t("accounts.types.#{name.underscore}", default: legacy_singular_display_name)
end
def display_name
self.name.pluralize.titleize
I18n.t("accounts.types_plural.#{name.underscore}", default: -> { legacy_display_name })
end
def legacy_display_name
return singular_display_name if name.in?([ "Depository", "Crypto" ])
singular_display_name.pluralize
end
def legacy_singular_display_name
case name
when "Depository"
"Cash"
when "Crypto"
"Crypto"
else
name.underscore.humanize
end
end
# Sums the balances of all active accounts of this type, converting foreign currencies to the family's currency.
@@ -80,6 +101,10 @@ module Accountable
end
end
def singular_display_name
self.class.singular_display_name
end
def display_name
self.class.display_name
end

View File

@@ -34,9 +34,5 @@ class Crypto < ApplicationRecord
def icon
"bitcoin"
end
def display_name
"Crypto"
end
end
end

View File

@@ -12,10 +12,6 @@ class Depository < ApplicationRecord
}.freeze
class << self
def display_name
"Cash"
end
def color
"#875BF7"
end

View File

@@ -179,24 +179,24 @@ class Period
end
def label
if key_metadata
key_metadata.fetch(:label)
if key
I18n.t("period.#{key}.label", default: key_metadata&.fetch(:label) || "Custom Period")
else
"Custom Period"
I18n.t("period.custom.label", default: "Custom Period")
end
end
def label_short
if key_metadata
key_metadata.fetch(:label_short)
if key
I18n.t("period.#{key}.label_short", default: key_metadata&.fetch(:label_short) || "Custom")
else
"Custom"
I18n.t("period.custom.label_short", default: "Custom")
end
end
def comparison_label
if key_metadata
key_metadata.fetch(:comparison_label)
if key
I18n.t("period.#{key}.comparison_label", default: key_metadata&.fetch(:comparison_label) || "#{start_date.strftime(@date_format)} to #{end_date.strftime(@date_format)}")
else
"#{start_date.strftime(@date_format)} to #{end_date.strftime(@date_format)}"
end

View File

@@ -7,5 +7,5 @@
hex_color: accountable.color,
) %>
<%= accountable.display_name.singularize %>
<%= accountable.singular_display_name %>
<% end %>

View File

@@ -56,7 +56,7 @@
<div class="my-2">
<%= render DS::Link.new(
href: new_polymorphic_path(account_group.key, step: "method_select"),
text: t("accounts.sidebar.new_account_group", account_group: account_group.name.downcase.singularize),
text: t("accounts.sidebar.new_account_group", account_group: account_group.accountable_type.singular_display_name.downcase),
icon: "plus",
full_width: true,
variant: "ghost",

View File

@@ -118,10 +118,10 @@
) %>
<p class="text-primary text-sm font-medium mb-1 mt-4">
<%= t("pages.dashboard.balance_sheet.no_items", name: classification_group.name) %>
<%= t("pages.dashboard.balance_sheet.no_#{classification_group.classification}") %>
</p>
<p class="text-secondary text-sm text-center">
<%= t("pages.dashboard.balance_sheet.add_accounts", name: classification_group.name) %>
<%= t("pages.dashboard.balance_sheet.add_#{classification_group.classification}_accounts") %>
</p>
</div>
<% end %>

View File

@@ -10,11 +10,11 @@
<div class="text-sm space-y-1">
<% if Current.user.otp_required? %>
<p class="text-primary">Two-factor authentication is <span class="font-medium text-green-600">enabled</span></p>
<p class="text-secondary">Your account is protected with an additional layer of security.</p>
<p class="text-primary"><%= t(".mfa_enabled_status_html") %></p>
<p class="text-secondary"><%= t(".mfa_enabled_description") %></p>
<% else %>
<p class="text-primary">Two-factor authentication is <span class="font-medium text-red-600">disabled</span></p>
<p class="text-secondary">Enable 2FA to add an extra layer of security to your account.</p>
<p class="text-primary"><%= t(".mfa_disabled_status_html") %></p>
<p class="text-secondary"><%= t(".mfa_disabled_description") %></p>
<% end %>
</div>
</div>

View File

@@ -3,6 +3,8 @@
data-controller="bulk-select checkbox-toggle drag-and-drop-import"
data-bulk-select-singular-label-value="<%= t(".transaction") %>"
data-bulk-select-plural-label-value="<%= t(".transactions") %>"
data-bulk-select-selected-label-value="<%= t("transactions.selection_bar.selected") %>"
data-bulk-select-edit-label-value="<%= t("transactions.selection_bar.edit") %>"
class="flex flex-col bg-container rounded-xl shadow-border-xs px-3 py-4 lg:p-4 relative group">
<%= form_with url: imports_path, method: :post, class: "hidden", data: { drag_and_drop_import_target: "form", turbo: false } do |f| %>
@@ -29,7 +31,7 @@
action: "bulk-select#togglePageSelection",
checkbox_toggle_target: "selectionEntry"
} %>
<p>transaction</p>
<p><%= t(".transaction") %></p>
</div>
<p class="col-span-2 md:block hidden"><%= t("transactions.form.category_label") %></p>

View File

@@ -1,27 +1,27 @@
<%= render DS::Dialog.new(variant: "drawer", frame: "bulk_transaction_edit_drawer") do |dialog| %>
<% dialog.with_header(title: "Edit transactions", data: { bulk_select_target: "bulkEditDrawerHeader" }) %>
<% dialog.with_header(title: t(".header_title"), data: { bulk_select_target: "bulkEditDrawerHeader" }) %>
<% dialog.with_body do %>
<%= styled_form_with url: transactions_bulk_update_path, scope: "bulk_update", class: "h-full flex flex-col justify-between gap-4", data: { turbo_frame: "_top" } do |form| %>
<div class="space-y-4">
<%= render DS::Disclosure.new(title: "Overview", open: true) do %>
<%= form.date_field :date, label: "Date", max: Date.current %>
<%= render DS::Disclosure.new(title: t(".overview"), open: true) do %>
<%= form.date_field :date, label: t(".date_label"), max: Date.current %>
<% end %>
<%= render DS::Disclosure.new(title: "Transactions", open: true) do %>
<%= render DS::Disclosure.new(title: t(".transactions_section"), open: true) do %>
<div class="space-y-2">
<%= form.text_field :name, label: t("transactions.bulk_updates.new.name_label"), placeholder: t("transactions.bulk_updates.new.name_placeholder") %>
<%= form.collection_select :category_id, Current.family.categories.alphabetically, :id, :name, { prompt: "Select a category", label: "Category", class: "text-subdued" } %>
<%= form.collection_select :merchant_id, Current.family.available_merchants_for(Current.user).alphabetically, :id, :name, { prompt: "Select a merchant", label: "Merchant", class: "text-subdued" } %>
<%= form.select :tag_ids, Current.family.tags.alphabetically.pluck(:name, :id), { include_blank: "None", multiple: true, label: "Tags", include_hidden: false } %>
<%= form.text_area :notes, label: "Notes", placeholder: "Enter a note that will be applied to selected transactions", rows: 5 %>
<%= form.text_field :name, label: t(".name_label"), placeholder: t(".name_placeholder") %>
<%= form.collection_select :category_id, Current.family.categories.alphabetically, :id, :name, { prompt: t(".category_prompt"), label: t(".category_label"), class: "text-subdued" } %>
<%= form.collection_select :merchant_id, Current.family.available_merchants_for(Current.user).alphabetically, :id, :name, { prompt: t(".merchant_prompt"), label: t(".merchant_label"), class: "text-subdued" } %>
<%= form.select :tag_ids, Current.family.tags.alphabetically.pluck(:name, :id), { include_blank: t(".none"), multiple: true, label: t(".tags_label"), include_hidden: false } %>
<%= form.text_area :notes, label: t(".notes_label"), placeholder: t(".notes_placeholder"), rows: 5 %>
</div>
<% end %>
</div>
<div class="flex justify-end gap-2 mt-auto">
<%= render DS::Button.new(text: "Cancel", variant: "ghost", data: { action: "click->DS--dialog#close" }) %>
<%= render DS::Button.new(text: "Save", data: { bulk_select_scope_param: "bulk_update", action: "bulk-select#submitBulkRequest" }) %>
<%= render DS::Button.new(text: t(".cancel"), variant: "ghost", data: { action: "click->DS--dialog#close" }) %>
<%= render DS::Button.new(text: t(".save"), data: { bulk_select_scope_param: "bulk_update", action: "bulk-select#submitBulkRequest" }) %>
</div>
<% end %>
<% end %>

View File

@@ -1,5 +1,5 @@
<%= render DS::Dialog.new(scrollable: false, content_class: "lg:max-h-none lg:overflow-y-auto") do |dialog| %>
<% dialog.with_header(title: "New transaction") %>
<% dialog.with_header(title: t(".new_transaction")) %>
<% dialog.with_body do %>
<%= render "form", entry: @entry, categories: @categories %>
<% end %>