feat: first batch of work

This commit is contained in:
Herculino Trotta
2025-11-01 03:15:44 -03:00
parent e600d87968
commit a63367a772
175 changed files with 3433 additions and 2245 deletions

View File

@@ -1,6 +1,6 @@
{% load i18n %}
{% if account_data.labels %}
<div class="chart-container tw:relative tw:h-[400px] tw:w-full"
<div class="chart-container relative h-[400px] w-full"
_="init call setupAccountChart() end">
<canvas id="accountChart"></canvas>
</div>

View File

@@ -1,6 +1,6 @@
{% load i18n %}
{% if currency_data.labels %}
<div class="chart-container tw:relative tw:h-[400px] tw:w-full"
<div class="chart-container relative h-[400px] w-full"
_="init call setupCurrencyChart() end">
<canvas id="currencyChart"></canvas>
</div>

View File

@@ -7,25 +7,25 @@
{% crispy category_form %}
</form>
<div class="tw:grid tw:grid-cols-1 tw:lg:grid-cols-2 tw:gap-3">
<div class="tw:w-full">
<div class="tw:card tw:bg-base-100 tw:shadow-xl tw:h-full">
<div class="tw:card-header tw:bg-base-200 tw:p-4 tw:font-semibold">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-3">
<div class="w-full">
<div class="card bg-base-100 shadow-xl h-full">
<div class="card-header bg-base-200 p-4 font-semibold">
{% trans "Income/Expense by Account" %}
</div>
<div class="tw:card-body">
<div class="card-body">
<div id="account-card" class="show-loading" hx-get="{% url 'category_sum_by_account' %}"
hx-trigger="updated from:window" hx-include="#category-form, #picker-form, #picker-type">
</div>
</div>
</div>
</div>
<div class="tw:w-full">
<div class="tw:card tw:bg-base-100 tw:shadow-xl tw:h-full">
<div class="tw:card-header tw:bg-base-200 tw:p-4 tw:font-semibold">
<div class="w-full">
<div class="card bg-base-100 shadow-xl h-full">
<div class="card-header bg-base-200 p-4 font-semibold">
{% trans "Income/Expense by Currency" %}
</div>
<div class="tw:card-body">
<div class="card-body">
<div id="currency-card" class="show-loading" hx-get="{% url 'category_sum_by_currency' %}"
hx-trigger="updated from:window" hx-include="#category-form, #picker-form, #picker-type">
</div>

View File

