Merge pull request #659 from eitchtee/fix/linked-transfer-exchanged-values

feat(transactions): redesign combined transfer items + show exchanged amounts for both sides
This commit is contained in:
Herculino Trotta authored and GitHub committed 2026-11-09 23:40:42 -03:00
commit 674870641c
2 files changed
+74 -65

No files matched your search

@@ -0,0 +1,50 @@
{% load i18n %}
{# One side of a combined transfer, laid out like a mini transaction: account + metadata | amount #}
{# Only the divider between legs gets the extra space; the outer edges stay flush #}
<div class="relative flex items-start gap-x-3 py-3.5 first:pt-0 last:pb-0">
{# Timeline rail: x sits under the transfer icon's center (7.5px into the column, minus the container's ps-5 = 20px). #}
{# The first leg's rail reaches up past the description's mb + container mt to the icon; the last one stops at its icon, making └ #}
<span aria-hidden="true" class="absolute -left-[13px] w-px bg-base-content/20
{% if first %}-top-[20px] lg:-top-[16px] bottom-0{% else %}top-0 h-[24px]{% endif %}"></span>
{# Stub from the rail into this leg's icon, at the icon's vertical center #}
<span aria-hidden="true" class="absolute -left-[13px] w-[13px] h-px bg-base-content/20
{% if first %}top-[10px]{% else %}top-[24px]{% endif %}"></span>
{# h-5 matches the account line's height so the icon centers on it #}
<div class="h-5 flex items-center" title="{{ label }}">
<i class="fa-solid {{ icon }} fa-fw fa-xs {% if color == 'red' %}text-expense{% else %}text-income{% endif %}"></i>
</div>
{# Like a regular item: amount drops below on small screens, bottom-right on lg #}
<div class="flex-1 min-w-0 flex flex-wrap lg:flex-nowrap gap-x-3 gap-y-2 lg:gap-y-1">
<div class="w-full lg:flex-1 min-w-0">
<div class="text-base-content truncate">{% if leg.account.group %}{{ leg.account.group.name }} • {% endif %}{{ leg.account.name }}</div>
{# Same rhythm as a regular item's metadata rows: mb-2 lg:mb-1 between rows, pe-1 + mr-1 after the icon #}
<div class="flex flex-col gap-2 lg:gap-1 mt-1 text-xs text-base-content/60">
{# h-4 = one text-xs line, so each icon centers on its row's first line #}
{% if leg.category %}<div class="flex gap-2"><span class="h-4 flex items-center"><i class="fa-solid fa-icons fa-fw fa-xs"></i></span><span>{{ leg.category.name }}</span></div>{% endif %}
{% if leg.tags.all %}<div class="flex gap-2"><span class="h-4 flex items-center"><i class="fa-solid fa-hashtag fa-fw fa-xs"></i></span><span>{{ leg.tags.all|join:", " }}</span></div>{% endif %}
{% if leg.entities.all %}<div class="flex gap-2"><span class="h-4 flex items-center"><i class="fa-solid fa-user-group fa-fw fa-xs"></i></span><span>{{ leg.entities.all|join:", " }}</span></div>{% endif %}
</div>
</div>
{# Mobile gaps match a regular item's amount rows (mb-2 lg:mb-0) #}
<div class="w-full lg:w-auto lg:text-right lg:self-end lg:pe-1 shrink-0 flex flex-col gap-2 lg:gap-0">
<c-amount.display
:amount="leg.amount"
:prefix="leg.account.currency.prefix"
:suffix="leg.account.currency.suffix"
:decimal_places="leg.account.currency.decimal_places"
:color="color"></c-amount.display>
{% if not dummy %}
{% with exchanged=leg.exchanged_amount %}
{% if exchanged %}
<c-amount.display
:amount="exchanged.amount"
:prefix="exchanged.prefix"
:suffix="exchanged.suffix"
:decimal_places="exchanged.decimal_places"
color="grey"></c-amount.display>
{% endif %}
{% endwith %}
{% endif %}
</div>
</div>
</div>
+24 -65
View File
@@ -76,42 +76,22 @@
<div class="flex-1 ps-0">{{ overriden_entities|join:", " }}</div>
</div>
{% elif is_transfer %}
{% if transaction.entities.all or transfer_to.entities.all %}
<div class="grid grid-cols-[auto_1fr] mb-2 lg:mb-1 text-base-content/70">
<div class="w-auto pe-1"><i class="fa-solid fa-user-group fa-fw mr-1 fa-xs"></i></div>
<div>{{ transaction.entities.all|join:", "|default:"—" }}</div>
<div class="w-auto pe-1"><i class="fa-solid fa-arrow-right fa-fw mr-1 fa-xs text-base-content/40"></i></div>
<div>{{ transfer_to.entities.all|join:", "|default:"—" }}</div>
</div>
{% endif %}
{% comment %} If no override, fall back to transaction entities, but ONLY if the transaction has an ID. {% endcomment %}
{% elif transaction.id and transaction.entities.all %}
{% elif transaction.id and transaction.entities.all and not is_transfer %}
<div class="flex flex-wrap mb-2 lg:mb-1 text-base-content/70">
<div class="w-auto pe-1"><i class="fa-solid fa-user-group fa-fw mr-1 fa-xs"></i></div>
<div class="flex-1 ps-0">{{ transaction.entities.all|join:", " }}</div>
</div>
{% endif %}
{# Notes#}
{% if transaction.notes %}
{% if transaction.notes and not is_transfer %}
<div class="flex flex-wrap mb-2 lg:mb-1 text-base-content/70">
<div class="w-auto pe-1"><i class="fa-solid fa-align-left fa-fw mr-1 fa-xs"></i></div>
<div class="flex-1 ps-0">{{ transaction.notes | limited_markdown | linebreaksbr }}</div>
</div>
{% endif %}
{# Category#}
{% if is_transfer %}
{# Each side keeps its own category: "from" line, then "to" line with the arrow under the icon #}
{% if transaction.category or transfer_to.category %}
<div class="grid grid-cols-[auto_1fr] mb-2 lg:mb-1 text-base-content/70">
<div class="w-auto pe-1"><i class="fa-solid fa-icons fa-fw mr-1 fa-xs"></i></div>
<div>{{ transaction.category.name|default:"—" }}</div>
<div class="w-auto pe-1"><i class="fa-solid fa-arrow-right fa-fw mr-1 fa-xs text-base-content/40"></i></div>
<div>{{ transfer_to.category.name|default:"—" }}</div>
</div>
{% endif %}
{% elif transaction.category %}
{% if transaction.category and not is_transfer %}
<div class="flex flex-wrap mb-2 lg:mb-1 text-base-content/70">
<div class="w-auto pe-1"><i class="fa-solid fa-icons fa-fw mr-1 fa-xs"></i></div>
<div class="flex-1 ps-0">{{ transaction.category.name }}</div>
@@ -125,52 +105,33 @@
<div class="flex-1 ps-0">{{ overriden_tags|join:", " }}</div>
</div>
{% elif is_transfer %}
{% if transaction.tags.all or transfer_to.tags.all %}
<div class="grid grid-cols-[auto_1fr] mb-2 lg:mb-1 text-base-content/70">
<div class="w-auto pe-1"><i class="fa-solid fa-hashtag fa-fw mr-1 fa-xs"></i></div>
<div>{{ transaction.tags.all|join:", "|default:"—" }}</div>
<div class="w-auto pe-1"><i class="fa-solid fa-arrow-right fa-fw mr-1 fa-xs text-base-content/40"></i></div>
<div>{{ transfer_to.tags.all|join:", "|default:"—" }}</div>
</div>
{% endif %}
{% comment %} If no override, fall back to transaction tags, but ONLY if the transaction has an ID. {% endcomment %}
{% elif transaction.id and transaction.tags.all %}
{% elif transaction.id and transaction.tags.all and not is_transfer %}
<div class="flex flex-wrap mb-2 lg:mb-1 text-base-content/70">
<div class="w-auto pe-1"><i class="fa-solid fa-hashtag fa-fw mr-1 fa-xs"></i></div>
<div class="flex-1 ps-0">{{ transaction.tags.all|join:", " }}</div>
</div>
{% endif %}
</div>
{% if is_transfer %}
{# ps-5 leaves room for the timeline rail that drops from the transfer icon (see _transfer_leg) #}
<div class="main-amount mt-2 mb-1 ps-5 text-sm">
{% translate 'From' as from_label %}{% translate 'To' as to_label %}
{% include "cotton/transaction/_transfer_leg.html" with leg=transaction color="red" icon="fa-arrow-right-from-bracket" label=from_label first=True %}
{% include "cotton/transaction/_transfer_leg.html" with leg=transfer_to color="green" icon="fa-arrow-right-to-bracket" label=to_label first=False %}
</div>
{# Notes go below the legs so they don't sit in the rail's path #}
{% if transaction.notes %}
<div class="flex flex-wrap mt-2 text-sm text-base-content/70">
<div class="w-auto pe-1"><i class="fa-solid fa-align-left fa-fw mr-1 fa-xs"></i></div>
<div class="flex-1 ps-0">{{ transaction.notes | limited_markdown | linebreaksbr }}</div>
</div>
{% endif %}
{% endif %}
</div>
{% if not is_transfer %}
<div
class="lg:w-auto lg:min-w-0 lg:max-w-1/2 w-full lg:text-right self-end {% if transaction.account.is_untracked_by or transaction.category.mute or transaction.mute %}opacity-70{% endif %}">
{% if is_transfer %}
{# Each side: amount, with its account below, like a regular item #}
<div class="main-amount mt-3 lg:mt-0 mb-2 lg:mb-0 grid grid-cols-[auto_minmax(0,auto)] gap-x-2 gap-y-2 items-baseline w-fit max-w-full lg:ms-auto">
<span></span>
<div class="min-w-0">
<c-amount.display
:amount="transaction.amount"
:prefix="transaction.account.currency.prefix"
:suffix="transaction.account.currency.suffix"
:decimal_places="transaction.account.currency.decimal_places"
color="red"></c-amount.display>
<div class="truncate">{% if transaction.account.group %}{{ transaction.account.group.name }} • {% endif %}{{ transaction.account.name }}</div>
</div>
<i class="fa-solid fa-arrow-right fa-xs text-base-content/40"></i>
<div class="min-w-0">
<c-amount.display
:amount="transfer_to.amount"
:prefix="transfer_to.account.currency.prefix"
:suffix="transfer_to.account.currency.suffix"
:decimal_places="transfer_to.account.currency.decimal_places"
color="green"></c-amount.display>
<div class="truncate">{% if transfer_to.account.group %}{{ transfer_to.account.group.name }} • {% endif %}{{ transfer_to.account.name }}</div>
</div>
</div>
{% else %}
<div class="main-amount mb-2 lg:mb-0">
<c-amount.display
:amount="transaction.amount"
@@ -179,7 +140,6 @@
:decimal_places="transaction.account.currency.decimal_places"
color="{% if transaction.type == "EX" %}red{% else %}green{% endif %}"></c-amount.display>
</div>
{% endif %}
{# Exchange Rate#}
{% if not dummy %}
{% with exchanged=transaction.exchanged_amount %}
@@ -195,12 +155,11 @@
{% endif %}
{% endwith %}
{% endif %}
{% if not is_transfer %}
<div>
{% if transaction.account.group %}{{ transaction.account.group.name }} • {% endif %}{{ transaction.account.name }}
</div>
{% endif %}
<div>
{% if transaction.account.group %}{{ transaction.account.group.name }} • {% endif %}{{ transaction.account.name }}
</div>
</div>
{% endif %}
{% if not dummy %}
<div class="z-1000">
{# Item actions#}