1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-08 23:15:24 +02:00

Persistent account tab selections

This commit is contained in:
Zach Gollwitzer 2025-05-14 11:42:17 -04:00
parent db02718d71
commit c83aa10f01
20 changed files with 86 additions and 58 deletions

View file

@ -1,6 +1,7 @@
<%= tag.div data: {
controller: "tabs",
testid: testid,
tabs_session_key_value: session_key,
tabs_url_param_key_value: url_param_key,
tabs_nav_btn_active_class: active_btn_classes,
tabs_nav_btn_inactive_class: inactive_btn_classes
@ -10,6 +11,11 @@
<% else %>
<%= nav %>
<%= form_with url: cookie_session_path, scope: :cookie_session, method: :put, data: { tabs_target: "persistSelectionForm" } do |f| %>
<%= f.hidden_field :tab_key, value: "account_group_tab" %>
<%= f.hidden_field :tab_value, value: "all", data: { tabs_target: "selectionInput" } %>
<% end %>
<% panels.each do |panel| %>
<%= panel %>
<% end %>

View file

@ -27,11 +27,12 @@ class TabsComponent < ViewComponent::Base
}
}
attr_reader :active_tab, :url_param_key, :variant, :testid
attr_reader :active_tab, :url_param_key, :session_key, :variant, :testid
def initialize(active_tab:, url_param_key: nil, variant: :default, active_btn_classes: "", inactive_btn_classes: "", testid: nil)
def initialize(active_tab:, url_param_key: nil, session_key: nil, variant: :default, active_btn_classes: "", inactive_btn_classes: "", testid: nil)
@active_tab = active_tab
@url_param_key = url_param_key
@session_key = session_key
@variant = variant.to_sym
@active_btn_classes = active_btn_classes
@inactive_btn_classes = inactive_btn_classes

View file

@ -3,8 +3,8 @@ import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="tabs--components"
export default class extends Controller {
static classes = ["navBtnActive", "navBtnInactive"];
static targets = ["panel", "navBtn"];
static values = { urlParamKey: String };
static targets = ["panel", "navBtn", "persistSelectionForm", "selectionInput"];
static values = { sessionKey: String, urlParamKey: String };
show(e) {
const btn = e.target.closest("button");
@ -28,11 +28,16 @@ export default class extends Controller {
}
});
// Update URL with the selected tab
if (this.urlParamKeyValue) {
const url = new URL(window.location.href);
url.searchParams.set(this.urlParamKeyValue, selectedTabId);
window.history.replaceState({}, "", url);
}
// Update URL with the selected tab
if (this.sessionKeyValue) {
this.selectionInputTarget.value = selectedTabId;
this.persistSelectionFormTarget.requestSubmit();
}
}
}

View file

@ -5,40 +5,20 @@ module AccountGroupable
before_action :set_account_group_tab
end
def set_account_group_tab
last_selected_tab = session[:account_group_tab] || "asset"
selected_tab = if account_group_tab_param
account_group_tab_param
elsif on_asset_page?
"asset"
elsif on_liability_page?
"liability"
else
last_selected_tab
end
session[:account_group_tab] = selected_tab
@account_group_tab = selected_tab
end
private
def set_account_group_tab
last_selected_tab = session["custom_account_group_tab"] || "asset"
@account_group_tab = account_group_tab_param || last_selected_tab
end
def valid_account_group_tabs
%w[asset liability all]
end
def account_group_tab_param
valid_tabs = %w[asset liability all]
params[:account_group_tab].in?(valid_tabs) ? params[:account_group_tab] : nil
end
def on_asset_page?
accountable_controller_names_for("asset").include?(controller_name)
end
def on_liability_page?
accountable_controller_names_for("liability").include?(controller_name)
end
def accountable_controller_names_for(classification)
Accountable::TYPES.map(&:constantize)
.select { |a| a.classification == classification }
.map { |a| a.model_name.plural }
param_value = params[:account_group_tab]
return nil unless param_value.in?(valid_account_group_tabs)
param_value
end
end

