feat: Tabellen per Spaltenkopf sortierbar (A11-T4)
Neues static/sort.js sortiert Tabellen clientseitig per Klick auf den Spaltenkopf (⇅/▲/▼): Datum TT.MM.JJJJ, deutsche Zahlen/Beträge, sonst Text (Intl.Collator 'de'). Sortierwert optional per td[data-sort] (Punkt-Dezimal/ISO) überschreibbar, th[data-nosort] bekommt keinen Button, table[data-sort-mode="server"] wird übersprungen (Buchungen sortiert serverseitig, folgt in Task 5). Bearbeiten-/Tilgungsplan- Zeilen (tr[data-sort-follow]) wandern mit ihrer Vorzeile mit. Initialisierung über htmx.onLoad, greift auch bei nachgeladenen Fragmenten. Templates entsprechend annotiert (Planung, Szenarien, Admin, Index, Salden, Import-Liste, Buchungen); Salden-Gesamtzeile dafür in ein tfoot verschoben. test_gui.py::test_gui_zeigt_deutsche_betragsformate angepasst, da das neue machinenlesbare data-sort-Attribut bewusst Punkt-Dezimal führt (Interface-Konvention) - die Prüfung auf deutsches Format im sichtbaren Text bleibt bestehen. JS-Unit-/DOM-Tests unter tests/js (node --test, DOM-Teil optional via jsdom), pytest-Wrapper skip ohne node (Fable-Gate führt sie per Podman aus). Suite: 248 passed, 3 skipped (vorher 242/2). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
'use strict';
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const S = require('../../app/static/sort.js');
|
||||
|
||||
const items = (vals, ds) => vals.map((v, i) => ({ key: S.sortKey(v, !!ds), index: i, label: v }));
|
||||
const labels = (arr) => arr.map((i) => i.label);
|
||||
|
||||
test('Datum TT.MM.JJJJ', () => {
|
||||
const k = S.sortKey('05.03.2026', false);
|
||||
assert.equal(k.kind, 'date');
|
||||
assert.equal(k.value, 20260305);
|
||||
});
|
||||
|
||||
test('deutsche Beträge und Zahlen', () => {
|
||||
assert.equal(S.sortKey('-1.234,56 €', false).value, -1234.56);
|
||||
assert.equal(S.sortKey('5,00 %', false).value, 5);
|
||||
assert.equal(S.sortKey('15', false).kind, 'number');
|
||||
assert.equal(S.sortKey('1.234', false).value, 1234);
|
||||
});
|
||||
|
||||
test('data-sort: Punkt-Dezimal und ISO-Datum', () => {
|
||||
assert.equal(S.sortKey('-1234.56', true).value, -1234.56);
|
||||
assert.equal(S.sortKey('2026-06-30', true).kind, 'text');
|
||||
});
|
||||
|
||||
test('Leerwerte sind null', () => {
|
||||
assert.equal(S.sortKey('', false), null);
|
||||
assert.equal(S.sortKey(' ', false), null);
|
||||
assert.equal(S.sortKey('–', false), null);
|
||||
assert.equal(S.sortKey(undefined, false), null);
|
||||
});
|
||||
|
||||
test('Zahlen numerisch statt alphabetisch', () => {
|
||||
assert.deepEqual(labels(S.sortItems(items(['10,00', '9,00', '-100,00']), 'asc')),
|
||||
['-100,00', '9,00', '10,00']);
|
||||
assert.deepEqual(labels(S.sortItems(items(['10,00', '9,00', '-100,00']), 'desc')),
|
||||
['10,00', '9,00', '-100,00']);
|
||||
});
|
||||
|
||||
test('Datum chronologisch', () => {
|
||||
assert.deepEqual(labels(S.sortItems(items(['01.02.2026', '15.01.2026', '31.12.2025']), 'asc')),
|
||||
['31.12.2025', '15.01.2026', '01.02.2026']);
|
||||
});
|
||||
|
||||
test('Leerwerte immer am Ende, beide Richtungen', () => {
|
||||
const v = ['01.02.2026', '–', '15.01.2026', ''];
|
||||
assert.deepEqual(labels(S.sortItems(items(v), 'asc')), ['15.01.2026', '01.02.2026', '–', '']);
|
||||
assert.deepEqual(labels(S.sortItems(items(v), 'desc')), ['01.02.2026', '15.01.2026', '–', '']);
|
||||
});
|
||||
|
||||
test('gemischte Typen -> Text', () => {
|
||||
assert.equal(S.columnKind([S.sortKey('10,00'), S.sortKey('abc'), null]), 'text');
|
||||
assert.equal(S.columnKind([S.sortKey('10,00'), null]), 'number');
|
||||
});
|
||||
|
||||
test('Text deutsch (Umlaute, Groß/klein)', () => {
|
||||
assert.deepEqual(labels(S.sortItems(items(['Zahnarzt', 'Ärztin', 'miete']), 'asc')),
|
||||
['Ärztin', 'miete', 'Zahnarzt']);
|
||||
});
|
||||
|
||||
test('stabil bei gleichen Werten', () => {
|
||||
const it = [{ key: S.sortKey('5'), index: 0, label: 'a' }, { key: S.sortKey('5'), index: 1, label: 'b' }];
|
||||
assert.deepEqual(labels(S.sortItems(it, 'desc')), ['a', 'b']);
|
||||
});
|
||||
|
||||
test('groupRows: Folgezeilen hängen an der Vorzeile', () => {
|
||||
const rows = [{ id: 'a' }, { id: 'a-edit', f: true }, { id: 'a-plan', f: true },
|
||||
{ id: 'b' }, { id: 'b-edit', f: true }];
|
||||
assert.deepEqual(S.groupRows(rows, (r) => !!r.f).map((g) => g.map((r) => r.id)),
|
||||
[['a', 'a-edit', 'a-plan'], ['b', 'b-edit']]);
|
||||
});
|
||||
Reference in New Issue
Block a user