.
========================================================================= #}
{% macro sortable_th(label, col, current_sort, current_dir, base_qs,
target=None, indicator=None, align='left', extra_class='') %}
{# Normalize current_dir: anything other than 'asc' / 'desc' (None, '',
garbage from a tampered URL) is treated as "no direction supplied", which
means the column is NOT considered active even if current_sort == col.
This avoids rendering aria-sort="ascending" + an up-arrow when the caller
passed (col, None) — a common shape on the very first request. #}
{%- set active_dir = current_dir if current_dir in ('asc', 'desc') else none -%}
{%- set is_active = (current_sort == col and active_dir is not none) -%}
{%- set next_dir = 'asc' if (is_active and active_dir == 'desc') else 'desc' -%}
{%- set arrow = '\u25bc' if (is_active and active_dir == 'desc')
else ('\u25b2' if (is_active and active_dir == 'asc') else '') -%}
{%- set aria = 'descending' if (is_active and active_dir == 'desc')
else ('ascending' if (is_active and active_dir == 'asc') else 'none') -%}
{%- set sep = '&' if base_qs else '' -%}
{%- set href = '?' ~ base_qs ~ sep ~ 'sort=' ~ col ~ '&dir=' ~ next_dir -%}
{# Tailwind's static class scanner cannot detect dynamically-composed classes
like `text-{{ align }}`; if any of text-left/center/right is otherwise
unused in the consumer's templates, it would be purged from the build.
Materialize the literal class name in a static lookup so all 3 variants
are statically present in the rendered template source. #}
{%- set align_class = {
'left': 'text-left',
'center': 'text-center',
'right': 'text-right',
}.get(align, 'text-left') -%}
{% endmacro %}
{# -------------------------------------------------------------------------
strip_sort_params
Helper: take request.url.query and return it with sort/dir params removed.
Use the result as base_qs= for sortable_th / pivot_sort_th. Keeps every
other param (date range, filters, zoom, group_by, ...) intact across
sort clicks.
By default strips 'sort' and 'dir'. For pages with multiple independent
sortable tables (e.g. dayparts with Product x Daypart + Daypart x Category),
pass custom param names to strip only one table's params and leave the
other table's state intact. Main table uses sort/dir; secondary uses
sort2/dir2. For the main table's headers, base_qs preserves sort2/dir2
but drops sort/dir; for the secondary table, the inverse.
Usage (single-table default, as a Jinja snippet in a template):
set base_qs = strip_sort_params(request.url.query)
sortable_th("Revenue", "revenue", sort, dir, base_qs, target="#x", align="right")
Usage (two-pivot page):
set base_qs = strip_sort_params(request.url.query)
set base_qs_2 = strip_sort_params(request.url.query,
sort_param='sort2', dir_param='dir2')
------------------------------------------------------------------------- #}
{% macro strip_sort_params(qs, sort_param='sort', dir_param='dir') -%}
{%- set parts = [] -%}
{%- set to_strip = (sort_param, dir_param) -%}
{%- for chunk in qs.split('&') -%}
{# Match the *key* not the prefix: "sortx=1" must survive, and a bare
"sort" / "dir" (no '=') must be rejected. Split on first '=' to extract
the key, then compare. #}
{%- set key = chunk.split('=', 1)[0] -%}
{%- if chunk and key not in to_strip -%}
{%- set _ = parts.append(chunk) -%}
{%- endif -%}
{%- endfor -%}
{{ parts | join('&') }}
{%- endmacro %}
{# =========================================================================
pivot_sort_th
Variant of sortable_th for pivot/heatmap tables whose column set is
derived at runtime from the data (e.g. hour buckets, payment-type IDs,
branch names, daypart slugs). The sort key cannot be checked against a
static allowlist the way sortable_th's caller does — the route must
validate the key inline against the current column set instead.
Same URL contract as sortable_th (?sort=
&dir=), same
toggle behaviour, same ARIA, same progressive enhancement via target=.
Two additional parameters (sort_param, dir_param) let multiple pivot
tables share one page without param collision. A page with two pivots
uses sort_param='sort' / dir_param='dir' for the primary table and
sort_param='sort2' / dir_param='dir2' for the secondary.
Parameters
----------
label, col, current_sort, current_dir, base_qs, target, indicator,
align, extra_class: same as sortable_th.
sort_param : str = 'sort' — URL param name this th writes.
dir_param : str = 'dir' — direction param name.
IMPORTANT: the caller's base_qs MUST already have *this table's* sort/dir
stripped. If the page has two pivot tables, pass distinct base_qs values
(see strip_sort_params's multi-param form) so switching one table's sort
preserves the other's state.
========================================================================= #}
{% macro pivot_sort_th(label, col, current_sort, current_dir, base_qs,
target=None, indicator=None, align='center', extra_class='',
sort_param='sort', dir_param='dir') %}
{%- set active_dir = current_dir if current_dir in ('asc', 'desc') else none -%}
{%- set is_active = (current_sort == col and active_dir is not none) -%}
{%- set next_dir = 'asc' if (is_active and active_dir == 'desc') else 'desc' -%}
{%- set arrow = '\u25bc' if (is_active and active_dir == 'desc')
else ('\u25b2' if (is_active and active_dir == 'asc') else '') -%}
{%- set aria = 'descending' if (is_active and active_dir == 'desc')
else ('ascending' if (is_active and active_dir == 'asc') else 'none') -%}
{%- set sep = '&' if base_qs else '' -%}
{%- set href = '?' ~ base_qs ~ sep ~ sort_param ~ '=' ~ col ~ '&' ~ dir_param ~ '=' ~ next_dir -%}
{%- set align_class = {
'left': 'text-left',
'center': 'text-center',
'right': 'text-right',
}.get(align, 'text-center') -%}
{% endmacro %}
{# =========================================================================
sort_select
Dropdown-based sort control for card-grid pages (e.g. /reports/branches
in Grind) that have no column headers to click. Renders two