@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.
Files changed (40) hide show
  1. package/README.md +1 -0
  2. package/npm-shrinkwrap.json +4 -3
  3. package/package.json +5 -3
  4. package/server/auth/people.js +40 -2
  5. package/server/auth/routes.js +11 -0
  6. package/server/auth/session.js +22 -3
  7. package/server/core/custom-fields.js +4 -3
  8. package/server/core/invoice-rules.js +8 -3
  9. package/server/core/job-match.js +6 -2
  10. package/server/core/sending.js +4 -3
  11. package/server/integrations/sentry/index.js +9 -1
  12. package/server/release-channel.js +5 -2
  13. package/server/ui/app-page.js +1 -1
  14. package/server/ui/app.js +46 -162
  15. package/server/ui/automations.js +9 -3
  16. package/server/ui/columns.js +268 -0
  17. package/server/ui/custom-fields.js +17 -16
  18. package/server/ui/flow.css +268 -55
  19. package/server/ui/invoice-notes.js +7 -5
  20. package/server/ui/invoice-rules.js +3 -3
  21. package/server/ui/invoicing-settings.js +92 -35
  22. package/server/ui/invoicing.js +206 -119
  23. package/server/ui/job-board.js +20 -8
  24. package/server/ui/job-change.js +36 -0
  25. package/server/ui/job-view.js +255 -60
  26. package/server/ui/jobs.js +197 -145
  27. package/server/ui/kit.js +278 -0
  28. package/server/ui/list-views.js +73 -0
  29. package/server/ui/monitoring.js +1 -0
  30. package/server/ui/person.js +29 -0
  31. package/server/ui/price-list.js +2 -3
  32. package/server/ui/price-reconcile.js +7 -8
  33. package/server/ui/reconcile.js +114 -61
  34. package/server/ui/routes.js +1 -1
  35. package/server/ui/saved-views.js +141 -0
  36. package/server/ui/search.js +8 -0
  37. package/server/ui/sidecar.js +72 -0
  38. package/server/ui/tip.js +99 -0
  39. package/server/ui/view-bar.js +176 -70
  40. package/server/ui/xero-import.js +7 -10
@@ -1,15 +1,15 @@
1
- // Settings, Invoicing (/invoicing-settings): payment terms, the draft
2
- // automation, and the lab's own rules a ready job passes before Flow drafts
3
- // its invoice (invoice-rules.js, docs/INVOICE_RULES.md). Invoicing needs no integration: how an invoice
4
- // goes to Xero is on the Xero integration page (sendModeSection).
5
- // Draft when ready is an automation (server/core/automations.js); the
6
- // Seazona integration page holds the rule that marks a job ready.
7
- // One row for each setting: its name, one short line, and its control. A
8
- // change saves at once. Everyone can read them; an owner or admin changes
9
- // them. The full rules are in docs/INVOICING_RULES.md. app.js passes in its
10
- // shared pieces as `ui`.
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
- export async function renderInvoicingSettings(main, ui) {
24
- const { h, flow, toast, notice, pageHeader, me } = ui;
25
- const host = h('div', { class: 'fx-card', style: { padding: '8px 20px 16px' } }, h('div', { class: 'fx-skeleton' }));
26
- main.append(pageHeader('Invoice rules', { subtitle: 'The rules Flow uses to draft each invoice.' }), host);
27
- let body;
28
- try {
29
- body = await flow('/api/invoicing/settings');
30
- } catch (error) {
31
- host.replaceChildren(notice(error.message, 'danger'));
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
- let saved = body.settings;
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
- const section = (title, ...rows) => h('section', { class: 'fx-set' }, h('h2', { class: 'fx-set__title' }, title), ...rows);
66
- host.replaceChildren(...[
67
- disabled ? notice('Only an owner or admin can change these.', 'info') : null,
68
- section('Drafts',
69
- row('Payment terms', 'Days from invoice to due date.', days('paymentTermsDays', 'Payment terms in days')),
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: '{practitioner} - {patient}' }))),
72
- lineMappingSection(ui),
73
- automationSection(ui, 'invoicing', 'Automations'),
74
- rulesSection(ui),
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' }, h('h2', { class: 'fx-set__title' }, 'Sending'),
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 {