@arcforgelabs/flow 2026.10.46 → 2026.10.48-1

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 (64) hide show
  1. package/README.md +1 -0
  2. package/VISION.md +10 -10
  3. package/npm-shrinkwrap.json +11 -11
  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/seazona/sweep.js +11 -8
  26. package/server/integrations/sentry/index.js +14 -7
  27. package/server/integrations/setup.js +1 -0
  28. package/server/integrations/xero/catalogue.js +82 -14
  29. package/server/integrations/xero/client.js +37 -0
  30. package/server/integrations/xero/create-contact.js +126 -0
  31. package/server/integrations/xero/delete-invoice.js +104 -0
  32. package/server/integrations/xero/drafts.js +19 -8
  33. package/server/integrations/xero/index.js +9 -0
  34. package/server/integrations/xero/migrations/0011_xero_invoice_deleted.sql +11 -0
  35. package/server/integrations/xero/migrations/0012_xero_contact_details.sql +16 -0
  36. package/server/integrations/xero/migrations/meta/_journal.json +14 -0
  37. package/server/integrations/xero/routes.js +21 -0
  38. package/server/integrations/xero/settings.js +3 -0
  39. package/server/ui/alerts.js +2 -1
  40. package/server/ui/app.js +113 -28
  41. package/server/ui/bulk-bar.js +18 -5
  42. package/server/ui/columns.js +5 -2
  43. package/server/ui/contacts.js +260 -0
  44. package/server/ui/custom-fields.js +22 -7
  45. package/server/ui/flow.css +275 -24
  46. package/server/ui/invoicing.js +374 -70
  47. package/server/ui/job-board.js +10 -0
  48. package/server/ui/job-change.js +4 -3
  49. package/server/ui/job-view.js +3 -1
  50. package/server/ui/jobs.js +150 -19
  51. package/server/ui/kit.js +220 -45
  52. package/server/ui/list-views.js +50 -10
  53. package/server/ui/mobile.js +729 -0
  54. package/server/ui/monitoring.js +51 -0
  55. package/server/ui/nav-pins.js +44 -0
  56. package/server/ui/pager.js +57 -10
  57. package/server/ui/price-list.js +1 -1
  58. package/server/ui/price-reconcile.js +12 -12
  59. package/server/ui/reconcile.js +18 -17
  60. package/server/ui/routes.js +1 -1
  61. package/server/ui/saved-views.js +2 -1
  62. package/server/ui/sidecar.js +106 -6
  63. package/server/ui/view-bar.js +22 -11
  64. package/server/ui/xero-import.js +48 -19
