@arcforgelabs/flow 2026.10.46 → 2026.10.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,34 +3,74 @@
3
3
  // Filter, Columns), and one can be the person's default, which opens when
4
4
  // they go to the page. No DOM: the server checks a saved list with the same
5
5
  // rules the page uses (server/auth/people.js).
6
- import { cleanFilterView, filterJobs, normaliseJobsView } from './job-view.js';
6
+ import { caseNumber } from './job-board.js';
7
+ import { cleanFilterView, fieldIdOf, filterJobs, KIND_OF_TYPE, normaliseJobsView } from './job-view.js';
7
8
 
8
- // The Invoices filter's fields (invoicing.js). The tabs are the status, so
9
- // Status is not here. The page shows the chip (view-bar.js); the server keeps
10
- // the rows (GET /api/invoicing/invoices), so a filter reaches every invoice
11
- // of a tab.
9
+ // The Invoices filter's fields (invoicing.js): the list's own columns, then
10
+ // the job columns the Columns menu offers (Case, Practitioner, Shipped).
11
+ // Custom fields ('field:<id>') are fields too; their kind comes from their
12
+ // type. The page shows the chip (view-bar.js); the server keeps the rows
13
+ // (GET /api/invoicing/invoices), so a filter reaches every invoice of a tab.
12
14
  export const INVOICE_FILTER_FIELDS = {
13
15
  client: { label: 'Client', kind: 'choice', icon: 'building-2' },
14
16
  number: { label: 'Number', kind: 'text', icon: 'hash' },
15
17
  reference: { label: 'Reference', kind: 'text', icon: 'text' },
16
18
  date: { label: 'Date', kind: 'date', icon: 'calendar' },
19
+ due: { label: 'Due', kind: 'date', icon: 'calendar' },
17
20
  total: { label: 'Total', kind: 'number', icon: 'dollar-sign' },
21
+ status: { label: 'Status', kind: 'choice', icon: 'circle-dot' },
22
+ linked: { label: 'Linked', kind: 'choice', icon: 'link' },
18
23
  problem: { label: 'Problem', kind: 'check', icon: 'triangle-alert' },
24
+ case: { label: 'Case', kind: 'text', icon: 'briefcase' },
25
+ practitioner: { label: 'Practitioner', kind: 'choice', icon: 'stethoscope' },
26
+ shipped: { label: 'Shipped', kind: 'date', icon: 'calendar' },
19
27
  };
20
28
 
29
+ // What the Status column shows. `sending` replaces the tab while an invoice
30
+ // is on its way to the accounting system.
31
+ export const INVOICE_FILTER_STATUSES = Object.freeze([
32
+ { value: 'pending', label: 'Pending' },
33
+ { value: 'ready', label: 'Ready' },
34
+ { value: 'draft', label: 'Draft' },
35
+ { value: 'approved', label: 'Approved' },
36
+ { value: 'sent', label: 'Sent' },
37
+ { value: 'void', label: 'Void' },
38
+ { value: 'sending', label: 'Sending' },
39
+ ]);
40
+
21
41
  export const invoiceFilterKind = (field) => INVOICE_FILTER_FIELDS[field]?.kind ?? null;
22
42
 
43
+ // A field's kind for this lab: a built-in field, or a custom field's type.
44
+ // `fields` is the lab's custom fields ({ id, type }).
45
+ export function invoiceKindOf(fields) {
46
+ const type = new Map((fields ?? []).map((f) => [f.id, f.type]));
47
+ return (field) => INVOICE_FILTER_FIELDS[field]?.kind ?? KIND_OF_TYPE[type.get(fieldIdOf(field) ?? '')] ?? null;
48
+ }
49
+
23
50
  // An invoice's value for a filter field or a sort, as they compare it. A
24
- // draft has no issue date: it uses the day it was made.
51
+ // draft has no issue date: it uses the day it was made. A job column reads
52
+ // the invoice's job; a custom field reads that job's value (`fields`).
25
53
  export function invoiceValue(invoice, field) {
54
+ const job = invoice.job ?? null;
26
55
  switch (field) {
27
56
  case 'client': return invoice.contactName ?? null;
28
57
  case 'number': return invoice.number ?? null;
29
58
  case 'reference': return invoice.reference ?? null;
30
59
  case 'date': return invoice.issueDate ?? (invoice.createdAt ? new Date(invoice.createdAt).toISOString().slice(0, 10) : null);
60
+ case 'due': return invoice.dueDate ?? null;
31
61
  case 'total': return invoice.totalCents == null ? null : invoice.totalCents / 100;
62
+ case 'status': return invoice.sending ? 'sending' : invoice.tab ?? null;
63
+ case 'linked': return job?.sourceSystem ?? null;
32
64
  case 'problem': return (invoice.problems?.length ?? 0) > 0;
33
- default: return null;
65
+ case 'case': return job ? caseNumber(job) : null;
66
+ case 'practitioner': return job?.practitioner ?? null;
67
+ case 'shipped': return job?.shippedOn ?? null;
68
+ default: {
69
+ const id = fieldIdOf(field);
70
+ if (!id) return null;
71
+ const value = invoice.fields?.[id];
72
+ return value === undefined ? null : value;
73
+ }
34
74
  }
35
75
  }
36
76
 
@@ -41,8 +81,8 @@ export const INVOICE_SORTS = Object.freeze(['number', 'client', 'reference', 'da
41
81
  // them (an empty value last, either way), then `offset` and `limit` cut the
42
82
  // page. `total` and `totalCents` are of every kept row. The rows come in
43
83
  // newest first; that order breaks ties.
44
- export function invoicePage(invoices, { view, sort = null, dir = 'asc', offset = 0, limit = 100, today }) {
45
- const kept = filterJobs(invoices, view, { valueOf: invoiceValue, kindOf: invoiceFilterKind, today });
84
+ export function invoicePage(invoices, { view, sort = null, dir = 'asc', offset = 0, limit = 100, today, kindOf = invoiceFilterKind, me = null }) {
85
+ const kept = filterJobs(invoices, view, { valueOf: invoiceValue, kindOf, late: null, me, today });
46
86
  const sign = dir === 'desc' ? -1 : 1;
47
87
  const ordered = INVOICE_SORTS.includes(sort)
48
88
  ? kept.map((invoice, i) => [invoice, invoiceValue(invoice, sort), i]).sort(([, x, i], [, y, j]) => {
@@ -75,7 +115,7 @@ const CUSTOM_FIELD = /^field:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-
75
115
  const kindIn = (fields, custom) => (field) => (Object.hasOwn(fields, field) ? fields[field].kind : custom && CUSTOM_FIELD.test(field) ? '*' : null);
76
116
 
77
117
  // The Invoices and Reconcile views: their filter.
78
- export const normaliseInvoicesView = (value) => cleanFilterView(value, kindIn(INVOICE_FILTER_FIELDS, false));
118
+ export const normaliseInvoicesView = (value) => cleanFilterView(value, kindIn(INVOICE_FILTER_FIELDS, true));
79
119
  export const normaliseReconcileView = (value) => cleanFilterView(value, kindIn(RECONCILE_FILTER_FIELDS, true));
80
120
 
81
121
  // The pages with saved views. Each is named in a switch, never looked up