feat(invoices): let the invoice list read by row tone without the status column (#2353)
* feat(invoices): let the invoice list read by row tone without the status column A user asked for green/yellow/red row tints in the fakturalista (#2215) so the list can be read without the last column. The need is real; the tint is not the answer the design system already gives: status colours are data, not chrome (convention 12), and DECISIONS 2026-08-03 already ruled out row tints for this list. Settled rows (paid, cancelled, credited) now recede to muted text as a whole row. Open rows keep the foreground and the overdue chip stays the one marker on its row, so three row classes read at a glance with zero new colour: grey is done, dark is waiting for money, dark with an ochre chip is late. Works in dark mode and the brand themes for free and never stacks with the hover or selection tints. The tone lives in a pure lib helper (invoiceRowTone, tested) and one Record in the page maps tone to class; a row tint or a left-edge bar is a three-string swap there if the founder prefers colour. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvMaHcTnwAfxzgYD1fGYX1 * docs(decisions): record the first-principles choices of the 2026-09-06 evening issue batch Ten lines for #2215 #2351 #2218 #2220 #2312 #2184 #2293 #2239 #2331 #2332 (PRs #2353 to #2362), carried by this PR so the other nine branches do not all touch DECISIONS.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
Jakob Wennberg
parent
bf7773d74b
commit
0016732232
@@ -0,0 +1,43 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { invoiceRowTone, type InvoiceRowToneInput } from '@/lib/invoices/invoice-list-row-tone'
|
||||
|
||||
function row(status: InvoiceRowToneInput['status'], extra: Partial<InvoiceRowToneInput> = {}): InvoiceRowToneInput {
|
||||
return { status, credited_invoice_id: null, document_type: 'invoice', ...extra }
|
||||
}
|
||||
|
||||
describe('invoiceRowTone', () => {
|
||||
it('settled: paid, cancelled and credited rows recede regardless of document type', () => {
|
||||
expect(invoiceRowTone(row('paid'))).toBe('settled')
|
||||
expect(invoiceRowTone(row('cancelled'))).toBe('settled')
|
||||
expect(invoiceRowTone(row('credited'))).toBe('settled')
|
||||
expect(invoiceRowTone(row('paid', { credited_invoice_id: 'inv-1' }))).toBe('settled')
|
||||
expect(invoiceRowTone(row('cancelled', { document_type: 'quote' }))).toBe('settled')
|
||||
})
|
||||
|
||||
it('open: a sent or partially paid customer invoice is waiting for money', () => {
|
||||
expect(invoiceRowTone(row('sent'))).toBe('open')
|
||||
expect(invoiceRowTone(row('partially_paid'))).toBe('open')
|
||||
})
|
||||
|
||||
it('overdue: an overdue customer invoice', () => {
|
||||
expect(invoiceRowTone(row('overdue'))).toBe('overdue')
|
||||
})
|
||||
|
||||
it('none: drafts and non-invoice documents never read as receivables', () => {
|
||||
expect(invoiceRowTone(row('draft'))).toBe('none')
|
||||
expect(invoiceRowTone(row('sent', { document_type: 'proforma' }))).toBe('none')
|
||||
expect(invoiceRowTone(row('sent', { document_type: 'quote' }))).toBe('none')
|
||||
expect(invoiceRowTone(row('sent', { document_type: 'delivery_note' }))).toBe('none')
|
||||
expect(invoiceRowTone(row('overdue', { document_type: 'proforma' }))).toBe('none')
|
||||
})
|
||||
|
||||
it('none: an unpaid credit note is not a receivable even when sent or overdue', () => {
|
||||
expect(invoiceRowTone(row('sent', { credited_invoice_id: 'inv-1' }))).toBe('none')
|
||||
expect(invoiceRowTone(row('overdue', { credited_invoice_id: 'inv-1' }))).toBe('none')
|
||||
})
|
||||
|
||||
it('treats a missing or null document_type as a customer invoice, like the list page', () => {
|
||||
expect(invoiceRowTone({ status: 'sent', credited_invoice_id: null })).toBe('open')
|
||||
expect(invoiceRowTone({ status: 'overdue', credited_invoice_id: null, document_type: null })).toBe('overdue')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { Invoice } from '@/types'
|
||||
|
||||
/**
|
||||
* Row tone for the customer invoice list (#2215): the list must read without
|
||||
* the status column. Four tones, three of which the eye separates at a glance:
|
||||
*
|
||||
* - settled: the row is done (paid, cancelled, credited) and recedes
|
||||
* - open: a customer invoice waiting for money (sent, partially paid)
|
||||
* - overdue: an open customer invoice past its due date
|
||||
* - none: everything else (drafts, quotes, proformas, delivery notes, unpaid
|
||||
* credit notes); the status column already marks those with a chip
|
||||
*
|
||||
* The guards mirror matchesListTab in the list page: only a real customer
|
||||
* invoice (document_type 'invoice', not a credit note) counts as open or
|
||||
* overdue, so a sent proforma or quote never reads as a receivable.
|
||||
*/
|
||||
export type InvoiceRowTone = 'settled' | 'open' | 'overdue' | 'none'
|
||||
|
||||
export type InvoiceRowToneInput = Pick<Invoice, 'status' | 'credited_invoice_id'> & {
|
||||
/** Older list rows can lack the column; the list treats a missing value as 'invoice'. */
|
||||
document_type?: Invoice['document_type'] | null
|
||||
}
|
||||
|
||||
const SETTLED_STATUSES: ReadonlySet<Invoice['status']> = new Set(['paid', 'cancelled', 'credited'])
|
||||
|
||||
export function invoiceRowTone(invoice: InvoiceRowToneInput): InvoiceRowTone {
|
||||
if (SETTLED_STATUSES.has(invoice.status)) return 'settled'
|
||||
const isCustomerInvoice =
|
||||
(invoice.document_type || 'invoice') === 'invoice' && !invoice.credited_invoice_id
|
||||
if (!isCustomerInvoice) return 'none'
|
||||
if (invoice.status === 'overdue') return 'overdue'
|
||||
if (invoice.status === 'sent' || invoice.status === 'partially_paid') return 'open'
|
||||
return 'none'
|
||||
}
|
||||
Reference in New Issue
Block a user