@arcforgelabs/flow 2026.10.46 → 2026.10.48

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 (63) hide show
  1. package/README.md +1 -0
  2. package/VISION.md +10 -10
  3. package/npm-shrinkwrap.json +2 -2
  4. package/package.json +1 -1
  5. package/server/api/invoicing.js +89 -22
  6. package/server/auth/people.js +3 -1
  7. package/server/auth/routes.js +4 -1
  8. package/server/core/contacts.js +160 -0
  9. package/server/core/directory.js +4 -9
  10. package/server/core/invoice-rules.js +34 -4
  11. package/server/core/invoices.js +93 -1
  12. package/server/core/invoicing-rules.js +20 -3
  13. package/server/core/job-board.js +1 -1
  14. package/server/core/sending.js +141 -25
  15. package/server/db/doctor.js +1 -1
  16. package/server/db/index.js +19 -0
  17. package/server/db/migrations/0021_flow_schema.sql +68 -0
  18. package/server/db/migrations/0022_contacts.sql +11 -0
  19. package/server/db/migrations/meta/0021_snapshot.json +4377 -0
  20. package/server/db/migrations/meta/0022_snapshot.json +4446 -0
  21. package/server/db/migrations/meta/_journal.json +14 -0
  22. package/server/db/schema.js +15 -0
  23. package/server/integrations/port.js +2 -0
  24. package/server/integrations/schema.js +3 -2
  25. package/server/integrations/sentry/index.js +14 -7
  26. package/server/integrations/setup.js +1 -0
  27. package/server/integrations/xero/catalogue.js +82 -14
  28. package/server/integrations/xero/client.js +37 -0
  29. package/server/integrations/xero/create-contact.js +126 -0
  30. package/server/integrations/xero/delete-invoice.js +104 -0
  31. package/server/integrations/xero/drafts.js +19 -8
  32. package/server/integrations/xero/index.js +9 -0
  33. package/server/integrations/xero/migrations/0011_xero_invoice_deleted.sql +11 -0
  34. package/server/integrations/xero/migrations/0012_xero_contact_details.sql +16 -0
  35. package/server/integrations/xero/migrations/meta/_journal.json +14 -0
  36. package/server/integrations/xero/routes.js +21 -0
  37. package/server/integrations/xero/settings.js +3 -0
  38. package/server/ui/alerts.js +2 -1
  39. package/server/ui/app.js +113 -28
  40. package/server/ui/bulk-bar.js +18 -5
  41. package/server/ui/columns.js +5 -2
  42. package/server/ui/contacts.js +260 -0
  43. package/server/ui/custom-fields.js +22 -7
  44. package/server/ui/flow.css +275 -24
  45. package/server/ui/invoicing.js +374 -70
  46. package/server/ui/job-board.js +10 -0
  47. package/server/ui/job-change.js +4 -3
  48. package/server/ui/job-view.js +3 -1
  49. package/server/ui/jobs.js +150 -19
  50. package/server/ui/kit.js +220 -45
  51. package/server/ui/list-views.js +50 -10
  52. package/server/ui/mobile.js +729 -0
  53. package/server/ui/monitoring.js +51 -0
  54. package/server/ui/nav-pins.js +44 -0
  55. package/server/ui/pager.js +57 -10
  56. package/server/ui/price-list.js +1 -1
  57. package/server/ui/price-reconcile.js +12 -12
  58. package/server/ui/reconcile.js +18 -17
  59. package/server/ui/routes.js +1 -1
  60. package/server/ui/saved-views.js +2 -1
  61. package/server/ui/sidecar.js +106 -6
  62. package/server/ui/view-bar.js +22 -11
  63. package/server/ui/xero-import.js +48 -19
@@ -10,6 +10,16 @@
10
10
  import { bulkBar } from '/assets/ui/bulk-bar.js';
11
11
  import { lucide } from '/assets/ui/custom-fields.js';
12
12
  import { formatCents } from '/assets/ui/invoicing.js';
13
+ import { applyPhoneRule, assignPhoneSlots, phoneSlot } from './mobile.js';
14
+
15
+ // The list rows use the same phone slots as a column list.
16
+ const phoneAt = (defs) => {
17
+ const columns = assignPhoneSlots(defs);
18
+ return (index) => {
19
+ const slot = phoneSlot(columns[index]);
20
+ return slot ? { 'data-phone-slot': slot } : {};
21
+ };
22
+ };
13
23
 