@@ -0,0 +1,729 @@
1
+ // Phone rules for every shared component. The source is each card's
2
+ // mobile.json (design-system/components/<Card>/mobile.json). This module is
3
+ // what the browser reads, so a component and its card cannot drift.
4
+ // Regenerate with: node scripts/design-system/mobile-rules.mjs
5
+ // docs/DESIGN_SYSTEM.md, docs/MOBILE_TESTS.md.
6
+
7
+ // The three widths (docs/MOBILE_TESTS.md): phone up to 639px, tablet
8
+ // 640–1023px, desk from 1024px.
9
+ export const WIDTHS = Object.freeze({
10
+ phone: '(max-width: 639px)',
11
+ tabletMin: '(min-width: 640px)',
12
+ tabletMax: '(max-width: 1023px)',
13
+ desk: '(min-width: 1024px)',
14
+ });
15
+
16
+ export const RULES = {
17
+ "bulkBar": {
18
+ "component": "bulkBar",
19
+ "direction": "foot bar",
20
+ "phone": {
21
+ "layout": "foot"
22
+ },
23
+ "tablet": {
24
+ "layout": "foot"
25
+ },
26
+ "desk": {
27
+ "layout": "foot"
28
+ },
29
+ "touch": 44,
30
+ "pageOverflow": "none",
31
+ "screens": [
32
+ "invoices-ticked"
33
+ ],
34
+ "card": "BulkBar"
35
+ },
36
+ "fx-btn": {
37
+ "component": "fx-btn",
38
+ "direction": "44px control",
39
+ "phone": {
40
+ "layout": "control"
41
+ },
42
+ "tablet": {
43
+ "layout": "control"
44
+ },
45
+ "desk": {
46
+ "layout": "control"
47
+ },
48
+ "touch": 44,
49
+ "pageOverflow": "none",
50
+ "screens": [
51
+ "jobs-board",
52
+ "jobs-list",
53
+ "job-panel",
54
+ "invoices-ready",
55
+ "invoices-pending",
56
+ "invoices-approved",
57
+ "invoices-ticked",
58
+ "invoices-filter",
59
+ "invoice",
60
+ "reconcile",
61
+ "price-list",
62
+ "rules",
63
+ "automations",
64
+ "integrations",
65
+ "custom-fields",
66
+ "appearance"
67
+ ],
68
+ "mark": false,
69
+ "card": "Button"
70
+ },
71
+ "columnTable": {
72
+ "component": "columnTable",
73
+ "direction": "two-line row",
74
+ "phone": {
75
+ "layout": "two-line",
76
+ "rowHeight": 56,
77
+ "slots": {
78
+ "title": 2,
79
+ "end": 1,
80
+ "meta": 3
81
+ },
82
+ "columnFlag": "phone",
83
+ "flags": [
84
+ "title",
85
+ "meta",
86
+ "end",
87
+ "hide"
88
+ ]
89
+ },
90
+ "tablet": {
91
+ "layout": "table",
92
+ "sticky": "first"
93
+ },
94
+ "desk": {
95
+ "layout": "table"
96
+ },
97
+ "touch": 44,
98
+ "pageOverflow": "none",
99
+ "screens": [
100
+ "jobs-list",
101
+ "invoices-ready",
102
+ "invoices-pending",
103
+ "invoices-approved",
104
+ "invoices-ticked",
105
+ "invoices-filter",
106
+ "reconcile",
107
+ "price-list"
108
+ ],
109
+ "minRowsAt375": 7,
110
+ "card": "Columns"
111
+ },
112
+ "crumbs": {
113
+ "component": "crumbs",
114
+ "direction": "trail",
115
+ "phone": {
116
+ "layout": "trail"
117
+ },
118
+ "tablet": {
119
+ "layout": "trail"
120
+ },
121
+ "desk": {
122
+ "layout": "trail"
123
+ },
124
+ "touch": 44,
125
+ "pageOverflow": "none",
126
+ "screens": [],
127
+ "card": "Crumbs"
128
+ },
129
+ "dialog": {
130
+ "component": "dialog",
131
+ "direction": "sheet",
132
+ "phone": {
133
+ "layout": "sheet"
134
+ },
135
+ "tablet": {
136
+ "layout": "dialog"
137
+ },
138
+ "desk": {
139
+ "layout": "dialog"
140
+ },
141
+ "touch": 44,
142
+ "pageOverflow": "none",
143
+ "screens": [],
144
+ "card": "Dialog"
145
+ },
146
+ "fieldError": {
147
+ "component": "fieldError",
148
+ "direction": "field",
149
+ "phone": {
150
+ "layout": "field"
151
+ },
152
+ "tablet": {
153
+ "layout": "field"
154
+ },
155
+ "desk": {
156
+ "layout": "field"
157
+ },
158
+ "touch": 44,
159
+ "pageOverflow": "none",
160
+ "screens": [],
161
+ "card": "FieldError"
162
+ },
163
+ "viewBar": {
164
+ "component": "viewBar",
165
+ "direction": "sheet",
166
+ "phone": {
167
+ "layout": "sheet"
168
+ },
169
+ "tablet": {
170
+ "layout": "panel"
171
+ },
172
+ "desk": {
173
+ "layout": "panel"
174
+ },
175
+ "touch": 44,
176
+ "pageOverflow": "none",
177
+ "screens": [
178
+ "jobs-board",
179
+ "jobs-list",
180
+ "job-panel",
181
+ "invoices-ready",
182
+ "invoices-pending",
183
+ "invoices-approved",
184
+ "invoices-ticked",
185
+ "invoices-filter",
186
+ "reconcile"
187
+ ],
188
+ "card": "Filter"
189
+ },
190
+ "findBox": {
191
+ "component": "findBox",
192
+ "direction": "icon",
193
+ "phone": {
194
+ "layout": "icon"
195
+ },
196
+ "tablet": {
197
+ "layout": "icon"
198
+ },
199
+ "desk": {
200
+ "layout": "icon"
201
+ },
202
+ "touch": 44,
203
+ "pageOverflow": "none",
204
+ "screens": [
205
+ "jobs-board",
206
+ "jobs-list",
207
+ "job-panel",
208
+ "invoices-ready",
209
+ "invoices-pending",
210
+ "invoices-approved",
211
+ "invoices-ticked",
212
+ "invoices-filter",
213
+ "reconcile"
214
+ ],
215
+ "card": "FindBox"
216
+ },
217
+ "holdButton": {
218
+ "component": "holdButton",
219
+ "direction": "wait",
220
+ "phone": {
221
+ "layout": "wait"
222
+ },
223
+ "tablet": {
224
+ "layout": "wait"
225
+ },
226
+ "desk": {
227
+ "layout": "wait"
228
+ },
229
+ "touch": 44,
230
+ "pageOverflow": "none",
231
+ "screens": [],
232
+ "mark": false,
233
+ "card": "HoldButton"
234
+ },
235
+ "lucide": {
236
+ "component": "lucide",
237
+ "direction": "icon",
238
+ "phone": {
239
+ "layout": "icon"
240
+ },
241
+ "tablet": {
242
+ "layout": "icon"
243
+ },
244
+ "desk": {
245
+ "layout": "icon"
246
+ },
247
+ "touch": 44,
248
+ "pageOverflow": "none",
249
+ "screens": [],
250
+ "mark": false,
251
+ "card": "Icon"
252
+ },
253
+ "jobChangeBanner": {
254
+ "component": "jobChangeBanner",
255
+ "direction": "banner",
256
+ "phone": {
257
+ "layout": "banner"
258
+ },
259
+ "tablet": {
260
+ "layout": "banner"
261
+ },
262
+ "desk": {
263
+ "layout": "banner"
264
+ },
265
+ "touch": 44,
266
+ "pageOverflow": "none",
267
+ "screens": [],
268
+ "card": "JobChangeBanner"
269
+ },
270
+ "listFrame": {
271
+ "component": "listFrame",
272
+ "direction": "frame",
273
+ "phone": {
274
+ "layout": "page"
275
+ },
276
+ "tablet": {
277
+ "layout": "frame"
278
+ },
279
+ "desk": {
280
+ "layout": "frame"
281
+ },
282
+ "touch": 44,
283
+ "pageOverflow": "none",
284
+ "screens": [
285
+ "jobs-board",
286
+ "jobs-list",
287
+ "job-panel",
288
+ "invoices-ready",
289
+ "invoices-pending",
290
+ "invoices-approved",
291
+ "invoices-ticked",
292
+ "invoices-filter",
293
+ "reconcile"
294
+ ],
295
+ "card": "ListFrame"
296
+ },
297
+ "listHead": {
298
+ "component": "listHead",
299
+ "direction": "one row",
300
+ "phone": {
301
+ "layout": "one-row"
302
+ },
303
+ "tablet": {
304
+ "layout": "toolbar"
305
+ },
306
+ "desk": {
307
+ "layout": "toolbar"
308
+ },
309
+ "touch": 44,
310
+ "pageOverflow": "none",
311
+ "screens": [
312
+ "jobs-board",
313
+ "jobs-list",
314
+ "job-panel",
315
+ "invoices-ready",
316
+ "invoices-pending",
317
+ "invoices-approved",
318
+ "invoices-ticked",
319
+ "invoices-filter",
320
+ "reconcile"
321
+ ],
322
+ "card": "ListHead"
323
+ },
324
+ "listPager": {
325
+ "component": "listPager",
326
+ "direction": "slim foot",
327
+ "phone": {
328
+ "layout": "slim"
329
+ },
330
+ "tablet": {
331
+ "layout": "full"
332
+ },
333
+ "desk": {
334
+ "layout": "full"
335
+ },
336
+ "touch": 44,
337
+ "pageOverflow": "none",
338
+ "screens": [
339
+ "invoices-ready",
340
+ "invoices-pending",
341
+ "invoices-approved",
342
+ "invoices-ticked",
343
+ "invoices-filter"
344
+ ],
345
+ "card": "ListPager"
346
+ },
347
+ "messagePage": {
348
+ "component": "messagePage",
349
+ "direction": "card",
350
+ "phone": {
351
+ "layout": "card"
352
+ },
353
+ "tablet": {
354
+ "layout": "card"
355
+ },
356
+ "desk": {
357
+ "layout": "card"
358
+ },
359
+ "touch": 44,
360
+ "pageOverflow": "none",
361
+ "mark": false,
362
+ "screens": [],
363
+ "card": "MessagePage"
364
+ },
365
+ "notice": {
366
+ "component": "notice",
367
+ "direction": "note",
368
+ "phone": {
369
+ "layout": "note"
370
+ },
371
+ "tablet": {
372
+ "layout": "note"
373
+ },
374
+ "desk": {
375
+ "layout": "note"
376
+ },
377
+ "touch": 44,
378
+ "pageOverflow": "none",
379
+ "screens": [],
380
+ "card": "Notice"
381
+ },
382
+ "optionPill": {
383
+ "component": "optionPill",
384
+ "direction": "pill",
385
+ "phone": {
386
+ "layout": "pill"
387
+ },
388
+ "tablet": {
389
+ "layout": "pill"
390
+ },
391
+ "desk": {
392
+ "layout": "pill"
393
+ },
394
+ "touch": 44,
395
+ "pageOverflow": "none",
396
+ "screens": [],
397
+ "mark": false,
398
+ "card": "OptionPill"
399
+ },
400
+ "pageHeader": {
401
+ "component": "pageHeader",
402
+ "direction": "title",
403
+ "phone": {
404
+ "layout": "title"
405
+ },
406
+ "tablet": {
407
+ "layout": "title"
408
+ },
409
+ "desk": {
410
+ "layout": "title"
411
+ },
412
+ "touch": 44,
413
+ "pageOverflow": "none",
414
+ "screens": [
415
+ "jobs-board",
416
+ "jobs-list",
417
+ "job-panel",
418
+ "invoices-ready",
419
+ "invoices-pending",
420
+ "invoices-approved",
421
+ "invoices-ticked",
422
+ "invoices-filter",
423
+ "invoice",
424
+ "reconcile",
425
+ "price-list",
426
+ "rules",
427
+ "automations",
428
+ "integrations",
429
+ "custom-fields",
430
+ "appearance"
431
+ ],
432
+ "card": "PageHeader"
433
+ },
434
+ "fx-pager": {
435
+ "component": "fx-pager",
436
+ "direction": "record bar",
437
+ "phone": {
438
+ "layout": "bar"
439
+ },
440
+ "tablet": {
441
+ "layout": "bar"
442
+ },
443
+ "desk": {
444
+ "layout": "bar"
445
+ },
446
+ "touch": 44,
447
+ "pageOverflow": "none",
448
+ "screens": [
449
+ "invoice"
450
+ ],
451
+ "mark": false,
452
+ "selector": ".fx-pager",
453
+ "card": "Pager"
454
+ },
455
+ "rowMenu": {
456
+ "component": "rowMenu",
457
+ "direction": "menu",
458
+ "phone": {
459
+ "layout": "menu"
460
+ },
461
+ "tablet": {
462
+ "layout": "menu"
463
+ },
464
+ "desk": {
465
+ "layout": "menu"
466
+ },
467
+ "touch": 44,
468
+ "pageOverflow": "none",
469
+ "screens": [],
470
+ "card": "RowMenu"
471
+ },
472
+ "savedViews": {
473
+ "component": "savedViews",
474
+ "direction": "scrolling tabs",
475
+ "phone": {
476
+ "layout": "scroll"
477
+ },
478
+ "tablet": {
479
+ "layout": "tabs"
480
+ },
481
+ "desk": {
482
+ "layout": "tabs"
483
+ },
484
+ "touch": 44,
485
+ "pageOverflow": "none",
486
+ "screens": [
487
+ "jobs-board",
488
+ "jobs-list",
489
+ "job-panel",
490
+ "invoices-ready",
491
+ "invoices-pending",
492
+ "invoices-approved",
493
+ "invoices-ticked",
494
+ "invoices-filter",
495
+ "reconcile"
496
+ ],
497
+ "card": "SavedViews"
498
+ },
499
+ "settingRow": {
500
+ "component": "settingRow",
501
+ "direction": "sheet",
502
+ "phone": {
503
+ "layout": "sheet"
504
+ },
505
+ "tablet": {
506
+ "layout": "row"
507
+ },
508
+ "desk": {
509
+ "layout": "row"
510
+ },
511
+ "touch": 44,
512
+ "pageOverflow": "none",
513
+ "screens": [
514
+ "appearance"
515
+ ],
516
+ "card": "SettingRow"
517
+ },
518
+ "sidecar": {
519
+ "component": "sidecar",
520
+ "direction": "full page",
521
+ "phone": {
522
+ "layout": "page"
523
+ },
524
+ "tablet": {
525
+ "layout": "panel"
526
+ },
527
+ "desk": {
528
+ "layout": "panel"
529
+ },
530
+ "touch": 44,
531
+ "pageOverflow": "none",
532
+ "screens": [
533
+ "job-panel"
534
+ ],
535
+ "card": "Sidecar"
536
+ },
537
+ "fx-tabs": {
538
+ "component": "fx-tabs",
539
+ "direction": "scrolling tabs",
540
+ "phone": {
541
+ "layout": "scroll"
542
+ },
543
+ "tablet": {
544
+ "layout": "tabs"
545
+ },
546
+ "desk": {
547
+ "layout": "tabs"
548
+ },
549
+ "touch": 44,
550
+ "pageOverflow": "none",
551
+ "screens": [
552
+ "invoices-ready",
553
+ "invoices-pending",
554
+ "invoices-approved",
555
+ "invoices-ticked",
556
+ "invoices-filter",
557
+ "price-list"
558
+ ],
559
+ "mark": false,
560
+ "selector": ".fx-tabs",
561
+ "card": "Tabs"
562
+ },
563
+ "tileCard": {
564
+ "component": "tileCard",
565
+ "direction": "one-line rows",
566
+ "phone": {
567
+ "layout": "one-line",
568
+ "rowHeight": 56
569
+ },
570
+ "tablet": {
571
+ "layout": "cards"
572
+ },
573
+ "desk": {
574
+ "layout": "cards"
575
+ },
576
+ "touch": 44,
577
+ "pageOverflow": "none",
578
+ "screens": [
579
+ "rules",
580
+ "integrations"
581
+ ],
582
+ "card": "Tiles"
583
+ },
584
+ "tileSection": {
585
+ "component": "tileSection",
586
+ "direction": "one-line rows",
587
+ "phone": {
588
+ "layout": "one-line",
589
+ "rowHeight": 56
590
+ },
591
+ "tablet": {
592
+ "layout": "cards"
593
+ },
594
+ "desk": {
595
+ "layout": "cards"
596
+ },
597
+ "touch": 44,
598
+ "pageOverflow": "none",
599
+ "screens": [
600
+ "rules"
601
+ ],
602
+ "card": "Tiles"
603
+ },
604
+ "named": {
605
+ "component": "named",
606
+ "direction": "tip",
607
+ "phone": {
608
+ "layout": "tip"
609
+ },
610
+ "tablet": {
611
+ "layout": "tip"
612
+ },
613
+ "desk": {
614
+ "layout": "tip"
615
+ },
616
+ "touch": 44,
617
+ "pageOverflow": "none",
618
+ "screens": [],
619
+ "mark": false,
620
+ "card": "Tip"
621
+ },
622
+ "toast": {
623
+ "component": "toast",
624
+ "direction": "toast",
625
+ "phone": {
626
+ "layout": "toast"
627
+ },
628
+ "tablet": {
629
+ "layout": "toast"
630
+ },
631
+ "desk": {
632
+ "layout": "toast"
633
+ },
634
+ "touch": 44,
635
+ "pageOverflow": "none",
636
+ "screens": [],
637
+ "card": "Toast"
638
+ }
639
+ };
640
+
641
+ // The card's rules for one component (columnTable, listHead, sidecar, …).
642
+ export function rule(component) {
643
+ const found = RULES[component];
644
+ if (!found) throw new Error(`No phone rules for ${component}`);
645
+ return found;
646
+ }
647
+
648
+ export function isPhone() {
649
+ return typeof matchMedia === 'function' && matchMedia(WIDTHS.phone).matches;
650
+ }
651
+
652
+ export function isTablet() {
653
+ return typeof matchMedia === 'function' && matchMedia(WIDTHS.tabletMin).matches && matchMedia(WIDTHS.tabletMax).matches;
654
+ }
655
+
656
+ // The layout the card names for the width the page is at now.
657
+ export function layoutFor(component) {
658
+ const found = rule(component);
659
+ if (isPhone()) return found.phone.layout;
660
+ if (isTablet()) return found.tablet.layout;
661
+ return found.desk?.layout ?? found.tablet.layout;
662
+ }
663
+
664
+ // The rules that list this phone-suite screen.
665
+ export function rulesFor(screenId) {
666
+ return Object.values(RULES).filter((found) => found.screens?.includes(screenId));
667
+ }
668
+
669
+ // Mark an element with the layout, touch size and row height from its card,
670
+ // and keep the mark current when the window crosses a width. The phone suite
671
+ // reads these marks.
672
+ export function applyPhoneRule(el, component) {
673
+ const found = rule(component);
674
+ if (!el?.dataset) return el;
675
+ const paint = () => {
676
+ el.dataset.phoneRule = component;
677
+ el.dataset.phoneLayout = layoutFor(component);
678
+ el.dataset.phoneTouch = String(found.touch);
679
+ if (isPhone() && found.phone.rowHeight) el.dataset.phoneRowHeight = String(found.phone.rowHeight);
680
+ else delete el.dataset.phoneRowHeight;
681
+ };
682
+ paint();
683
+ if (typeof matchMedia === 'function' && !el.dataset.phoneWatch) {
684
+ el.dataset.phoneWatch = '1';
685
+ for (const query of [WIDTHS.phone, WIDTHS.tabletMin, WIDTHS.desk]) matchMedia(query).addEventListener?.('change', paint);
686
+ }
687
+ return el;
688
+ }
689
+
690
+ // A list column's phone slot, from the flag the Columns card names
691
+ // (phone: 'title' | 'meta' | 'end' | 'hide'). Anything else is unset.
692
+ export function phoneSlot(column) {
693
+ const found = rule('columnTable');
694
+ const value = column?.[found.phone.columnFlag];
695
+ return found.phone.flags?.includes(value) ? value : null;
696
+ }
697
+
698
+ // Defaults for one list's columns. An explicit phone flag wins, up to the
699
+ // card's slot counts. Otherwise the first column is the title, the money
700
+ // column (kind total, or num) is the end, a stage or status leads the meta
701
+ // line, and further columns are meta in order. Past the slot counts, hide.
702
+ export function assignPhoneSlots(columns) {
703
+ const { slots } = rule('columnTable').phone;
704
+ const counts = { title: 0, end: 0, meta: 0 };
705
+ const room = (slot) => slot === 'hide' || counts[slot] < slots[slot];
706
+ const claim = (slot) => {
707
+ if (slot !== 'hide') counts[slot] += 1;
708
+ return slot;
709
+ };
710
+ const chosen = columns.map((column) => {
711
+ const explicit = phoneSlot(column);
712
+ if (!explicit) return null;
713
+ return room(explicit) ? claim(explicit) : 'hide';
714
+ });
715
+ const open = () => chosen.findIndex((slot) => slot == null);
716
+ const give = (index, slot) => {
717
+ if (index < 0 || !room(slot)) return;
718
+ chosen[index] = claim(slot);
719
+ };
720
+ give(open(), 'title');
721
+ give(columns.findIndex((column, index) => chosen[index] == null && (column.kind === 'total' || column.num)), 'end');
722
+ give(columns.findIndex((column, index) => chosen[index] == null && (column.kind === 'stage' || column.kind === 'status')), 'meta');
723
+ let index = open();
724
+ while (index >= 0) {
725
+ chosen[index] = room('meta') ? claim('meta') : 'hide';
726
+ index = open();
727
+ }
728
+ return columns.map((column, i) => (column.phone === chosen[i] ? column : { ...column, phone: chosen[i] }));
729
+ }