@arcforgelabs/flow 2026.10.42 → 2026.10.44
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/README.md +1 -0
- package/npm-shrinkwrap.json +4 -3
- package/package.json +5 -3
- package/server/auth/people.js +40 -2
- package/server/auth/routes.js +11 -0
- package/server/auth/session.js +22 -3
- package/server/core/custom-fields.js +4 -3
- package/server/core/invoice-rules.js +8 -3
- package/server/core/job-match.js +6 -2
- package/server/core/sending.js +4 -3
- package/server/integrations/sentry/index.js +9 -1
- package/server/release-channel.js +5 -2
- package/server/ui/app-page.js +1 -1
- package/server/ui/app.js +46 -162
- package/server/ui/automations.js +9 -3
- package/server/ui/columns.js +268 -0
- package/server/ui/custom-fields.js +17 -16
- package/server/ui/flow.css +268 -55
- package/server/ui/invoice-notes.js +7 -5
- package/server/ui/invoice-rules.js +3 -3
- package/server/ui/invoicing-settings.js +92 -35
- package/server/ui/invoicing.js +206 -119
- package/server/ui/job-board.js +20 -8
- package/server/ui/job-change.js +36 -0
- package/server/ui/job-view.js +255 -60
- package/server/ui/jobs.js +197 -145
- package/server/ui/kit.js +278 -0
- package/server/ui/list-views.js +73 -0
- package/server/ui/monitoring.js +1 -0
- package/server/ui/person.js +29 -0
- package/server/ui/price-list.js +2 -3
- package/server/ui/price-reconcile.js +7 -8
- package/server/ui/reconcile.js +114 -61
- package/server/ui/routes.js +1 -1
- package/server/ui/saved-views.js +141 -0
- package/server/ui/search.js +8 -0
- package/server/ui/sidecar.js +72 -0
- package/server/ui/tip.js +99 -0
- package/server/ui/view-bar.js +176 -70
- package/server/ui/xero-import.js +7 -10
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
// Settings,
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
1
|
+
// Settings, Rules (/invoicing-settings): a section of cards for each part,
|
|
2
|
+
// like Claude's Customize page, and one page for each
|
|
3
|
+
// (/invoicing-settings/<key>).
|
|
4
|
+
// Drafts holds payment terms and the Reference; Price codes the lines with no
|
|
5
|
+
// price item (line-mappings.js); Invoices the checks a ready job passes
|
|
6
|
+
// before Flow drafts its invoice (invoice-rules.js, docs/INVOICE_RULES.md).
|
|
7
|
+
// Automations have their own page (automations.js).
|
|
8
|
+
// How an invoice goes to Xero is on the Xero integration page
|
|
9
|
+
// (sendModeSection). A change saves at once. Everyone can read them; an owner
|
|
10
|
+
// or admin changes them. The full rules are in docs/INVOICING_RULES.md.
|
|
11
|
+
// app.js passes in its shared pieces as `ui`.
|
|
11
12
|
|
|
12
|
-
import { automationSection } from '/assets/ui/automations.js';
|
|
13
13
|
import { rulesSection } from '/assets/ui/invoice-rules.js';
|
|
14
14
|
import { lineMappingSection } from '/assets/ui/line-mappings.js';
|
|
15
15
|
|
|
@@ -20,19 +20,73 @@ const SEND_MODES = [
|
|
|
20
20
|
{ value: 'approve_and_email', label: 'Approved and emailed' },
|
|
21
21
|
];
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
// The parts of Rules, each on its own page (/invoicing-settings/<key>).
|
|
24
|
+
const PARTS = [
|
|
25
|
+
{ key: 'invoices', title: 'Invoices', mount: (ui) => rulesSection(ui, 'Invoices') },
|
|
26
|
+
{ key: 'drafts', title: 'Drafts', mount: (ui) => draftsSection(ui) },
|
|
27
|
+
{ key: 'price-codes', title: 'Price codes', mount: (ui) => lineMappingSection(ui) },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// What a rule does, as its card's mark: Pending, Waits, Never drafted.
|
|
31
|
+
const KIND = {
|
|
32
|
+
pending: { mark: 'flag' },
|
|
33
|
+
wait: { mark: 'clock' },
|
|
34
|
+
skip: { mark: 'ban' },
|
|
35
|
+
};
|
|
36
|
+
const SHOWN = 6;
|
|
37
|
+
|
|
38
|
+
export function renderInvoicingSettings(main, ui, id) {
|
|
39
|
+
const { h, flow, notice, pageHeader, tileCard, tileSection, crumbs } = ui;
|
|
40
|
+
if (id) {
|
|
41
|
+
const part = PARTS.find((p) => p.key === id);
|
|
42
|
+
main.append(crumbs(['/invoicing-settings', 'Rules'], part?.title ?? 'Not found'),
|
|
43
|
+
part ? h('div', { class: 'fx-card', style: { padding: '8px 20px 16px' } }, part.mount(ui)) : notice('No such part of Rules.', 'danger'));
|
|
32
44
|
return;
|
|
33
45
|
}
|
|
46
|
+
// The landing is Claude's Customize page: a section for each part, one-line
|
|
47
|
+
// cards, and "Show all" to the part's own page, a list.
|
|
48
|
+
const host = h('div', { style: { display: 'grid', gap: '20px' } }, h('div', { class: 'fx-skeleton' }));
|
|
49
|
+
main.append(pageHeader('Rules'), host);
|
|
50
|
+
const page = (key) => `/invoicing-settings/${key}`;
|
|
51
|
+
(async () => {
|
|
52
|
+
let settings;
|
|
53
|
+
let labels;
|
|
54
|
+
let rules;
|
|
55
|
+
try {
|
|
56
|
+
[{ settings }, { labels }, { rules }] = await Promise.all([
|
|
57
|
+
flow('/api/invoicing/settings'), flow('/api/invoicing/line-labels'), flow('/api/invoicing/rules')]);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
host.replaceChildren(notice(error.message, 'danger'));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const ruleCard = (rule) => tileCard({
|
|
63
|
+
row: true, href: page('invoices'), mark: (KIND[rule.action.kind] ?? { mark: 'list-checks' }).mark, title: rule.name,
|
|
64
|
+
meta: rule.enabled ? 'On' : 'Off',
|
|
65
|
+
});
|
|
66
|
+
const waiting = labels.filter((l) => l.unmapped > 0).length;
|
|
67
|
+
host.replaceChildren(
|
|
68
|
+
tileSection('Invoices', rules.length ? rules.slice(0, SHOWN).map(ruleCard) : [h('p', { class: 'fx-dim' }, 'No rules yet.')],
|
|
69
|
+
{ href: page('invoices'), more: rules.length > SHOWN ? `Show all ${rules.length}` : 'Show all' }),
|
|
70
|
+
tileSection('Drafts', [
|
|
71
|
+
tileCard({ row: true, href: page('drafts'), mark: 'calendar-clock', title: 'Payment terms', meta: `${settings.paymentTermsDays} days` }),
|
|
72
|
+
tileCard({ row: true, href: page('drafts'), mark: 'text-cursor-input', title: 'Reference' }),
|
|
73
|
+
], { href: page('drafts') }),
|
|
74
|
+
tileSection('Price codes', [
|
|
75
|
+
tileCard({
|
|
76
|
+
row: true, href: page('price-codes'), mark: 'tag', title: 'Line labels',
|
|
77
|
+
status: waiting ? { label: `${waiting} wait`, tone: 'warn' } : { label: 'All mapped', tone: 'success' },
|
|
78
|
+
}),
|
|
79
|
+
], { href: page('price-codes') }),
|
|
80
|
+
);
|
|
81
|
+
})();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Payment terms and the Reference template.
|
|
85
|
+
function draftsSection(ui) {
|
|
86
|
+
const { h, flow, toast, notice, me } = ui;
|
|
34
87
|
const disabled = !['owner', 'admin'].includes(me?.person.role);
|
|
35
|
-
|
|
88
|
+
const section = h('section', { class: 'fx-set' }, h('h2', { class: 'fx-set__title' }, 'Drafts'), h('div', { class: 'fx-skeleton' }));
|
|
89
|
+
let saved;
|
|
36
90
|
|
|
37
91
|
async function save(changes, control) {
|
|
38
92
|
control.disabled = true;
|
|
@@ -60,19 +114,23 @@ export async function renderInvoicingSettings(main, ui) {
|
|
|
60
114
|
});
|
|
61
115
|
return input;
|
|
62
116
|
};
|
|
63
|
-
const days = (key, label) => box(key, label, { type: 'number', read: Number });
|
|
64
117
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
118
|
+
(async () => {
|
|
119
|
+
try {
|
|
120
|
+
saved = (await flow('/api/invoicing/settings')).settings;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
section.replaceChildren(h('h2', { class: 'fx-set__title' }, 'Drafts'), notice(error.message, 'danger'));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
section.replaceChildren(...[
|
|
126
|
+
h('h2', { class: 'fx-set__title' }, 'Drafts'),
|
|
127
|
+
disabled ? notice('Only an owner or admin can change these.', 'info') : null,
|
|
128
|
+
row('Payment terms', 'Days from invoice to due date.', box('paymentTermsDays', 'Payment terms in days', { type: 'number', read: Number })),
|
|
70
129
|
row('Reference', 'The Reference on each invoice. Use {practitioner} and {patient}.',
|
|
71
|
-
box('referenceTemplate', 'Reference rule', { placeholder: '
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
].filter(Boolean));
|
|
130
|
+
box('referenceTemplate', 'Reference rule', { placeholder: 'Type a reference rule' })),
|
|
131
|
+
].filter(Boolean));
|
|
132
|
+
})();
|
|
133
|
+
return section;
|
|
76
134
|
}
|
|
77
135
|
|
|
78
136
|
// The Xero integration page's "Sending" section: how a checked invoice goes to
|
|
@@ -82,11 +140,10 @@ export function sendModeSection(ui) {
|
|
|
82
140
|
const disabled = !['owner', 'admin'].includes(me?.person.role);
|
|
83
141
|
const select = h('select', { class: 'fx-select', 'aria-label': 'Send to Xero as', disabled },
|
|
84
142
|
SEND_MODES.map((m) => h('option', { value: m.value }, m.label)));
|
|
85
|
-
const section = h('section', { class: 'fx-set' },
|
|
143
|
+
const section = h('section', { class: 'fx-set' }, ui.sectionTitle('Sending', { href: DOCS, label: 'How sending works' }),
|
|
86
144
|
h('div', { class: 'fx-set__row' },
|
|
87
145
|
h('div', { class: 'fx-set__text' }, h('span', { class: 'fx-set__name' }, 'Send to Xero as'), h('span', { class: 'fx-set__hint' }, 'Draft: approve in Xero. Approved: Xero emails it.')),
|
|
88
|
-
h('div', { class: 'fx-set__control' }, select))
|
|
89
|
-
h('p', { class: 'fx-dim', style: { margin: '8px 0 0' } }, h('a', { href: DOCS, target: '_blank', rel: 'noopener' }, 'How sending works')));
|
|
146
|
+
h('div', { class: 'fx-set__control' }, select)));
|
|
90
147
|
let saved = null;
|
|
91
148
|
(async () => {
|
|
92
149
|
try {
|