Files
sure/app/views/reports/_comparison_chart.html.erb
soky srm d9f8d064af Implement Reporting tab (#276)
* First reporting version

* Fixes for all tabs

* Transactions table

* Budget section re-design

* FIX exports

Fix transactions table aggregation

* Add support for google sheets

Remove pdf and xlsx for now

* Multiple fixes

- Trends & Insights now follows top filter
- Transactions Breakdown removed filters, implemented sort by amount.
- The entire section follows top filters.
- Export to CSV adds per month breakdown

* Linter and tests

* Fix amounts

- Correctly handle amounts across the views and controller.
- Pass proper values to do calculation on, and not loose precision

* Update Gemfile.lock

* Add support for api-key on reports

Also fix custom date filter

* Review fixes

* Move budget status calculations out of the view.

* fix ensures that quarterly reports end at the quarter boundary

* Fix bugdet days remaining

Fix raw css style

* Fix test

* Implement google sheets properly with hotwire

* Improve UX on period comparison

* FIX csv export for non API key auth
2025-11-05 14:54:45 +01:00

251 lines
12 KiB
Plaintext

<%
currency = Current.family.currency
# Helper to calculate percentage change and determine if it's good or bad
def comparison_class(current, previous, inverse: false)
return "text-primary" if previous.zero?
change = current - previous
is_positive_change = change > 0
# For expenses, lower is better (inverse logic)
is_good = inverse ? !is_positive_change : is_positive_change
is_good ? "text-green-600" : "text-gray-600"
end
def percentage_change(current, previous)
return 0 if previous.zero?
((current - previous) / previous.abs * 100).round(1)
end
%>
<div>
<div class="flex items-center justify-between mb-6">
<h2 class="text-lg font-medium text-primary">
<%= t("reports.comparison.title") %>
</h2>
<p class="text-sm text-tertiary">
<%= t("reports.comparison.currency", symbol: comparison_data[:currency_symbol]) %>
</p>
</div>
<div class="space-y-6">
<%# Income Comparison %>
<div>
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-medium text-secondary flex items-center gap-2">
<%= icon("trending-up", class: "w-4 h-4 text-success") %>
<%= t("reports.comparison.income") %>
</h3>
</div>
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-3">
<span class="text-2xl font-semibold <%= comparison_class(comparison_data[:current][:income], comparison_data[:previous][:income]) %>">
<%= Money.new(comparison_data[:current][:income], currency).format %>
</span>
<% change = percentage_change(comparison_data[:current][:income], comparison_data[:previous][:income]) %>
<% if change != 0 %>
<% income_improved = comparison_data[:current][:income] > comparison_data[:previous][:income] %>
<div class="flex items-center gap-1.5">
<span class="text-sm font-medium <%= comparison_class(comparison_data[:current][:income], comparison_data[:previous][:income]) %>">
<%= change >= 0 ? "+" : "" %><%= change %>%
</span>
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium <%= income_improved ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600' %>">
<%= icon(income_improved ? "trending-up" : "trending-down", class: "w-3 h-3") %>
<%= t(income_improved ? "reports.comparison.status.improved" : "reports.comparison.status.decreased") %>
</span>
</div>
<% end %>
</div>
<span class="text-sm text-tertiary">
<%= t("reports.comparison.previous") %>: <%= Money.new(comparison_data[:previous][:income], currency).format %>
</span>
</div>
<%# Overlapping bars %>
<div class="relative h-10">
<%
current_income_abs = comparison_data[:current][:income].to_f.abs
previous_income_abs = comparison_data[:previous][:income].to_f.abs
max_income = [current_income_abs, previous_income_abs].max
if max_income > 0
current_width = [3, (current_income_abs / max_income * 100)].max
previous_width = [3, (previous_income_abs / max_income * 100)].max
else
current_width = 0
previous_width = 0
end
# Income: green if increased, gray/primary if decreased
income_increased = comparison_data[:current][:income] >= comparison_data[:previous][:income]
income_bar_color = income_increased ? "bg-green-500" : "bg-gray-600"
income_bg_color = income_increased ? "bg-green-200" : "bg-gray-300"
%>
<% if previous_width > 0 || current_width > 0 %>
<%# Previous period bar (background) %>
<% if previous_width > 0 %>
<div class="absolute top-0 left-0 h-10 <%= income_bg_color %> rounded-lg transition-all duration-500"
style="width: <%= previous_width %>%"></div>
<% end %>
<%# Current period bar (foreground) %>
<% if current_width > 0 %>
<div class="absolute top-2 left-0 h-6 <%= income_bar_color %> rounded-lg transition-all duration-500"
style="width: <%= current_width %>%"></div>
<% end %>
<% else %>
<div class="flex items-center justify-center h-10 text-sm text-tertiary">
<%= t("reports.comparison.no_data") %>
</div>
<% end %>
</div>
</div>
<%# Expenses Comparison %>
<div>
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-medium text-secondary flex items-center gap-2">
<%= icon("trending-down", class: "w-4 h-4 text-danger") %>
<%= t("reports.comparison.expenses") %>
</h3>
</div>
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-3">
<span class="text-2xl font-semibold <%= comparison_class(comparison_data[:current][:expenses], comparison_data[:previous][:expenses], inverse: true) %>">
<%= Money.new(comparison_data[:current][:expenses], currency).format %>
</span>
<% change = percentage_change(comparison_data[:current][:expenses], comparison_data[:previous][:expenses]) %>
<% if change != 0 %>
<% expenses_improved = comparison_data[:current][:expenses] < comparison_data[:previous][:expenses] %>
<div class="flex items-center gap-1.5">
<span class="text-sm font-medium <%= comparison_class(comparison_data[:current][:expenses], comparison_data[:previous][:expenses], inverse: true) %>">
<%= change >= 0 ? "+" : "" %><%= change %>%
</span>
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium <%= expenses_improved ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600' %>">
<%= icon(expenses_improved ? "trending-down" : "trending-up", class: "w-3 h-3") %>
<%= t(expenses_improved ? "reports.comparison.status.reduced" : "reports.comparison.status.increased") %>
</span>
</div>
<% end %>
</div>
<span class="text-sm text-tertiary">
<%= t("reports.comparison.previous") %>: <%= Money.new(comparison_data[:previous][:expenses], currency).format %>
</span>
</div>
<%# Overlapping bars %>
<div class="relative h-10">
<%
current_expenses_abs = comparison_data[:current][:expenses].to_f.abs
previous_expenses_abs = comparison_data[:previous][:expenses].to_f.abs
max_expenses = [current_expenses_abs, previous_expenses_abs].max
if max_expenses > 0
current_width = [3, (current_expenses_abs / max_expenses * 100)].max
previous_width = [3, (previous_expenses_abs / max_expenses * 100)].max
else
current_width = 0
previous_width = 0
end
# Expenses: green if decreased (inverse logic), gray/primary if increased
expenses_decreased = comparison_data[:current][:expenses] <= comparison_data[:previous][:expenses]
expenses_bar_color = expenses_decreased ? "bg-green-500" : "bg-gray-600"
expenses_bg_color = expenses_decreased ? "bg-green-200" : "bg-gray-300"
%>
<% if previous_width > 0 || current_width > 0 %>
<%# Previous period bar (background) %>
<% if previous_width > 0 %>
<div class="absolute top-0 left-0 h-10 <%= expenses_bg_color %> rounded-lg transition-all duration-500"
style="width: <%= previous_width %>%"></div>
<% end %>
<%# Current period bar (foreground) %>
<% if current_width > 0 %>
<div class="absolute top-2 left-0 h-6 <%= expenses_bar_color %> rounded-lg transition-all duration-500"
style="width: <%= current_width %>%"></div>
<% end %>
<% else %>
<div class="flex items-center justify-center h-10 text-sm text-tertiary">
<%= t("reports.comparison.no_data") %>
</div>
<% end %>
</div>
</div>
<%# Net Savings Comparison %>
<div>
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-medium text-secondary flex items-center gap-2">
<%= icon("piggy-bank", class: "w-4 h-4 text-primary") %>
<%= t("reports.comparison.net_savings") %>
</h3>
</div>
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-3">
<span class="text-2xl font-semibold <%= comparison_class(comparison_data[:current][:net], comparison_data[:previous][:net]) %>">
<%= Money.new(comparison_data[:current][:net], currency).format %>
</span>
<% change = percentage_change(comparison_data[:current][:net], comparison_data[:previous][:net]) %>
<% if change != 0 %>
<% net_improved = comparison_data[:current][:net] > comparison_data[:previous][:net] %>
<div class="flex items-center gap-1.5">
<span class="text-sm font-medium <%= comparison_class(comparison_data[:current][:net], comparison_data[:previous][:net]) %>">
<%= change >= 0 ? "+" : "" %><%= change %>%
</span>
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium <%= net_improved ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600' %>">
<%= icon(net_improved ? "trending-up" : "trending-down", class: "w-3 h-3") %>
<%= t(net_improved ? "reports.comparison.status.improved" : "reports.comparison.status.decreased") %>
</span>
</div>
<% end %>
</div>
<span class="text-sm text-tertiary">
<%= t("reports.comparison.previous") %>: <%= Money.new(comparison_data[:previous][:net], currency).format %>
</span>
</div>
<%# Overlapping bars %>
<div class="relative h-10">
<%
current_net_abs = comparison_data[:current][:net].to_f.abs
previous_net_abs = comparison_data[:previous][:net].to_f.abs
max_net = [current_net_abs, previous_net_abs].max
if max_net > 0
current_width = [3, (current_net_abs / max_net * 100)].max
previous_width = [3, (previous_net_abs / max_net * 100)].max
else
current_width = 0
previous_width = 0
end
# Net Savings: green if improved (increased), gray/primary if got worse
net_improved = comparison_data[:current][:net] >= comparison_data[:previous][:net]
net_bar_color = net_improved ? "bg-green-500" : "bg-gray-600"
net_bg_color = net_improved ? "bg-green-200" : "bg-gray-300"
%>
<% if previous_width > 0 || current_width > 0 %>
<%# Previous period bar (background) %>
<% if previous_width > 0 %>
<div class="absolute top-0 left-0 h-10 <%= net_bg_color %> rounded-lg transition-all duration-500"
style="width: <%= previous_width %>%"></div>
<% end %>
<%# Current period bar (foreground) %>
<% if current_width > 0 %>
<div class="absolute top-2 left-0 h-6 <%= net_bar_color %> rounded-lg transition-all duration-500"
style="width: <%= current_width %>%"></div>
<% end %>
<% else %>
<div class="flex items-center justify-center h-10 text-sm text-tertiary">
<%= t("reports.comparison.no_data") %>
</div>
<% end %>
</div>
</div>
</div>
</div>