mirror of
https://github.com/we-promise/sure.git
synced 2026-04-07 22:34:47 +00:00
* Fix cashflow and outflows widgets to respect user's default period preference Resolves issue #118 where the Cashflow and Outflows widgets on the dashboard were hardcoded to use a 30-day period instead of respecting the user's default period preference setting. Changes: - Updated @cashflow_period to use Current.user&.default_period as fallback - Updated @outflows_period to use Current.user&.default_period as fallback - Both now follow the same pattern as the Periodable concern's set_period method This ensures consistency across all dashboard widgets - Net Worth, Cashflow, and Outflows now all respect the user's preference. * Synchronize period selection across all dashboard widgets All three dashboard widgets (Net Worth, Cashflow, and Outflows) now use a single shared period parameter, ensuring consistent data magnitudes across the dashboard. Changes: - Simplified controller to use single @period for all three widgets - Removed widget-specific period parameters (cashflow_period, outflows_period) - All widgets now use the shared 'period' parameter - All period dropdowns use turbo_frame: "_top" to reload entire page - Removed turbo_frame_tags from dashboard view for cleaner implementation User experience improvement: - Changing the period in any widget now updates all three widgets - Ensures data consistency and easier comparison across widgets - Maintains respect for user's default period preference * Make Net Worth widget title styling consistent with Cashflow and Outflows Changed Net Worth title from <p> with text-sm/text-secondary to <h2> with text-lg to match the consistent styling used by Cashflow and Outflows widgets. This provides a more unified visual appearance across all dashboard widgets. --------- Co-authored-by: Claude <noreply@anthropic.com>
170 lines
6.0 KiB
Ruby
170 lines
6.0 KiB
Ruby
class PagesController < ApplicationController
|
||
include Periodable
|
||
|
||
skip_authentication only: :redis_configuration_error
|
||
|
||
def dashboard
|
||
@balance_sheet = Current.family.balance_sheet
|
||
@accounts = Current.family.accounts.visible.with_attached_logo
|
||
|
||
family_currency = Current.family.currency
|
||
|
||
# Use the same period for all widgets (set by Periodable concern)
|
||
income_totals = Current.family.income_statement.income_totals(period: @period)
|
||
expense_totals = Current.family.income_statement.expense_totals(period: @period)
|
||
|
||
@cashflow_sankey_data = build_cashflow_sankey_data(income_totals, expense_totals, family_currency)
|
||
@outflows_data = build_outflows_donut_data(expense_totals)
|
||
|
||
@breadcrumbs = [ [ "Home", root_path ], [ "Dashboard", nil ] ]
|
||
end
|
||
|
||
def changelog
|
||
@release_notes = github_provider.fetch_latest_release_notes
|
||
|
||
# Fallback if no release notes are available
|
||
if @release_notes.nil?
|
||
@release_notes = {
|
||
avatar: "https://github.com/we-promise.png",
|
||
username: "we-promise",
|
||
name: "Release notes unavailable",
|
||
published_at: Date.current,
|
||
body: "<p>Unable to fetch the latest release notes at this time. Please check back later or visit our <a href='https://github.com/we-promise/sure/releases' target='_blank'>GitHub releases page</a> directly.</p>"
|
||
}
|
||
end
|
||
|
||
render layout: "settings"
|
||
end
|
||
|
||
def feedback
|
||
render layout: "settings"
|
||
end
|
||
|
||
def redis_configuration_error
|
||
render layout: "blank"
|
||
end
|
||
|
||
private
|
||
def github_provider
|
||
Provider::Registry.get_provider(:github)
|
||
end
|
||
|
||
def build_cashflow_sankey_data(income_totals, expense_totals, currency_symbol)
|
||
nodes = []
|
||
links = []
|
||
node_indices = {} # Memoize node indices by a unique key: "type_categoryid"
|
||
|
||
# Helper to add/find node and return its index
|
||
add_node = ->(unique_key, display_name, value, percentage, color) {
|
||
node_indices[unique_key] ||= begin
|
||
nodes << { name: display_name, value: value.to_f.round(2), percentage: percentage.to_f.round(1), color: color }
|
||
nodes.size - 1
|
||
end
|
||
}
|
||
|
||
total_income_val = income_totals.total.to_f.round(2)
|
||
total_expense_val = expense_totals.total.to_f.round(2)
|
||
|
||
# --- Create Central Cash Flow Node ---
|
||
cash_flow_idx = add_node.call("cash_flow_node", "Cash Flow", total_income_val, 0, "var(--color-success)")
|
||
|
||
# --- Process Income Side (Top-level categories only) ---
|
||
income_totals.category_totals.each do |ct|
|
||
# Skip subcategories – only include root income categories
|
||
next if ct.category.parent_id.present?
|
||
|
||
val = ct.total.to_f.round(2)
|
||
next if val.zero?
|
||
|
||
percentage_of_total_income = total_income_val.zero? ? 0 : (val / total_income_val * 100).round(1)
|
||
|
||
node_display_name = ct.category.name
|
||
node_color = ct.category.color.presence || Category::COLORS.sample
|
||
|
||
current_cat_idx = add_node.call(
|
||
"income_#{ct.category.id}",
|
||
node_display_name,
|
||
val,
|
||
percentage_of_total_income,
|
||
node_color
|
||
)
|
||
|
||
links << {
|
||
source: current_cat_idx,
|
||
target: cash_flow_idx,
|
||
value: val,
|
||
color: node_color,
|
||
percentage: percentage_of_total_income
|
||
}
|
||
end
|
||
|
||
# --- Process Expense Side (Top-level categories only) ---
|
||
expense_totals.category_totals.each do |ct|
|
||
# Skip subcategories – only include root expense categories to keep Sankey shallow
|
||
next if ct.category.parent_id.present?
|
||
|
||
val = ct.total.to_f.round(2)
|
||
next if val.zero?
|
||
|
||
percentage_of_total_expense = total_expense_val.zero? ? 0 : (val / total_expense_val * 100).round(1)
|
||
|
||
node_display_name = ct.category.name
|
||
node_color = ct.category.color.presence || Category::UNCATEGORIZED_COLOR
|
||
|
||
current_cat_idx = add_node.call(
|
||
"expense_#{ct.category.id}",
|
||
node_display_name,
|
||
val,
|
||
percentage_of_total_expense,
|
||
node_color
|
||
)
|
||
|
||
links << {
|
||
source: cash_flow_idx,
|
||
target: current_cat_idx,
|
||
value: val,
|
||
color: node_color,
|
||
percentage: percentage_of_total_expense
|
||
}
|
||
end
|
||
|
||
# --- Process Surplus ---
|
||
leftover = (total_income_val - total_expense_val).round(2)
|
||
if leftover.positive?
|
||
percentage_of_total_income_for_surplus = total_income_val.zero? ? 0 : (leftover / total_income_val * 100).round(1)
|
||
surplus_idx = add_node.call("surplus_node", "Surplus", leftover, percentage_of_total_income_for_surplus, "var(--color-success)")
|
||
links << { source: cash_flow_idx, target: surplus_idx, value: leftover, color: "var(--color-success)", percentage: percentage_of_total_income_for_surplus }
|
||
end
|
||
|
||
# Update Cash Flow and Income node percentages (relative to total income)
|
||
if node_indices["cash_flow_node"]
|
||
nodes[node_indices["cash_flow_node"]][:percentage] = 100.0
|
||
end
|
||
# No primary income node anymore, percentages are on individual income cats relative to total_income_val
|
||
|
||
{ nodes: nodes, links: links, currency_symbol: Money::Currency.new(currency_symbol).symbol }
|
||
end
|
||
|
||
def build_outflows_donut_data(expense_totals)
|
||
currency_symbol = Money::Currency.new(expense_totals.currency).symbol
|
||
total = expense_totals.total
|
||
|
||
# Only include top-level categories with non-zero amounts
|
||
categories = expense_totals.category_totals
|
||
.reject { |ct| ct.category.parent_id.present? || ct.total.zero? }
|
||
.sort_by { |ct| -ct.total }
|
||
.map do |ct|
|
||
{
|
||
id: ct.category.id,
|
||
name: ct.category.name,
|
||
amount: ct.total.to_f.round(2),
|
||
percentage: ct.weight.round(1),
|
||
color: ct.category.color.presence || Category::UNCATEGORIZED_COLOR,
|
||
icon: ct.category.lucide_icon
|
||
}
|
||
end
|
||
|
||
{ categories: categories, total: total.to_f.round(2), currency_symbol: currency_symbol }
|
||
end
|
||
end
|