View file

@ -0,0 +1,22 @@
class CookieSessionsController < ApplicationController
def update
save_kv_to_session(
cookie_session_params[:tab_key],
cookie_session_params[:tab_value]
)
redirect_back_or_to root_path
end
private
def cookie_session_params
params.require(:cookie_session).permit(:tab_key, :tab_value)
end
def save_kv_to_session(key, value)
raise "Key must be a string" unless key.is_a?(String)
raise "Value must be a string" unless value.is_a?(String)
session["custom_#{key}"] = value
end
end

View file

@ -31,4 +31,8 @@ class SessionsController < ApplicationController
def set_session
@session = Current.user.sessions.find(params[:id])
end
def session_params
params.require(:session).permit(:tab_key, :tab_value)
end
end

View file

@ -87,7 +87,7 @@ class UsersController < ApplicationController
def user_params
params.require(:user).permit(
:first_name, :last_name, :email, :profile_image, :redirect_to, :delete_profile_image, :onboarded_at,
:first_name, :last_name, :email, :profile_image, :redirect_to, :delete_profile_image, :onboarded_at, :preferred_account_group_tab,
:show_sidebar, :default_period, :show_ai_sidebar, :ai_enabled, :theme, :set_onboarding_preferences_at, :set_onboarding_goals_at,
family_attributes: [ :name, :currency, :country, :locale, :date_format, :timezone, :id ],
goals: []

View file

@ -48,9 +48,10 @@ class BalanceSheet
def account_groups(classification = nil)
classification_accounts = classification ? totals_query.filter { |t| t.classification == classification } : totals_query
classification_total = classification_accounts.sum(&:converted_balance)
account_groups = classification_accounts.group_by(&:accountable_type).transform_keys { |k| Accountable.from_type(k) }
account_groups = classification_accounts.group_by(&:accountable_type)
.transform_keys { |k| Accountable.from_type(k) }
account_groups.map do |accountable, accounts|
groups = account_groups.map do |accountable, accounts|
group_total = accounts.sum(&:converted_balance)
AccountGroup.new(
@ -71,7 +72,13 @@ class BalanceSheet
account
end.sort_by(&:weight).reverse
)
end.sort_by(&:weight).reverse
end
groups.sort_by do |group|
manual_order = Accountable::TYPES
type_name = group.key.camelize
manual_order.index(type_name) || Float::INFINITY
end
end
def net_worth_series(period: Period.last_30_days)

View file

@ -21,7 +21,7 @@
</details>
<% end %>
<%= render TabsComponent.new(active_tab: active_account_group_tab, testid: "account-sidebar-tabs") do |tabs| %>
<%= render TabsComponent.new(active_tab: active_account_group_tab, session_key: "account_group_tab", testid: "account-sidebar-tabs") do |tabs| %>
<% tabs.with_nav do |nav| %>
<% nav.with_btn(id: "asset", label: "Assets") %>
<% nav.with_btn(id: "liability", label: "Debts") %>

View file

@ -2,7 +2,7 @@
<% trend = series.trend %>
<%= turbo_frame_tag dom_id(@account, :chart_details) do %>
<% if @account.syncing?%>
<% if @account.syncing? %>
<%= render "accounts/chart_loader" %>
<% else %>
<div class="px-4">

View file

@ -14,7 +14,7 @@
<% end %>
</div>
<% if account.syncing? %>
<% if account.syncing? %>
<div class="bg-loader rounded-md h-7 w-20"></div>
<% else %>
<%= tag.p format_money(account.balance_money), class: "text-primary text-3xl font-medium truncate" %>

View file

@ -26,7 +26,7 @@
<%= render(
"accounts/account_sidebar_tabs",
family: Current.family,
active_account_group_tab: params[:account_group_tab] || "assets"
active_account_group_tab: @account_group_tab
) %>
</div>
@ -81,7 +81,7 @@
<% else %>
<div class="h-full flex flex-col">
<div class="overflow-y-auto grow">
<%= render "accounts/account_sidebar_tabs", family: Current.family, active_account_group_tab: params[:account_group_tab] || "assets" %>
<%= render "accounts/account_sidebar_tabs", family: Current.family, active_account_group_tab: @account_group_tab %>
</div>
<% if Current.family.trialing? && !self_hosted? %>

View file

@ -26,9 +26,9 @@
<div class="w-full space-y-6 pb-24">
<% if Current.family.accounts.any? %>
<section class="bg-container py-4 rounded-xl shadow-border-xs">
<%= render partial: "pages/dashboard/net_worth_chart", locals: {
balance_sheet: @balance_sheet,
period: @period
<%= render partial: "pages/dashboard/net_worth_chart", locals: {
balance_sheet: @balance_sheet,
period: @period
} %>
</section>
<% else %>

View file

@ -42,7 +42,7 @@
</div>
<% end %>
</div>
<% end%>
<% end %>
</div>
<div class="bg-surface rounded-xl p-1 space-y-1 overflow-x-auto">

View file

@ -1,5 +1,5 @@
<%# locals: (rule:) %>
<div class="flex justify-between items-center p-4 <%= rule.active? ? 'text-primary' : 'text-secondary' %>">
<div class="flex justify-between items-center p-4 <%= rule.active? ? "text-primary" : "text-secondary" %>">
<div class="text-sm space-y-1.5">
<% if rule.name.present? %>
<h3 class="font-medium text-md"><%= rule.name %></h3>
@ -49,7 +49,7 @@
<% if rule.effective_date.nil? %>
All past and future <%= rule.resource_type.pluralize %>
<% else %>
<%= rule.resource_type.pluralize %> on or after <%= rule.effective_date.strftime('%b %-d, %Y') %>
<%= rule.resource_type.pluralize %> on or after <%= rule.effective_date.strftime("%b %-d, %Y") %>
<% end %>
</span>
</p>

View file

@ -1,5 +1,5 @@
<%= render DialogComponent.new(reload_on_close: true) do |dialog| %>
<%
<%
title = if @rule.name.present?
"Confirm changes to \"#{@rule.name}\""
else
@ -7,7 +7,7 @@
end
%>
<% dialog.with_header(title: title) %>
<% dialog.with_body do %>
<p class="text-secondary text-sm mb-4">
You are about to apply this rule to

View file

@ -1,7 +1,7 @@
<%= link_to "Back to rules", rules_path %>
<%= render DialogComponent.new do |dialog| %>
<%
<%
title = if @rule.name.present?
"Edit #{@rule.resource_type} rule \"#{@rule.name}\""
else

View file

@ -60,7 +60,7 @@
<div class="p-1">
<div class="flex flex-col bg-container rounded-xl shadow-border-xs first_child:rounded-t-xl last_child:rounded-b-xl">
<% @rules.each_with_index do |rule, idx| %>
<%= render "rule", rule: rule%>
<%= render "rule", rule: rule %>
<% unless idx == @rules.size - 1 %>
<div class="h-px bg-divider ml-4 mr-6"></div>
<% end %>

View file

@ -26,6 +26,8 @@ Rails.application.routes.draw do
get "changelog", to: "pages#changelog"
get "feedback", to: "pages#feedback"
resource :cookie_session, only: %i[update]
resource :registration, only: %i[new create]
resources :sessions, only: %i[new create destroy]
resource :password_reset, only: %i[new create edit update]

3
db/schema.rb generated
View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2025_05_13_122703) do
ActiveRecord::Schema[7.2].define(version: 2025_05_14_143017) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@ -696,6 +696,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_05_13_122703) do
t.text "goals", default: [], array: true
t.datetime "set_onboarding_preferences_at"
t.datetime "set_onboarding_goals_at"
t.string "preferred_account_group_tab", default: "asset"
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["family_id"], name: "index_users_on_family_id"
t.index ["last_viewed_chat_id"], name: "index_users_on_last_viewed_chat_id"