@@ -2,9 +2,9 @@
<div hx-get="{% url 'category_overview' %}" hx-trigger="updated from:window" class="show-loading" hx-swap="outerHTML"
hx-include="#picker-form, #picker-type, #view-type, #show-tags, #showing, #show-entities">
<div class="tw:h-full tw:text-center tw:mb-4">
<div class="tw:join" role="group" id="view-type" _="on change trigger updated">
<input type="radio" class="tw:join-item tw:btn tw:btn-outline tw:btn-primary tw:rounded-full"
<div class="h-full text-center mb-4">
<div class="join" role="group" id="view-type" _="on change trigger updated">
<input type="radio" class="join-item btn btn-outline btn-primary rounded-full"
name="view_type"
id="table-view"
autocomplete="off"
@@ -13,7 +13,7 @@
{% if view_type == "table" %}checked{% endif %}>
<input type="radio"
class="tw:join-item tw:btn tw:btn-outline tw:btn-primary tw:rounded-full"
class="join-item btn btn-outline btn-primary rounded-full"
name="view_type"
id="bars-view"
autocomplete="off"
@@ -22,16 +22,16 @@
{% if view_type == "bars" %}checked{% endif %}>
</div>
</div>
<div class="tw:mt-3 tw:mb-1 tw:flex tw:flex-col tw:md:flex-row tw:justify-between">
<div class="tw:flex tw:gap-4">
<div class="mt-3 mb-1 flex flex-col md:flex-row justify-between">
<div class="flex gap-4">
{% if view_type == 'table' %}
<div class="tw:form-control" id="show-tags">
<div class="form-control" id="show-tags">
<input type="hidden" name="show_tags" value="off">
<label class="tw:label tw:cursor-pointer tw:gap-2">
<input type="checkbox" class="tw:toggle" id="show-tags-switch" name="show_tags"
<label class="label cursor-pointer gap-2">
<input type="checkbox" class="toggle" id="show-tags-switch" name="show_tags"
_="on change trigger updated" {% if show_tags %}checked{% endif %}>
{% spaceless %}
<span class="tw:label-text">
<span class="label-text">
{% trans 'Tags' %}
</span>
<c-ui.help-icon
@@ -40,13 +40,13 @@
{% endspaceless %}
</label>
</div>
<div class="tw:form-control" id="show-entities" {% if not show_tags %}style="display: none;"{% endif %}>
<div class="form-control" id="show-entities" {% if not show_tags %}style="display: none;"{% endif %}>
<input type="hidden" name="show_entities" value="off">
<label class="tw:label tw:cursor-pointer tw:gap-2">
<input type="checkbox" class="tw:toggle" id="show-entities-switch" name="show_entities"
<label class="label cursor-pointer gap-2">
<input type="checkbox" class="toggle" id="show-entities-switch" name="show_entities"
_="on change trigger updated" {% if show_entities %}checked{% endif %}>
{% spaceless %}
<span class="tw:label-text">
<span class="label-text">
{% trans 'Entities' %}
</span>
<c-ui.help-icon
@@ -57,21 +57,21 @@
</div>
{% endif %}
</div>
<div class="tw:join" role="group" id="showing" _="on change trigger updated">
<input type="radio" class="tw:join-item tw:btn tw:btn-outline tw:btn-primary tw:btn-sm" name="showing" id="showing-projected" autocomplete="off" aria-label="{% trans 'Projected' %}"
<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="tw:join-item tw:btn tw:btn-outline tw:btn-primary tw:btn-sm" name="showing" id="showing-current" autocomplete="off" value="current" aria-label="{% trans 'Current' %}"
<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="tw:join-item tw:btn tw:btn-outline tw:btn-primary tw:btn-sm" name="showing" id="showing-final" autocomplete="off" value="final" aria-label="{% trans 'Final total' %}"
<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="tw:overflow-x-auto">
<table class="tw:table tw:table-zebra">
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th scope="col">{% trans 'Category' %}</th>
@@ -83,7 +83,7 @@
<tbody>
{% for category in total_table.values %}
<!-- Category row -->
<tr class="tw:font-semibold">
<tr class="font-semibold">
<th>{% if category.name %}{{ category.name }}{% else %}{% trans 'Uncategorized' %}{% endif %}</th>
<td> {# income #}
{% for currency in category.currencies.values %}
@@ -175,9 +175,9 @@
{% if show_tags %}
{% for tag_id, tag in category.tags.items %}
{% if tag.name or not tag.name and category.tags.values|length > 1 %}
<tr class="tw:bg-base-200">
<td class="tw:ps-4">
<i class="fa-solid fa-hashtag fa-fw tw:me-2 tw:text-base-content/60"></i>{% if tag.name %}{{ tag.name }}{% else %}{% trans 'Untagged' %}{% endif %}
<tr class="bg-base-200">
<td class="ps-4">
<i class="fa-solid fa-hashtag fa-fw me-2 text-base-content/60"></i>{% if tag.name %}{{ tag.name }}{% else %}{% trans 'Untagged' %}{% endif %}
</td>
<td>
{% for currency in tag.currencies.values %}
@@ -268,9 +268,9 @@
{% if show_entities %}
{% for entity_id, entity in tag.entities.items %}
{% if entity.name or not entity.name and tag.entities.values|length > 1 %}
<tr class="tw:bg-base-300">
<td class="tw:ps-5">
<i class="fa-solid fa-user-group fa-fw tw:me-2 tw:text-base-content/60"></i>{% if entity.name %}{{ entity.name }}{% else %}{% trans 'No entity' %}{% endif %}
<tr class="bg-base-300">
<td class="ps-5">
<i class="fa-solid fa-user-group fa-fw me-2 text-base-content/60"></i>{% if entity.name %}{{ entity.name }}{% else %}{% trans 'No entity' %}{% endif %}
</td>
<td>
{% for currency in entity.currencies.values %}
@@ -371,7 +371,7 @@
{% elif view_type == "bars" %}
<div>
<div class="chart-container tw:relative tw:h-[78vh] tw:w-full" _="init call setupChart() end">
<div class="chart-container relative h-[78vh] w-full" _="init call setupChart() end">
<canvas id="categoryChart"></canvas>
</div>
</div>

View File

@@ -3,33 +3,33 @@
<div hx-get="{% url 'insights_emergency_fund' %}" hx-trigger="updated from:window" class="show-loading"
hx-swap="outerHTML">
<div class="tw:join tw:join-vertical tw:w-full" id="emergency-fund-accordion">
<div class="join join-vertical w-full" id="emergency-fund-accordion">
{% for id, data in data.items %}
{% if data.average %}
<div class="tw:collapse tw:collapse-arrow tw:join-item tw:border-base-300 tw:border">
<div class="collapse collapse-arrow join-item border-base-300 border">
<input type="radio" name="emergency-fund-accordion" />
<div class="tw:collapse-title tw:text-base tw:font-medium">
<div class="collapse-title text-base font-medium">
<span>
<span class="tw:text-base-content/60">{% trans "You've spent an average of" %}</span>
<span class="text-base-content/60">{% trans "You've spent an average of" %}</span>
<c-amount.display
:amount="data.average"
:prefix="data.currency.prefix"
:suffix="data.currency.suffix"
:decimal_places="data.currency.decimal_places"
custom_class="tw:text-3xl"
custom_class="text-3xl"
divless></c-amount.display>
<span class="tw:text-base-content/60">{% trans 'on the last 12 months, at this rate you could go by' %}</span>
<span class="tw:text-3xl">{{ data.months }}</span>
<span class="tw:text-base-content/60">{% trans 'months without any income.' %}</span>
<span class="text-base-content/60">{% trans 'on the last 12 months, at this rate you could go by' %}</span>
<span class="text-3xl">{{ data.months }}</span>
<span class="text-base-content/60">{% trans 'months without any income.' %}</span>
</span>
</div>
<div class="tw:collapse-content">
<div class="tw:flex tw:justify-between tw:items-baseline tw:mt-2">
<div class="tw:text-end tw:font-mono">
<div class="tw:text-base-content/60">{% translate 'average expenses' %}</div>
<div class="collapse-content">
<div class="flex justify-between items-baseline mt-2">
<div class="text-end font-mono">
<div class="text-base-content/60">{% translate 'average expenses' %}</div>
</div>
<div class="dotted-line tw:flex-grow"></div>
<div class="tw:text-end tw:font-mono">
<div class="dotted-line flex-grow"></div>
<div class="text-end font-mono">
<c-amount.display
:amount="data.average"
:prefix="data.currency.prefix"
@@ -38,12 +38,12 @@
color="red"></c-amount.display>
</div>
</div>
<div class="tw:flex tw:justify-between tw:items-baseline tw:mt-2">
<div class="tw:text-end tw:font-mono">
<div class="tw:text-base-content/60">{% translate 'liquid total' %}</div>
<div class="flex justify-between items-baseline mt-2">
<div class="text-end font-mono">
<div class="text-base-content/60">{% translate 'liquid total' %}</div>
</div>
<div class="dotted-line tw:flex-grow"></div>
<div class="tw:text-end tw:font-mono">
<div class="dotted-line flex-grow"></div>
<div class="text-end font-mono">
<c-amount.display
:amount="data.total_current"
:prefix="data.currency.prefix"
@@ -52,12 +52,12 @@
color="{% if data.total_current > 0 %}green{% elif data.total_current < 0 %}red{% endif %}"></c-amount.display>
</div>
</div>
<div class="tw:flex tw:justify-between tw:items-baseline tw:mt-2">
<div class="tw:text-end tw:font-mono">
<div class="tw:text-base-content/60">{% translate 'months left' %}</div>
<div class="flex justify-between items-baseline mt-2">
<div class="text-end font-mono">
<div class="text-base-content/60">{% translate 'months left' %}</div>
</div>
<div class="dotted-line tw:flex-grow"></div>
<div class="tw:text-end tw:font-mono">
<div class="dotted-line flex-grow"></div>
<div class="text-end font-mono">
<span>{{ data.months }}</span>
</div>
</div>

View File

@@ -7,7 +7,7 @@
<div class="show-loading" hx-get="{% url 'insights_sankey_by_currency' %}" hx-trigger="updated from:window"
hx-swap="outerHTML" hx-include="#picker-form, #picker-type">
{% endif %}
<div class="chart-container tw:relative tw:min-h-[85vh] tw:max-h-[85vh] tw:h-full tw:w-full"
<div class="chart-container relative min-h-[85vh] max-h-[85vh] h-full w-full"
id="sankeyContainer"
_="init call setupSankeyChart() end">
<canvas id="sankeyChart"></canvas>

View File

@@ -5,42 +5,42 @@
{% block title %}{% translate 'Insights' %}{% endblock %}
{% block content %}
<div class="tw:container-fluid">
<div class="tw:flex tw:flex-wrap tw:my-3 tw:h-full">
<div class="tw:w-full tw:lg:w-2/12 tw:md:w-3/12 tw:mb-3 tw:md:mb-0">
<div class="tw:sticky tw:top-3">
<div class="container-fluid">
<div class="flex flex-wrap my-3 h-full">
<div class="w-full lg:w-2/12 md:w-3/12 mb-3 md:mb-0">
<div class="sticky top-3">
<div class="">
<div class="tw:mb-2 tw:w-full tw:lg:inline-flex tw:grid tw:gap-2 tw:flex-wrap tw:lg:justify-center" role="group"
<div class="mb-2 w-full lg:inline-flex grid gap-2 flex-wrap lg:justify-center" role="group"
_="on change
set type to event.target.value
add .tw:hidden to <#picker-form > div:not(.tw\\:hidden)/>
add .hidden to <#picker-form > div:not(.tw\\:hidden)/>
if type == 'month'
remove .tw:hidden from #month-form
remove .hidden from #month-form
end
if type == 'year'
remove .tw:hidden from #year-form
remove .hidden from #year-form
end
if type == 'month-range'
remove .tw:hidden from #month-range-form
remove .hidden from #month-range-form
end
if type == 'year-range'
remove .tw:hidden from #year-range-form
remove .hidden from #year-range-form
end
if type == 'date-range'
remove .tw:hidden from #date-range-form
remove .hidden from #date-range-form
end
then trigger updated"
id="picker-type">
<input type="radio" class="tw:join-item tw:btn tw:btn-sm tw:btn-outline tw:btn-primary tw:flex-grow" name="type" value="month" id="monthradio" autocomplete="off" aria-label="{% translate 'Month' %}" checked>
<input type="radio" class="join-item btn btn-sm btn-outline btn-primary flex-grow" name="type" value="month" id="monthradio" autocomplete="off" aria-label="{% translate 'Month' %}" checked>
<input type="radio" class="tw:join-item tw:btn tw:btn-sm tw:btn-outline tw:btn-primary tw:flex-grow" name="type" value="year" id="yearradio" autocomplete="off" aria-label="{% translate 'Year' %}">
<input type="radio" class="join-item btn btn-sm btn-outline btn-primary flex-grow" name="type" value="year" id="yearradio" autocomplete="off" aria-label="{% translate 'Year' %}">
<input type="radio" class="tw:join-item tw:btn tw:btn-sm tw:btn-outline tw:btn-primary tw:flex-grow" name="type" value="month-range" id="monthrangeradio" autocomplete="off" aria-label="{% translate 'Month Range' %}">
<input type="radio" class="join-item btn btn-sm btn-outline btn-primary flex-grow" name="type" value="month-range" id="monthrangeradio" autocomplete="off" aria-label="{% translate 'Month Range' %}">
<input type="radio" class="tw:join-item tw:btn tw:btn-sm tw:btn-outline tw:btn-primary tw:flex-grow" name="type" value="year-range" id="yearrangeradio" autocomplete="off" aria-label="{% translate 'Year Range' %}">
<input type="radio" class="join-item btn btn-sm btn-outline btn-primary flex-grow" name="type" value="year-range" id="yearrangeradio" autocomplete="off" aria-label="{% translate 'Year Range' %}">
<input type="radio" class="tw:join-item tw:btn tw:btn-sm tw:btn-outline tw:btn-primary tw:flex-grow" name="type" value="date-range" id="daterangeradio" autocomplete="off" aria-label="{% translate 'Date Range' %}">
<input type="radio" class="join-item btn btn-sm btn-outline btn-primary flex-grow" name="type" value="date-range" id="daterangeradio" autocomplete="off" aria-label="{% translate 'Date Range' %}">
</div>
<form id="picker-form"
_="install init_datepicker
@@ -48,63 +48,63 @@
<div id="month-form" class="">
{% crispy month_form %}
</div>
<div id="year-form" class="tw:hidden">
<div id="year-form" class="hidden">
{% crispy year_form %}
</div>
<div id="month-range-form" class="tw:hidden">
<div id="month-range-form" class="hidden">
{% crispy month_range_form %}
</div>
<div id="year-range-form" class="tw:hidden">
<div id="year-range-form" class="hidden">
{% crispy year_range_form %}
</div>
<div id="date-range-form" class="tw:hidden">
<div id="date-range-form" class="hidden">
{% crispy date_range_form %}
</div>
</form>
</div>
<div class="tw:menu tw:menu-vertical tw:flex-col" id="v-pills-tab" role="tablist"
<div class="menu menu-vertical flex-col" id="v-pills-tab" role="tablist"
aria-orientation="vertical">
<button class="tw:btn tw:btn-ghost tw:justify-start" id="v-pills-tab" data-bs-toggle="pill" data-bs-target="#v-pills-content"
<button class="btn btn-ghost justify-start" id="v-pills-tab" data-bs-toggle="pill" data-bs-target="#v-pills-content"
type="button" role="tab" aria-controls="v-pills-content" aria-selected="false"
hx-get="{% url 'insights_sankey_by_account' %}" hx-include="#picker-form, #picker-type"
hx-indicator="#tab-content"
hx-target="#tab-content">{% trans 'Account Flow' %}
</button>
<button class="tw:btn tw:btn-ghost tw:justify-start" id="v-pills-tab" data-bs-toggle="pill" data-bs-target="#v-pills-content"
<button class="btn btn-ghost justify-start" id="v-pills-tab" data-bs-toggle="pill" data-bs-target="#v-pills-content"
type="button" role="tab" aria-controls="v-pills-content" aria-selected="false"
hx-get="{% url 'insights_sankey_by_currency' %}"
hx-include="#picker-form, #picker-type"
hx-indicator="#tab-content"
hx-target="#tab-content">{% trans 'Currency Flow' %}
</button>
<button class="tw:btn tw:btn-ghost tw:justify-start" id="v-pills-tab" data-bs-toggle="pill" data-bs-target="#v-pills-content"
<button class="btn btn-ghost justify-start" id="v-pills-tab" data-bs-toggle="pill" data-bs-target="#v-pills-content"
type="button" role="tab" aria-controls="v-pills-content" aria-selected="false"
hx-get="{% url 'category_explorer_index' %}"
hx-include="#picker-form, #picker-type"
hx-indicator="#tab-content"
hx-target="#tab-content">{% trans 'Category Explorer' %}
</button>
<button class="tw:btn tw:btn-ghost tw:justify-start" id="v-pills-tab" data-bs-toggle="pill" data-bs-target="#v-pills-content"
<button class="btn btn-ghost justify-start" id="v-pills-tab" data-bs-toggle="pill" data-bs-target="#v-pills-content"
type="button" role="tab" aria-controls="v-pills-content" aria-selected="false"
hx-get="{% url 'category_overview' %}"
hx-include="#picker-form, #picker-type"
hx-indicator="#tab-content"
hx-target="#tab-content">{% trans 'Categories Overview' %}
</button>
<hr class="tw:border-base-300">
<button class="tw:btn tw:btn-ghost tw:justify-start" id="v-pills-tab" data-bs-toggle="pill" data-bs-target="#v-pills-content"
<hr class="hr">
<button class="btn btn-ghost justify-start" id="v-pills-tab" data-bs-toggle="pill" data-bs-target="#v-pills-content"
type="button" role="tab" aria-controls="v-pills-content" aria-selected="false"
hx-get="{% url 'insights_late_transactions' %}"
hx-indicator="#tab-content"
hx-target="#tab-content">{% trans 'Late Transactions' %}
</button>
<button class="tw:btn tw:btn-ghost tw:justify-start" id="v-pills-tab" data-bs-toggle="pill" data-bs-target="#v-pills-content"
<button class="btn btn-ghost justify-start" id="v-pills-tab" data-bs-toggle="pill" data-bs-target="#v-pills-content"
type="button" role="tab" aria-controls="v-pills-content" aria-selected="false"
hx-get="{% url 'insights_latest_transactions' %}"
hx-indicator="#tab-content"
hx-target="#tab-content">{% trans 'Latest Transactions' %}
</button>
<button class="tw:btn tw:btn-ghost tw:justify-start" id="v-pills-tab" data-bs-toggle="pill" data-bs-target="#v-pills-content"
<button class="btn btn-ghost justify-start" id="v-pills-tab" data-bs-toggle="pill" data-bs-target="#v-pills-content"
type="button" role="tab" aria-controls="v-pills-content" aria-selected="false"
hx-get="{% url 'insights_emergency_fund' %}"
hx-indicator="#tab-content"
@@ -113,7 +113,7 @@
</div>
</div>
</div>
<div class="tw:w-full tw:md:w-9/12 tw:lg:w-10/12">
<div class="w-full md:w-9/12 lg:w-10/12">
<div id="tab-content" class="show-loading"></div>
</div>
</div>