14
24
  const XERO_STATUS = { DRAFT: 'Draft', SUBMITTED: 'Awaiting approval', AUTHORISED: 'Approved', PAID: 'Paid' };
15
25
  const XERO_TONE = { DRAFT: 'neutral', SUBMITTED: 'warn', AUTHORISED: 'info', PAID: 'success' };
@@ -212,13 +222,23 @@ export async function renderXeroImport(main, ui) {
212
222
  drawBar();
213
223
  } });
214
224
  const sure = rows.filter((r) => r.sure);
225
+ const at = phoneAt([
226
+ { kind: 'text', label: 'Number' },
227
+ { kind: 'client', label: 'Client', phone: 'title' },
228
+ { kind: 'date', label: 'Date', phone: 'meta' },
229
+ { kind: 'total', num: true, label: 'Total' },
230
+ { kind: 'status', label: 'Match', phone: 'meta' },
231
+ { kind: 'text', label: 'Flow', phone: 'meta' },
232
+ { kind: 'date', label: 'Flow date', phone: 'hide' },
233
+ { kind: 'total', label: 'Flow total', phone: 'hide' },
234
+ ]);
215
235
  host.replaceChildren(
216
236
  h('div', { class: 'fx-xi__bulk' }, icon('list-checks'), h('span', { class: 'fx-xi__bulk-text' }, 'Tick rows, then use the bar below.'),
217
237
  canAct && sure.length ? h('button', { class: 'fx-btn fx-btn--sm', type: 'button', onclick: () => {
218
238
  for (const r of sure) { ticked.add(r.xeroInvoiceId); boxes.get(r.xeroInvoiceId).checked = true; }
219
239
  drawBar();
220
240
  } }, icon('check-check'), `Tick ${sure.length} sure`) : null),
221
- h('div', { class: 'fx-xi__scroll' }, h('table', { class: 'fx-table fx-table--rows fx-xi-list' },
241
+ applyPhoneRule(h('div', { class: 'fx-xi__scroll' }, h('table', { class: 'fx-table fx-table--rows fx-xi-list' },
222
242
  h('thead', {}, h('tr', {},
223
243
  canAct ? h('th', { class: 'fx-tick-cell' }, all) : null,
224
244
  h('th', { colspan: '4' }, xeroMark()), h('th', { class: 'fx-xi-list__mid' }), h('th', { colspan: '3' }, flowMark()))),
@@ -231,15 +251,15 @@ export async function renderXeroImport(main, ui) {
231
251
  boxes.set(row.xeroInvoiceId, box);
232
252
  return h('tr', {},
233
253
  canAct ? h('td', { class: 'fx-tick-cell' }, box) : null,
234
- h('td', { class: 'fx-num' }, row.number ?? '—'),
235
- h('td', {}, row.clientName ?? '—'),
236
- h('td', { class: 'fx-dim' }, shortDate(row.issuedOn)),
237
- h('td', { class: 'fx-num fx-strong' }, money(row.totalCents)),
238
- h('td', { class: 'fx-xi-list__mid' }, row.sure ? icon('check-check', 'Sure') : fitIcon(fit), fit.count > 1 ? h('span', { class: 'fx-dim' }, `${fit.count}`) : null),
239
- h('td', { class: 'fx-num' }, fit.name),
240
- h('td', { class: 'fx-dim' }, shortDate(fit.date)),
241
- h('td', { class: 'fx-num fx-strong' }, money(fit.totalCents)));
242
- })))));
254
+ h('td', { class: 'fx-num', ...at(0) }, row.number ?? '—'),
255
+ h('td', { ...at(1) }, row.clientName ?? '—'),
256
+ h('td', { class: 'fx-dim', ...at(2) }, shortDate(row.issuedOn)),
257
+ h('td', { class: 'fx-num fx-strong', ...at(3) }, money(row.totalCents)),
258
+ h('td', { class: 'fx-xi-list__mid', ...at(4) }, row.sure ? icon('check-check', 'Sure') : fitIcon(fit), fit.count > 1 ? h('span', { class: 'fx-dim' }, `${fit.count}`) : null),
259
+ h('td', { class: 'fx-num', ...at(5) }, fit.name),
260
+ h('td', { class: 'fx-dim', ...at(6) }, shortDate(fit.date)),
261
+ h('td', { class: 'fx-num fx-strong', ...at(7) }, money(fit.totalCents)));
262
+ })))), 'columnTable'));
243
263
  drawBar();
