304b50b5eb
* feat(invoices): grouped sections and a grouping picker on the customer invoice list Default view groups rows into Utkast / Väntar på betalning / Betalda och avslutade sections; a toolbar picker switches grouping to customer, month, or none, written back to the URL as ?group= so views stay shareable. Column sorting applies within each section and cycles asc / desc / default so an applied sort can be released; paging and the detail pager follow the rendered group order. Both message catalogs carry the new strings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: group by customer_id, not display name (review) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): address review: shared bucketing helper, flat default, no #2093 revert - keep CHECKBOX_REVEAL_CLASS and useRangeSelect from main: the PR had re-inlined the old hidden-until-hover classes, reverting #2093, and the same region now carries #2117's shift-click range selection - default the grouping picker to 'none': sections on the most-used list page are a design change, so grouping stays an explicit choice - extract the ~90 lines of bucketing into lib/lists/group-rows.ts, a pure helper with vitest coverage that both list pages now render from - derive statusGroupOf from matchesListTab so the sections and the tabs cannot drift apart - replace the banned em dash placeholders with an UNKNOWN_GROUP_KEY sentinel and a translated 'Saknas' label - section headers carry data-no-stagger so they skip the row animation Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(invoices): flat is the URL-less default; drop the sort cycle (review) Picking Status stripped the group param while the initialiser falls back to the flat list, so the choice was lost on reload; now only 'none' owns the URL-less state. The header sort returns to main's two-state toggle (DECISIONS 2026-08-11). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XUxGhBBwQSMu59bWq6Vrf --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
72 lines
2.5 KiB
TypeScript
72 lines
2.5 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { groupRows, ungrouped } from '../group-rows'
|
|
|
|
interface Row {
|
|
id: string
|
|
customerId: string | null
|
|
customerName: string
|
|
month: string
|
|
}
|
|
|
|
const rows: Row[] = [
|
|
{ id: 'a', customerId: 'c1', customerName: 'Ådalen AB', month: '2026-08' },
|
|
{ id: 'b', customerId: 'c2', customerName: 'Bolag AB', month: '2026-09' },
|
|
{ id: 'c', customerId: 'c1', customerName: 'Ådalen AB', month: '2026-09' },
|
|
{ id: 'd', customerId: 'c3', customerName: 'Ådalen AB', month: '2026-08' },
|
|
]
|
|
|
|
const byCustomer = {
|
|
keyOf: (row: Row) => ({ key: row.customerId ?? row.customerName, label: row.customerName }),
|
|
order: (a: { label: string }, b: { label: string }) => a.label.localeCompare(b.label, 'sv'),
|
|
}
|
|
|
|
describe('ungrouped', () => {
|
|
it('keeps the order and marks every row keyless', () => {
|
|
const result = ungrouped(rows)
|
|
expect(result.rows.map((r) => r.row.id)).toEqual(['a', 'b', 'c', 'd'])
|
|
expect(result.rows.every((r) => r.groupKey === null)).toBe(true)
|
|
expect(result.meta.size).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe('groupRows', () => {
|
|
it('buckets by key and preserves the incoming order inside each bucket', () => {
|
|
const { rows: flat, meta } = groupRows(rows, byCustomer)
|
|
|
|
// Sections ordered by label (sv collation puts Å last), rows keep a-c / b / d order.
|
|
expect(flat.map((r) => r.row.id)).toEqual(['b', 'a', 'c', 'd'])
|
|
expect([...meta.keys()]).toEqual(['c2', 'c1', 'c3'])
|
|
expect(meta.get('c1')).toEqual({ label: 'Ådalen AB', count: 2 })
|
|
})
|
|
|
|
it('separates two customers sharing a display name', () => {
|
|
const { meta } = groupRows(rows, byCustomer)
|
|
expect(meta.get('c1')?.label).toBe('Ådalen AB')
|
|
expect(meta.get('c3')?.label).toBe('Ådalen AB')
|
|
expect(meta.get('c1')?.count).toBe(2)
|
|
expect(meta.get('c3')?.count).toBe(1)
|
|
})
|
|
|
|
it('follows a fixed order and drops keys it does not list', () => {
|
|
const { rows: flat, meta } = groupRows(rows, {
|
|
keyOf: (row) => ({ key: row.month, label: row.month }),
|
|
order: ['2026-09', '2026-07'],
|
|
})
|
|
expect([...meta.keys()]).toEqual(['2026-09'])
|
|
expect(flat.map((r) => r.row.id)).toEqual(['b', 'c'])
|
|
})
|
|
|
|
it('every flattened row carries the key of its section', () => {
|
|
const { rows: flat } = groupRows(rows, byCustomer)
|
|
for (const entry of flat) {
|
|
expect(entry.groupKey).toBe(entry.row.customerId)
|
|
}
|
|
})
|
|
|
|
it('handles an empty list', () => {
|
|
const { rows: flat, meta } = groupRows([], byCustomer)
|
|
expect(flat).toEqual([])
|
|
expect(meta.size).toBe(0)
|
|
})
|
|
})
|