@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.
- package/VISION.md +10 -10
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/server/api/invoicing.js +56 -16
- package/server/auth/people.js +3 -1
- package/server/auth/routes.js +4 -1
- package/server/core/invoice-rules.js +34 -4
- package/server/core/invoices.js +40 -1
- package/server/core/invoicing-rules.js +20 -3
- package/server/core/job-board.js +1 -1
- package/server/core/sending.js +76 -22
- package/server/integrations/sentry/index.js +14 -7
- package/server/ui/alerts.js +2 -1
- package/server/ui/app.js +105 -25
- package/server/ui/bulk-bar.js +18 -5
- package/server/ui/columns.js +5 -2
- package/server/ui/custom-fields.js +22 -7
- package/server/ui/flow.css +235 -24
- package/server/ui/invoicing.js +254 -55
- package/server/ui/job-board.js +10 -0
- package/server/ui/job-change.js +4 -3
- package/server/ui/job-view.js +3 -1
- package/server/ui/jobs.js +150 -19
- package/server/ui/kit.js +198 -43
- package/server/ui/list-views.js +50 -10
- package/server/ui/mobile.js +726 -0
- package/server/ui/monitoring.js +51 -0
- package/server/ui/nav-pins.js +41 -0
- package/server/ui/pager.js +57 -10
- package/server/ui/price-list.js +1 -1
- package/server/ui/price-reconcile.js +12 -12
- package/server/ui/reconcile.js +18 -17
- package/server/ui/routes.js +1 -1
- package/server/ui/saved-views.js +2 -1
- package/server/ui/sidecar.js +106 -6
- package/server/ui/view-bar.js +22 -11
- package/server/ui/xero-import.js +48 -19
package/server/ui/list-views.js
CHANGED
|
@@ -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 {
|
|
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)
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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
|