Investments Page Refinements — Design
Date: 2026-05-28
Scope: /investments page form, Liquidez tab, Distribuição tab, Timeline tab
Goals
- Slim down the new-investment modal by removing
statusandholderinputs. - Derive
statusfrompurchase_date,liquidity_date, andmaturity_dateinstead of asking the user. - Fix the Resumo Mensal de Liquidez showing maturity values; show both liquidity and maturity values per month.
- In Calendário de Vencimentos, show both liquidity and maturity values per month with clear visual differentiation.
- Sort Próximos Vencimentos by
maturity_dateascending. - Merge the Distribuição and Timeline tabs into a single Distribuição tab.
1. Form — remove Status & Holder
File: app/views/investments/_form.html.erb
- Drop the Status input (line 97–103) and the Titular input (line 104–109).
- The fourth row becomes
grid-cols-1 sm:grid-cols-2: Indexador + Taxa only.
File: app/controllers/investments_controller.rb
- Remove
:statusand:holderfrominvestment_params(line 135).
DB: No migration. Columns status and holder remain (still used by admin filters, MCP tools, model indexes). They become internal — never written via the user form.
2. Derived status
File: app/models/investment.rb
Add liquidity_reached to STATUSES and the enum :status mapping.
STATUSES = %w[active liquidity_reached maturing_soon matured].freeze
enum :status, {
active: "active",
liquidity_reached: "liquidity_reached",
maturing_soon: "maturing_soon",
matured: "matured"
}, default: :active, prefix: true
Add a before_validation :recompute_status callback:
def recompute_status
today = Date.current
self.status =
if maturity_date.present? && maturity_date < today
"matured"
elsif maturity_date.present? && maturity_date <= today + 30.days
"maturing_soon"
elsif liquidity_date.present? && liquidity_date <= today &&
(maturity_date.blank? || maturity_date > today)
"liquidity_reached"
else
"active"
end
end
Update status_label:
def status_label
case status
when "active" then "Ativo"
when "liquidity_reached" then "Disponível"
when "maturing_soon" then "Vencendo em breve"
when "matured" then "Vencido"
else status&.titleize
end
end
Cascading changes
app/views/investments/_investment_row.html.erb: add a fourthelsifforstatus_liquidity_reached?— teal badge (#06b6d4, bgrgba(6,182,212,0.12)), label "Disponível".app/views/investments/_timeline_tab.html.erb: extendstatus_configwith"liquidity_reached" => { label: "Disponível", color: "#06b6d4", bg: "rgba(6,182,212,0.12)" }(used by the cards section that moves to the merged Distribuição tab).- i18n entries
investments.statuses.liquidity_reachedinconfig/locales/{pt-BR,en}.ymlfor both languages (mirroring existing keys).
Data backfill
No backfill task — the callback runs on next save. Existing records keep their stored value until edited. Acceptable because:
- Existing status values are still valid (
active,maturing_soon,matured). - The new
liquidity_reachedstate will only appear after the next save event.
3. Liquidez tab — Calendário de Vencimentos (grouped bars)
File: app/views/investments/_liquidez_tab.html.erb
Build two parallel monthly series:
<% monthly_totals = calendar_months.map { |m|
liq_invs = maturity_investments.select { |i| i.liquidity_date.present? && i.liquidity_date.beginning_of_month == m }
mat_invs = maturity_investments.select { |i| i.maturity_date.present? && i.maturity_date.beginning_of_month == m }
{
month: m,
liquidity_total: liq_invs.sum(&:current_value),
maturity_total: mat_invs.sum(&:current_value),
liquidity_invs: liq_invs,
maturity_invs: mat_invs
}
} %>
<% max_bar = [monthly_totals.flat_map { |m| [m[:liquidity_total], m[:maturity_total]] }.max || 1, 1].max %>
Render two narrow bars side-by-side per month:
- Blue bar (
#3b82f6linear gradient) forliquidity_total - Orange bar (
#f97316linear gradient) formaturity_total
Add a legend above the chart:
<div class="flex items-center gap-4 mb-3 text-[11px]">
<span class="flex items-center gap-1.5"><span class="w-3 h-3 rounded-sm" style="background:#3b82f6"></span>Liquidez</span>
<span class="flex items-center gap-1.5"><span class="w-3 h-3 rounded-sm" style="background:#f97316"></span>Vencimento</span>
</div>
Update subtitle: "Liquidez (azul) e Vencimentos (laranja) por mês".
4. Liquidez tab — Resumo Mensal cards
File: app/views/investments/_liquidez_tab.html.erb
Each card uses the new monthly_totals structure. Two rows per card:
<div class="rounded-2xl p-4 bg-white dark:bg-[#161b22] border border-gray-200/60 dark:border-white/[0.06]" style="min-height: 110px;">
<p class="text-[12px] font-bold text-gray-700 dark:text-gray-200 mb-2"><%= format_month_pt(m[:month]) %></p>
<div class="flex items-center justify-between mb-0.5">
<span class="text-[11px] font-medium" style="color:#3b82f6;">Liquidez</span>
<span class="text-[13px] font-bold text-gray-900 dark:text-white">
<%= m[:liquidity_total] > 0 ? format_brl(m[:liquidity_total]) : "—" %>
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-[11px] font-medium" style="color:#f97316;">Vencimento</span>
<span class="text-[13px] font-bold text-gray-900 dark:text-white">
<%= m[:maturity_total] > 0 ? format_brl(m[:maturity_total]) : "—" %>
</span>
</div>
<% combined_names = (m[:liquidity_invs] + m[:maturity_invs]).map(&:name).uniq %>
<% if combined_names.any? %>
<p class="text-[10px] text-gray-500 mt-1.5 truncate"><%= combined_names.join(" • ") %></p>
<% end %>
</div>
Drop the obsolete accent_colors lookup.
5. Próximos Vencimentos sort
File: app/views/investments/_liquidez_tab.html.erb
Replace the current bucket sort (lines 29–37) with:
<% sorted_investments = maturity_investments.sort_by { |i| i.maturity_date || Date.new(9999, 12, 31) } %>
(Investments without maturity_date go last — but note maturity_investments is already scoped via with_maturity, so nil isn't expected.)
6. Merge Distribuição + Timeline tabs
Tabs list
File: app/views/investments/index.html.erb (line 48)
<% tabs = [
["lista", t("investments.tabs.list")],
["distribuicao", t("investments.tabs.distribution")],
["liquidez", t("investments.tabs.liquidez")]
] %>
Remove the timeline tab entry. Remove the <div data-investments-page-target="tabContent" data-tab="timeline"> block (lines 135–138).
If params[:tab] == "timeline", the controller will redirect to distribuicao.
Controller
File: app/controllers/investments_controller.rb
- Remove
@timeline_monthscomputation (no longer used). - Keep
@timeline_cardsrename it to@upcoming_maturity_cardsfor clarity; keep the same logic: investments withmaturity_date, sorted bymaturity_dateasc, first 6. - In
index:@active_tab = (params[:tab] == "timeline") ? "distribuicao" : (params[:tab] || "lista").
Distribuição partial
File: app/views/investments/_distribuicao_tab.html.erb
After the existing 2-column grid (donut + bars), append a "Próximos Vencimentos" section:
<% if @upcoming_maturity_cards.any? %>
<div class="mt-6 bg-white dark:bg-[#161b22] rounded-2xl p-5 border border-gray-200/60 dark:border-white/[0.06]">
<h3 class="text-[15px] font-bold text-gray-900 dark:text-white mb-4">Próximos Vencimentos</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<% @upcoming_maturity_cards.each do |inv| %>
<% st = status_config[inv.status.to_s] || status_config["active"] %>
<div class="rounded-xl p-4 flex items-center justify-between bg-[#f8fafc] dark:bg-[#1e2736]">
<div class="min-w-0 pr-3">
<p class="text-[13px] font-semibold text-gray-900 dark:text-white truncate"><%= inv.name %></p>
<p class="text-[11px] text-gray-500 dark:text-gray-400 mt-0.5">Vence: <%= l(inv.maturity_date) %></p>
</div>
<div class="text-right shrink-0">
<p class="text-[13px] font-bold text-gray-900 dark:text-white mb-1"><%= format_brl(inv.current_value) %></p>
<span class="inline-flex items-center px-2 py-0.5 rounded-lg text-[10px] font-bold"
style="background: <%= st[:bg] %>; color: <%= st[:color] %>;">
<%= st[:label] %>
</span>
</div>
</div>
<% end %>
</div>
</div>
<% end %>
Move the status_config definition to the top of _distribuicao_tab.html.erb (from _timeline_tab.html.erb). Delete _timeline_tab.html.erb.
i18n changes
config/locales/pt-BR.yml, config/locales/en.yml:
- Add
investments.statuses.liquidity_reached("Disponível" / "Available") - Remove no longer used keys:
investments.tabs.timeline,investments.fields.holder,investments.fields.status(only if no other view references them).
Out of scope
- Backfilling existing investment status values
- Removing the
holder/statusDB columns - Updating Investment admin views (admin still shows status)
- Updating MCP tool schemas (status/holder fields kept for API users)
Files changed
| File | Change |
|---|---|
app/models/investment.rb |
Add liquidity_reached, before_validation :recompute_status, label, comment update |
app/controllers/investments_controller.rb |
Drop status/holder from params, rename @timeline_cards, drop @timeline_months, redirect timeline → distribuicao |
app/views/investments/_form.html.erb |
Remove status + holder inputs; row 4 becomes 2 cols |
app/views/investments/_investment_row.html.erb |
Add liquidity_reached badge case |
app/views/investments/_liquidez_tab.html.erb |
Grouped bars (chart), 2-row monthly cards, sort by maturity_date |
app/views/investments/_distribuicao_tab.html.erb |
Append "Próximos Vencimentos" cards section + status_config |
app/views/investments/_timeline_tab.html.erb |
Delete |
app/views/investments/index.html.erb |
Remove timeline tab + tab content block |
config/locales/pt-BR.yml, en.yml |
Add statuses.liquidity_reached; remove timeline tab key, holder/status field keys |