Files
WYGIWYH/app/templates/insights/fragments/overview/index.html
T

284 lines
13 KiB
HTML

{% load i18n %}
<div hx-get="{{ refresh_url }}" hx-trigger="updated from:window" class="show-loading" hx-swap="outerHTML"
hx-include="#picker-form, #picker-type, #view-type, #show-level-2, #showing, #show-level-3">
<div class="h-full text-center mb-4">
<div class="tabs tabs-box mx-auto w-fit" role="group" id="view-type" _="on change trigger updated">
<label class="tab">
<input type="radio"
name="view_type"
id="table-view"
autocomplete="off"
value="table"
aria-label="{% trans 'Table' %}"
{% if view_type == "table" %}checked{% endif %}>
<i class="fa-solid fa-table fa-fw me-2"></i>
{% trans 'Table' %}
</label>
<label class="tab">
<input type="radio"
name="view_type"
id="bars-view"
autocomplete="off"
value="bars"
aria-label="{% trans 'Bars' %}"
{% if view_type == "bars" %}checked{% endif %}>
<i class="fa-solid fa-chart-bar fa-fw me-2"></i>
{% trans 'Bars' %}
</label>
</div>
</div>
<div class="my-3 flex flex-col gap-3 md:flex-row justify-between">
<div class="flex gap-4">
{% if view_type == 'table' %}
<div id="show-level-2">
<label class="label">
<input type="hidden" name="show_level_2" value="off">
<input type="checkbox" class="toggle toggle-primary toggle-sm" id="show-level-2-switch" name="show_level_2"
_="on change trigger updated" {% if show_level_2 %}checked{% endif %}>
<span>
{{ level_2.label }}
</span>
<c-ui.help-icon
content="{% trans 'Transaction amounts associated with multiple tags or entities will be counted once for each one of them' %}"
icon="fa-solid fa-circle-exclamation"></c-ui.help-icon>
</label>
</div>
<div id="show-level-3" class="{% if not show_level_2 %}hidden{% endif %}">
<label class="label">
<input type="hidden" name="show_level_3" value="off">
<input type="checkbox" class="toggle toggle-primary toggle-sm" id="show-level-3-switch"
name="show_level_3"
_="on change trigger updated" {% if show_level_3 %}checked{% endif %}>
<span>
{{ level_3.label }}
</span>
<c-ui.help-icon
content="{% trans 'Transaction amounts associated with multiple tags or entities will be counted once for each one of them' %}"
icon="fa-solid fa-circle-exclamation"></c-ui.help-icon>
</label>
</div>
{% endif %}
</div>
<div class="join" role="group" id="showing" _="on change trigger updated">
<input type="radio" class="join-item btn btn-outline btn-primary btn-sm" name="showing" id="showing-projected"
autocomplete="off" aria-label="{% trans 'Projected' %}"
value="projected" {% if showing == 'projected' %}checked{% endif %}>
<input type="radio" class="join-item btn btn-outline btn-primary btn-sm" name="showing" id="showing-current"
autocomplete="off" value="current" aria-label="{% trans 'Current' %}"
{% if showing == 'current' %}checked{% endif %}>
<input type="radio" class="join-item btn btn-outline btn-primary btn-sm" name="showing" id="showing-final"
autocomplete="off" value="final" aria-label="{% trans 'Final total' %}"
{% if showing == 'final' %}checked{% endif %}>
</div>
</div>
{% if total_table %}
{% if view_type == "table" %}
<div class="card bg-base-100 card-border">
<div class="card-body">
<c-config.search></c-config.search>
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th scope="col">{{ level_1.singular }}</th>
<th scope="col">{% trans 'Income' %}</th>
<th scope="col">{% trans 'Expense' %}</th>
<th scope="col">{% trans 'Total' %}</th>
</tr>
</thead>
<tbody>
{% for item in total_table.values %}
{# Top level row #}
{% include "insights/fragments/overview/_row.html" with node=item is_root=True row_class="font-semibold" empty_label=level_1.empty search_path=item.search_path %}
{# Second level rows #}
{% if show_level_2 %}
{% for child in item.children.values %}
{% if child.name or item.children.values|length > 1 %}
{% include "insights/fragments/overview/_row.html" with node=child row_class="bg-base-200" indent="ps-6" icon=level_2.icon empty_label=level_2.empty search_path=child.search_path %}
{# Third level rows #}
{% if show_level_3 %}
{% for grandchild in child.children.values %}
{% if grandchild.name or child.children.values|length > 1 %}
{% include "insights/fragments/overview/_row.html" with node=grandchild row_class="bg-base-300" indent="ps-10" icon=level_3.icon empty_label=level_3.empty search_path=grandchild.search_path %}
{% endif %}
{% endfor %}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% elif view_type == "bars" %}
<div class="card bg-base-100 card-border">
<div class="card-body">
<div class="chart-container relative h-[75vh] w-full" _="init call setupChart() end">
<canvas id="overviewChart"></canvas>
</div>
</div>
</div>
{{ total_table|json_script:"overviewData" }}
<script>
function setupChart() {
var rawData = JSON.parse(document.getElementById('overviewData').textContent);
// --- Dynamic Data Processing ---
var items = [];
var currencyDetails = {}; // Stores details like { BRL: {code: 'BRL', name: 'Real', ...}, ... }
var currencyData = {}; // Stores data arrays like { BRL: [val1, null, val3,...], ... }
// Pass 1: Collect items and currency details
Object.values(rawData).forEach(item => {
var itemName = item.name === null ? "{{ level_1.empty|escapejs }}" : item.name;
if (!items.includes(itemName)) {
items.push(itemName);
}
if (item.currencies) {
Object.values(item.currencies).forEach(curr => {
var details = curr.currency;
if (details && details.code && !currencyDetails[details.code]) {
var decimals = parseInt(details.decimal_places, 10);
currencyDetails[details.code] = {
code: details.code,
name: details.name || details.code,
prefix: details.prefix || '',
suffix: details.suffix || '',
// Ensure decimal_places is a non-negative integer
decimal_places: !isNaN(decimals) && decimals >= 0 ? decimals : 2
};
}
});
}
});
// Initialize data structure for each currency with nulls
Object.keys(currencyDetails).forEach(code => {
currencyData[code] = new Array(items.length).fill(null);
});
// Pass 2: Populate data arrays (store all valid numbers now)
Object.values(rawData).forEach(item => {
var itemName = item.name === null ? "{{ level_1.empty|escapejs }}" : item.name;
var itemIndex = items.indexOf(itemName);
if (itemIndex === -1) return;
if (item.currencies) {
Object.values(item.currencies).forEach(curr => {
var code = curr.currency?.code;
if (code && currencyData[code]) {
var value = parseFloat(curr.shown.total);
// Store the number if it's valid, otherwise keep null
currencyData[code][itemIndex] = !isNaN(value) ? value : null;
}
});
}
});
// --- Dynamic Chart Configuration ---
var datasets = Object.keys(currencyDetails).map((code, index) => {
return {
label: currencyDetails[code].name, // Use currency name for the legend label
data: currencyData[code],
currencyCode: code, // Store code for easy lookup in tooltip
borderWidth: 1
};
});
new Chart(document.getElementById('overviewChart'),
{
type: 'bar',
data: {
labels: items,
datasets: datasets
},
options: {
indexAxis: 'y',
responsive: true,
interaction: {
intersect: false,
mode: 'nearest',
axis: "y"
},
maintainAspectRatio: false,
plugins: {
title: {
display: false
},
tooltip: {
callbacks: {
label: function (context) {
const dataset = context.dataset;
const currencyCode = dataset.currencyCode;
const details = currencyDetails[currencyCode];
const value = context.parsed.x; // Use 'x' because indexAxis is 'y'
if (value === null || value === undefined || !details) {
// Display the item name if the value is null/undefined
return null;
}
let formattedValue = '';
try {
// Use Intl.NumberFormat for ALL values, configured with locale and exact decimal places
formattedValue = new Intl.NumberFormat(undefined, {
minimumFractionDigits: details.decimal_places,
maximumFractionDigits: details.decimal_places,
// Do NOT use style: 'currency' here, as we add prefix/suffix manually
}).format(value);
} catch (e) {
formattedValue = value.toFixed(details.decimal_places);
}
// Return label with currency name and formatted value including prefix/suffix
return `${details.prefix}${formattedValue}${details.suffix}`;
}
}
},
legend: {
position: 'top',
}
},
scales: {
x: {
stacked: true,
type: 'linear',
title: {
display: true,
text: '{% trans 'Total' %}'
},
ticks: {
// Format ticks using the detected locale
callback: function (value, index, ticks) {
return value.toLocaleString();
}
}
},
y: {
stacked: true,
title: {
display: false
}
}
}
}
});
}
</script>
{% endif %}
{% else %}
<c-msg.empty title="{{ empty_message }}"></c-msg.empty>
{% endif %}
</div>