244
264
  }
245
265
 
@@ -259,13 +279,22 @@ export async function renderXeroImport(main, ui) {
259
279
  } });
260
280
  const more = h('div', {});
261
281
  const STATE = { linked: ['check', 'In Flow'], invoice_fits: ['receipt', 'A Flow invoice fits'], job_fits: ['briefcase', 'A job fits'], none: ['circle-dashed', 'Not in Flow'] };
282
+ const at = phoneAt([
283
+ { kind: 'status', label: 'State', phone: 'meta' },
284
+ { kind: 'text', label: 'Number' },
285
+ { kind: 'client', label: 'Client', phone: 'title' },
286
+ { kind: 'date', label: 'Date', phone: 'meta' },
287
+ { kind: 'total', num: true, label: 'Total' },
288
+ { kind: 'status', label: 'Xero', phone: 'meta' },
289
+ { kind: 'text', label: 'Flow', phone: 'hide' },
290
+ ]);
262
291
  host.replaceChildren(
263
292
  h('div', { class: 'fx-xi__bulk' }, icon('search'), search),
264
- h('div', { class: 'fx-xi__scroll' }, h('table', { class: 'fx-table fx-table--rows fx-xi-list' },
293
+ applyPhoneRule(h('div', { class: 'fx-xi__scroll' }, h('table', { class: 'fx-table fx-table--rows fx-xi-list' },
265
294
  h('thead', {}, h('tr', {},
266
295
  canAct ? h('th', { class: 'fx-tick-cell' }, all) : null,
267
296
  h('th', {}, ''), h('th', {}, 'Number'), h('th', {}, 'Client'), h('th', {}, 'Date'), h('th', { class: 'fx-num' }, 'Total'), h('th', {}, xeroMark()), h('th', {}, flowMark()))),
268
- body)),
297
+ body)), 'columnTable'),
269
298
  more);
270
299
  let timer = null;
271
300
  search.addEventListener('input', () => {
@@ -295,13 +324,13 @@ export async function renderXeroImport(main, ui) {
295
324
  } });
296
325
  return h('tr', { 'data-state': row.state },
297
326
  canAct ? h('td', { class: 'fx-tick-cell' }, box) : null,
298
- h('td', { class: 'fx-xi-list__mid' }, row.state === 'linked' || row.state === 'none' ? icon(glyph, label) : h('a', { href: '/xero-import', onclick: (event) => ui.go(event), 'aria-label': label, title: label }, icon(glyph))),
299
- h('td', { class: 'fx-num' }, row.number ?? '—'),
300
- h('td', {}, row.clientName ?? '—'),
301
- h('td', { class: 'fx-dim' }, shortDate(row.issuedOn)),
302
- h('td', { class: 'fx-num fx-strong' }, money(row.totalCents)),
303
- h('td', {}, pill(row)),
304
- h('td', { class: 'fx-num' }, row.state === 'linked' ? go(`/invoices/${row.link.invoiceId}`, row.link.number ?? 'Draft') : ''));
327
+ h('td', { class: 'fx-xi-list__mid', ...at(0) }, row.state === 'linked' || row.state === 'none' ? icon(glyph, label) : h('a', { href: '/xero-import', onclick: (event) => ui.go(event), 'aria-label': label, title: label }, icon(glyph))),
328
+ h('td', { class: 'fx-num', ...at(1) }, row.number ?? '—'),
329
+ h('td', { ...at(2) }, row.clientName ?? '—'),
330
+ h('td', { class: 'fx-dim', ...at(3) }, shortDate(row.issuedOn)),
331
+ h('td', { class: 'fx-num fx-strong', ...at(4) }, money(row.totalCents)),
332
+ h('td', { ...at(5) }, pill(row)),
333
+ h('td', { class: 'fx-num', ...at(6) }, row.state === 'linked' ? go(`/invoices/${row.link.invoiceId}`, row.link.number ?? 'Draft') : ''));
305
334
  };
306
335
  async function load() {
307
336
  let page;