invoices: statistics page
Add /admin/invoices/stats with a year filter: invoiced, received and outstanding totals in GEL at each invoice's captured rate (the one currency every invoice has), overdue count, average days to pay, money received per month by payment date as a bar chart with the same figures in a table, per-currency totals, top clients and crypto option usage. Cancelled invoices are left out of every amount; hidden ones follow the usual superuser-only visibility. Assisted-by: Claude:opus-5
This commit is contained in:
parent
12d29dec0c
commit
9128867a33
7 changed files with 282 additions and 1 deletions
98
views/admin/invoices/stats.erb
Normal file
98
views/admin/invoices/stats.erb
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<% s = @stats %>
|
||||
<% money = ->(v) { whole, frac = ('%.2f' % v).split('.'); "#{whole.reverse.scan(/\d{1,3}/).join(',').reverse.sub(/\A,/, '')}.#{frac}" } %>
|
||||
<% gel = ->(v) { "₾ #{money.(v)}" } %>
|
||||
<%== erb :'admin/_nav', layout: false %>
|
||||
<div class="header">
|
||||
<h1>Invoice statistics</h1>
|
||||
<p class="tagline"><a href="/admin/invoices">← invoices</a></p>
|
||||
</div>
|
||||
|
||||
<nav class="year-filter" aria-label="Period">
|
||||
<a href="/admin/invoices/stats" class="<%= 'active' unless s.year %>">all time</a>
|
||||
<% s.years.each do |y| %><a href="/admin/invoices/stats?year=<%= y %>" class="<%= 'active' if s.year == y %>"><%= y %></a><% end %>
|
||||
</nav>
|
||||
|
||||
<section class="section">
|
||||
<div class="stats stats-6">
|
||||
<div><b><%= gel.(s.gel(s.live)) %></b><span>invoiced</span></div>
|
||||
<div><b><%= gel.(s.gel(s.paid)) %></b><span>received</span></div>
|
||||
<div><b><%= gel.(s.gel(s.unpaid)) %></b><span>outstanding</span></div>
|
||||
<div><b><%= s.overdue.size %></b><span>overdue</span></div>
|
||||
<div><b><%= s.avg_days_to_pay ? "#{s.avg_days_to_pay} d" : '—' %></b><span>avg. time to pay</span></div>
|
||||
<div><b><%= s.live.size %><% if s.cancelled.any? %> <small>+<%= s.cancelled.size %> cancelled</small><% end %></b><span>invoices</span></div>
|
||||
</div>
|
||||
<p class="optional">GEL at each invoice's captured rate. Cancelled invoices are excluded from all amounts.</p>
|
||||
</section>
|
||||
|
||||
<% rows = s.monthly %>
|
||||
<% if rows.any? %>
|
||||
<section class="section">
|
||||
<h2>Received per month <span class="optional">(₾, by payment date)</span></h2>
|
||||
<% peak = rows.map(&:last).max %>
|
||||
<% peak = BigDecimal('1') if peak.nil? || peak.zero? %>
|
||||
<figure class="bar-chart" role="img" aria-label="Received per month in GEL; exact values in the table below">
|
||||
<div class="bar-axis"><span><%= gel.(peak) %></span><span>0</span></div>
|
||||
<div class="bars">
|
||||
<% rows.each do |m, _n, _inv, rec| %>
|
||||
<div class="bar-col" tabindex="0" data-tip="<%= m.strftime('%b %Y') %>: <%= gel.(rec) %>">
|
||||
<div class="bar" style="height: <%= (rec / peak * 100).round(1).to_f %>%"></div>
|
||||
<span class="bar-label"><%= rows.size > 12 ? m.strftime('%m/%y') : m.strftime('%b') %></span>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</figure>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>By month</h2>
|
||||
<table class="invoice-list">
|
||||
<thead><tr><th>Month</th><th class="num">Issued</th><th class="num">Invoiced ₾</th><th class="num">Received ₾</th></tr></thead>
|
||||
<tbody>
|
||||
<% rows.reverse_each do |m, n, inv, rec| %>
|
||||
<% next if n.zero? && rec.zero? %>
|
||||
<tr><td class="mono"><%= m.strftime('%Y-%m') %></td><td class="num mono"><%= n %></td><td class="num mono"><%= money.(inv) %></td><td class="num mono"><%= money.(rec) %></td></tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<div class="dash">
|
||||
<section class="section">
|
||||
<h2>By currency</h2>
|
||||
<table class="invoice-list">
|
||||
<thead><tr><th>Currency</th><th class="num">Invoiced</th><th class="num">Received</th><th class="num">Outstanding</th></tr></thead>
|
||||
<tbody>
|
||||
<% s.by_currency.each do |cur, v| %>
|
||||
<tr><td class="mono"><%= cur %></td><td class="num mono"><%= money.(v[:invoiced]) %></td><td class="num mono"><%= money.(v[:paid]) %></td><td class="num mono"><%= money.(v[:outstanding]) %></td></tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Top clients</h2>
|
||||
<table class="invoice-list">
|
||||
<thead><tr><th>Client</th><th class="num">Invoices</th><th class="num">Invoiced ₾</th></tr></thead>
|
||||
<tbody>
|
||||
<% s.top_clients.each do |client, n, g| %>
|
||||
<tr><td><%= client %></td><td class="num mono"><%= n %></td><td class="num mono"><%= money.(g) %></td></tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<% if s.crypto_usage.any? %>
|
||||
<section class="section">
|
||||
<h2>Crypto payment option</h2>
|
||||
<table class="invoice-list">
|
||||
<thead><tr><th>Asset</th><th class="num">Invoices</th></tr></thead>
|
||||
<tbody>
|
||||
<% s.crypto_usage.each do |coin, n| %>
|
||||
<tr><td><span class="mono"><%= coin %></span> <span class="optional"><%= CryptoAsset.name(coin) %></span></td><td class="num mono"><%= n %></td></tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<% end %>
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue