@arcforgelabs/flow 2026.10.37 → 2026.10.38

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.
@@ -5,7 +5,8 @@
5
5
  // its name on hover and to a screen reader. More opens every action, with a
6
6
  // search box, grouped under small heads.
7
7
  //
8
- // bulkBar(ui) answers { element, draw(state) }. state = { count, onClear,
8
+ // bulkBar(ui, { label }) answers { element, draw(state) }. label names the
9
+ // bar for a screen reader ("Ticked invoices", "Ticked jobs"). state = { count, onClear,
9
10
  // groups: [[action]], more: [{ head, actions: [action] }] }; an action is
10
11
  // { icon, label, title?, run, primary?, iconOnly?, tone? }. title is the
11
12
  // long name, for the hover tip and the More menu. A count of 0 hides the
@@ -14,10 +15,10 @@ import { lucide } from '/assets/ui/custom-fields.js';
14
15
 
15
16
  let barCount = 0;
16
17
 
17
- export function bulkBar(ui) {
18
+ export function bulkBar(ui, { label = 'Ticked invoices' } = {}) {
18
19
  const { h } = ui;
19
20
  const id = `fx-bulk-more-${++barCount}`;
20
- const element = h('div', { class: 'fx-bulk', role: 'toolbar', 'aria-label': 'Ticked invoices', hidden: true });
21
+ const element = h('div', { class: 'fx-bulk', role: 'toolbar', 'aria-label': label, hidden: true });
21
22
  const panel = h('div', { class: 'fx-pop fx-bulk__more', id, popover: 'auto', role: 'menu' });
22
23
  let current = null;
23
24
  let busy = false;
@@ -512,6 +512,24 @@ a.fx-strong:hover { text-decoration: underline; }
512
512
  .fx-table tbody tr.fx-table__group:hover td { background: var(--t-s1); }
513
513
  .fx-table__group-row { display: flex; align-items: center; gap: 12px; }
514
514
  .fx-table__group-row .fx-board__sum { margin-left: 0; }
515
+ /* The Jobs list's group head folds its rows (jobs.js, list): a click on the
516
+ head or its chevron. */
517
+ .fx-table--jobs .fx-table__group { cursor: pointer; }
518
+ .fx-table--jobs .fx-table__group td.fx-tick-cell { padding-right: 0; }
519
+ .fx-table__fold { display: inline-flex; align-items: center; gap: 8px; min-width: 0; padding: 2px 4px 2px 0; border: 0; background: none; color: var(--t-dim); font: inherit; cursor: pointer; }
520
+ .fx-table__fold > svg { flex: none; transition: transform .15s; }
521
+ .fx-table__fold[aria-expanded="true"] > svg { transform: rotate(90deg); }
522
+ .fx-table__fold:hover, .fx-table__fold:focus-visible { color: var(--t-strong); outline: none; }
523
+ .fx-table__fold:focus-visible .fx-stage, .fx-table__fold:focus-visible .fx-cf__opt { box-shadow: 0 0 0 2px var(--t-accent); }
524
+ .fx-table__group--folded td { padding-bottom: 14px; }
525
+ /* A Jobs list column head opens the column's menu (a click or a
526
+ right-click); + at the end shows a hidden column. */
527
+ .fx-th { margin: -4px -6px; padding: 4px 6px; border: 0; border-radius: var(--t-radius); background: none; color: inherit; font: inherit; letter-spacing: inherit; text-transform: inherit; cursor: pointer; }
528
+ .fx-th:hover, .fx-th:focus-visible { background: var(--fx-hover); color: var(--t-strong); outline: none; }
529
+ .fx-th-add { width: 36px; text-align: right; }
530
+ .fx-th--add { font-size: 16px; line-height: 1; }
531
+ .fx-avatar--sm { width: 24px; height: 24px; font-size: 10px; }
532
+ .fx-c-date time { font-variant-numeric: tabular-nums; }
515
533
  .fx-stage[data-rank="plain"] { --stage: var(--t-dim); }
516
534
  /* The board is one row of lanes (one per group), each about a phone wide.
517
535
  Lanes share the width when they fit; more lanes scroll sideways inside
@@ -613,6 +631,70 @@ a.fx-strong:hover { text-decoration: underline; }
613
631
  .fx-task__fix h3 { padding: 12px 14px 4px; color: var(--t-warn); }
614
632
  .fx-task__fixrow { padding: 9px 14px; border-top: 1px solid var(--fx-line-soft); color: var(--t-strong); font-size: 13.5px; }
615
633
  .fx-task__fixrow:first-of-type { border-top: 0; }
634
+ .fx-task__choose { scroll-margin-top: 16px; margin-top: 22px; display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 12px 14px; border: 1px solid var(--t-warn-brd, var(--t-warn)); border-radius: var(--t-radius); background: var(--t-warn-bg); color: var(--t-strong); font-size: 13.5px; }
635
+ .fx-task__choose > svg { flex: none; width: 20px; height: 20px; color: var(--t-warn); }
636
+ .fx-task__choose > span { flex: 1 1 200px; }
637
+ .fx-task__choices { display: flex; flex-wrap: wrap; gap: 10px; }
638
+ .fx-task__none { display: inline-flex; align-items: center; gap: 6px; color: var(--t-warn); }
639
+ .fx-task__none svg { width: 16px; height: 16px; }
640
+
641
+ /* Reconcile: the queue left, one job and its possible invoices right. */
642
+ .fx-rec { display: grid; grid-template-columns: 280px minmax(0, 1fr); border: 1px solid var(--fx-line); border-radius: var(--t-radius-lg); background: var(--t-paper); min-height: 420px; }
643
+ .fx-rec > .fx-empty, .fx-rec > .fx-dim { grid-column: 1 / -1; }
644
+ .fx-rec__queue { display: flex; flex-direction: column; border-right: 1px solid var(--fx-line); max-height: 75vh; overflow-y: auto; }
645
+ .fx-rec__tabs { display: flex; flex-wrap: wrap; gap: 6px; padding: 12px 16px; border-bottom: 1px solid var(--fx-line); }
646
+ .fx-rec__tab { display: inline-flex; align-items: center; min-height: 32px; padding: 0 10px; border: 1px solid var(--fx-line); border-radius: 999px; color: var(--t-text); font-size: 12.5px; font-weight: 600; text-decoration: none; }
647
+ .fx-rec__tab[aria-selected="true"] { border-color: var(--t-primary); background: var(--t-primary); color: var(--t-on-primary); }
648
+ .fx-rec__search { width: 100%; min-width: 0; }
649
+ .fx-rec__results { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
650
+ .fx-rec__item { display: grid; gap: 3px; min-height: 44px; padding: 12px 16px; border-bottom: 1px solid var(--fx-line-soft); border-left: 3px solid transparent; color: var(--t-text); text-decoration: none; }
651
+ .fx-rec__item:hover { background: var(--fx-hover); }
652
+ .fx-rec__item[aria-current="true"] { border-left-color: var(--t-accent); background: var(--t-s1); }
653
+ .fx-rec__item-top { display: flex; justify-content: space-between; gap: 8px; }
654
+ .fx-rec__item .fx-num { font-family: var(--t-data); font-variant-numeric: tabular-nums; }
655
+ .fx-rec__detail { display: flex; flex-direction: column; gap: 16px; min-width: 0; padding: 20px 24px; }
656
+ .fx-rec__head { display: flex; justify-content: space-between; align-items: flex-end; gap: 16px; flex-wrap: wrap; }
657
+ .fx-rec__head > div { display: grid; gap: 2px; }
658
+ .fx-rec__back { display: none; color: var(--t-accent); text-decoration: none; font-size: 13px; }
659
+ .fx-rec__title { margin: 0; font-family: var(--t-display); font-weight: var(--t-display-weight); color: var(--t-strong); font-size: 24px; }
660
+ .fx-rec__table { display: grid; grid-template-columns: 110px minmax(150px, 1fr) repeat(var(--cols), minmax(170px, 1fr)); border: 1px solid var(--fx-line); border-radius: var(--t-radius); overflow-x: auto; }
661
+ .fx-rec__row { display: contents; }
662
+ .fx-rec__row > span { display: flex; flex-direction: column; gap: 2px; padding: 10px 12px; border-top: 1px solid var(--fx-line-soft); font-size: 13.5px; color: var(--t-strong); }
663
+ .fx-rec__row:first-child > span { border-top: 0; background: var(--t-s1); }
664
+ .fx-rec__row > span[role="cell"]:not(.fx-rec__job) { flex-direction: row; align-items: flex-start; gap: 6px; }
665
+ .fx-rec__row > span[role="cell"].fx-rec__acts { flex-direction: column; align-items: stretch; gap: 8px; }
666
+ .fx-rec__acts > .fx-task__link, .fx-rec__card > .fx-task__link { align-self: flex-start; }
667
+ .fx-rec__job { background: color-mix(in srgb, var(--t-accent) 5%, transparent); }
668
+ .fx-rec__row:first-child > .fx-rec__job { background: color-mix(in srgb, var(--t-accent) 10%, var(--t-s1)); }
669
+ .fx-rec__lines { display: flex; flex-direction: column; font-family: var(--t-data); font-variant-numeric: tabular-nums; font-size: 13px; line-height: 1.6; }
670
+ .fx-rec__mark { flex: none; font-weight: 800; }
671
+ [data-mark="same"] > .fx-rec__mark { color: var(--t-success); }
672
+ [data-mark="differs"], [data-mark="differs"] > .fx-rec__mark { color: var(--t-warn); }
673
+ .fx-rec__best { color: var(--t-success); font-size: 12px; font-weight: 700; }
674
+ .fx-rec__foot { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; }
675
+ .fx-rec__foot-acts { display: flex; gap: 10px; flex-wrap: wrap; }
676
+ .fx-rec__cards { display: none; }
677
+ .fx-rec__card { display: flex; flex-direction: column; gap: 6px; padding: 12px; border: 1px solid var(--fx-line); border-radius: var(--t-radius); font-size: 13.5px; color: var(--t-strong); }
678
+ .fx-rec__card.fx-rec__job { border-color: transparent; }
679
+ .fx-rec__card--best { border-color: var(--t-success); }
680
+ .fx-rec__card-top { display: flex; justify-content: space-between; gap: 8px; }
681
+ .fx-rec__label { color: var(--t-dim); font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
682
+ .fx-rec__checks { display: flex; flex-wrap: wrap; gap: 4px 12px; }
683
+ @media (max-width: 1100px) {
684
+ .fx-rec { grid-template-columns: minmax(0, 1fr); min-height: 0; }
685
+ .fx-rec__queue { border-right: 0; max-height: none; }
686
+ .fx-rec--open .fx-rec__queue { display: none; }
687
+ .fx-rec:not(.fx-rec--open) .fx-rec__detail { display: none; }
688
+ .fx-rec__back { display: inline; }
689
+ }
690
+ @media (max-width: 820px) {
691
+ .fx-rec__detail { padding: 16px; }
692
+ .fx-rec__table { display: none; }
693
+ .fx-rec__cards { display: flex; flex-direction: column; gap: 12px; }
694
+ .fx-rec__cards .fx-btn, .fx-rec__foot .fx-btn { min-height: 44px; }
695
+ .fx-rec__foot-acts { width: 100%; }
696
+ .fx-rec__foot-acts .fx-btn { flex: 1 1 auto; }
697
+ }
616
698
  .fx-task__custom { margin-top: 24px; }
617
699
  .fx-task__custom h3 { margin: 0 0 4px; color: var(--t-dim); font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
618
700
  .fx-task__input { width: 100%; min-width: 0; }
@@ -838,6 +920,20 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
838
920
  .fx-people td:nth-child(2) .fx-select { width: 100%; }
839
921
  .fx-people td:nth-child(3) { grid-area: 3 / 1 / 4 / -1; }
840
922
  .fx-people td:nth-child(3):empty { display: none; }
923
+ /* Jobs list: the tick at the left of each row and group head; the client
924
+ and total on the first line, then each other column on its own line,
925
+ with its name. */
926
+ .fx-table--jobs tr { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto; align-items: center; gap: 4px 8px; }
927
+ .fx-table--jobs td, .fx-table--jobs td:nth-child(2), .fx-table--jobs td:nth-child(3) { display: block; width: auto; margin: 0; padding: 0; }
928
+ .fx-table--jobs tr > td.fx-tick-cell { grid-column: 1; grid-row: 1; align-self: start; padding-top: 2px; }
929
+ .fx-table--jobs tr.fx-row-link > td:not(.fx-tick-cell) { grid-column: 2 / -1; }
930
+ .fx-table--jobs tr.fx-row-link > td.fx-c-client { grid-area: 1 / 2; }
931
+ .fx-table--jobs tr.fx-row-link > td.fx-c-total { grid-area: 1 / 3; }
932
+ .fx-table--jobs td.fx-c-end { display: none; }
933
+ .fx-table--jobs tr.fx-row-link > td[data-label]:not(.fx-c-total, .fx-c-stage)::before { content: attr(data-label) " "; color: var(--t-dim); font-size: 12px; }
934
+ .fx-table--jobs tr.fx-table__group { grid-template-columns: 28px minmax(0, 1fr); background: var(--t-s1); }
935
+ .fx-table--jobs tr.fx-table__group > td.fx-tick-cell { grid-row: 1; align-self: center; padding: 0; }
936
+ .fx-table--jobs tr.fx-table__group > td { background: none; }
841
937
  }
842
938
 
843
939
  @keyframes fx-rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@@ -1092,3 +1188,48 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
1092
1188
  .fx-bulk__sep { margin: 0 2px; }
1093
1189
  .fx-bulk__act:not(.fx-bulk__act--primary) .fx-bulk__word { display: none; }
1094
1190
  }
1191
+
1192
+ /* Xero › Import (server/ui/xero-import.js): Review compares one fact per row
1193
+ with ✓ or ≠ between the sides; the list and history tick rows for the bulk
1194
+ bar. */
1195
+ .fx-xi { display: flex; flex-direction: column; gap: 16px; min-width: 0; max-width: 980px; }
1196
+ .fx-xi .fx-lucide { width: 16px; height: 16px; }
1197
+ .fx-xi__views { display: inline-flex; border: 1px solid var(--fx-line); }
1198
+ .fx-xi__view { display: inline-grid; place-items: center; width: 40px; height: 36px; border: 0; background: transparent; color: var(--t-dim); cursor: pointer; }
1199
+ .fx-xi__view[aria-selected="true"] { background: var(--t-primary); color: var(--t-on-primary); }
1200
+ .fx-xi__count { display: inline-flex; align-items: center; gap: 10px; font-variant-numeric: tabular-nums; }
1201
+ .fx-xi__progress { display: inline-block; width: 120px; height: 4px; background: var(--fx-line); }
1202
+ .fx-xi__progress > span { display: block; height: 100%; background: var(--t-primary); }
1203
+ .fx-xi__bulk { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 10px 14px; border: 1px dashed var(--t-primary); background: color-mix(in srgb, var(--t-primary) 6%, transparent); color: var(--t-strong); font-size: 13.5px; }
1204
+ .fx-xi__bulk-text { margin-right: auto; }
1205
+ .fx-xi__bulk .fx-btn { display: inline-flex; align-items: center; gap: 6px; }
1206
+ .fx-xi__bulk .fx-xi__search { flex: 1; min-width: 0; margin-right: 0; }
1207
+ .fx-xi-cmp { width: 100%; border-collapse: collapse; border: 1px solid var(--fx-line); background: var(--t-s1); }
1208
+ .fx-xi-cmp th, .fx-xi-cmp td { padding: 11px 16px; border-top: 1px solid var(--fx-line-soft); text-align: left; font-size: 14px; color: var(--t-strong); }
1209
+ .fx-xi-cmp thead th { border-top: 0; padding: 14px 16px; }
1210
+ .fx-xi-cmp tbody th { width: 44px; color: var(--t-dim); }
1211
+ .fx-xi-cmp td:nth-child(2), .fx-xi-cmp th:nth-child(2) { width: 42%; }
1212
+ .fx-xi-cmp__mid { width: 48px; text-align: center !important; }
1213
+ .fx-xi__mark { height: 22px; width: auto; vertical-align: middle; }
1214
+ .fx-xi-cmp thead th:last-child { display: flex; align-items: center; gap: 10px; color: var(--t-dim); }
1215
+ .fx-xi__agree { display: inline-grid; place-items: center; width: 24px; height: 24px; }
1216
+ .fx-xi__agree[data-state="same"] { color: var(--t-success); }
1217
+ .fx-xi__agree[data-state="differs"] { color: var(--t-warn); }
1218
+ .fx-xi__acts { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; }
1219
+ .fx-xi__acts .fx-btn { display: inline-flex; align-items: center; gap: 6px; }
1220
+ .fx-xi__done { display: flex; align-items: center; gap: 12px; padding: 24px; border: 1px solid var(--fx-line); color: var(--t-success); }
1221
+ .fx-xi__done p { margin: 0; color: var(--t-strong); }
1222
+ .fx-xi__scroll { overflow-x: auto; }
1223
+ .fx-xi-list .fx-xi__mark { height: 18px; }
1224
+ .fx-xi-list__mid { width: 56px; text-align: center; color: var(--t-dim); white-space: nowrap; }
1225
+ .fx-xi-list__mid .fx-dim { margin-left: 4px; font-size: 12px; }
1226
+ .fx-xi-list tr[data-state="linked"] .fx-xi-list__mid { color: var(--t-success); }
1227
+ .fx-xi-list tr[data-state="invoice_fits"] .fx-xi-list__mid a, .fx-xi-list tr[data-state="job_fits"] .fx-xi-list__mid a { color: var(--t-warn); }
1228
+ @media (max-width: 820px) {
1229
+ .fx-xi-cmp th, .fx-xi-cmp td { padding: 10px 8px; font-size: 13px; }
1230
+ .fx-xi-cmp tbody th { width: 28px; }
1231
+ .fx-xi-cmp__mid { width: 28px; }
1232
+ .fx-xi__acts .fx-btn { flex: 1 1 auto; min-height: 44px; justify-content: center; }
1233
+ }
1234
+ .fx-xi-count { font-size: 18px; }
1235
+ .fx-xi-list td.fx-num:not(.fx-strong) { text-align: left; }
@@ -97,9 +97,9 @@ function reasonFix(reason) {
97
97
  if (reason === 'the job status moved back after its invoice was made') return fix('Status moved back', 'Check the job in the work system. Void or hold its invoice if the work is not done.', 'work');
98
98
  if (/deleted in /i.test(reason)) return fix('Deleted', 'Check the job in the work system. Void its invoice if it has one.', 'work');
99
99
  // The matcher found invoices that may be this job's, but no exact link.
100
- // Flow holds the job so that it is not invoiced two times.
100
+ // A person makes an invoice or links one (jobs.js draws the two buttons).
101
101
  if (/^the accounting evidence is ambiguous/.test(reason)) {
102
- return { label: 'Invoice unclear', text: 'Flow cannot tell if this job has an invoice already.', todo: 'Check the invoice number on the job in the work system. Flow holds the job so it is not invoiced two times.', go: 'work' };
102
+ return { label: 'No invoice', text: 'This job has no invoice.', todo: null, go: 'choose' };
103
103
  }
104
104
  if (/^the accounting evidence is conflict/.test(reason)) {
105
105
  return { label: 'Invoice link changed', text: 'A second invoice now answers for this job, or its invoice is gone.', todo: 'Check the invoice number on the job in the work system. Void an extra invoice.', go: 'work' };
@@ -133,11 +133,13 @@ function draftFix(problem) {
133
133
  return { label: 'Invoice problem', text: 'The invoice for this job needs a fix before it can be sent.', todo: 'Open the invoice for the steps.', go: 'invoice' };
134
134
  }
135
135
 
136
- // Where a fix's link goes: Flow's invoice, the job in the work system, or a
137
- // Flow settings page (jobs.js draws the link).
138
- export const GO = ['invoice', 'work', 'price-list', 'invoicing-settings', 'integrations'];
136
+ // Where a fix's link goes: Flow's invoice, the job in the work system, a
137
+ // Flow settings page, or `choose`: Make invoice and Link invoice buttons
138
+ // (jobs.js draws them).
139
+ export const GO = ['invoice', 'work', 'price-list', 'invoicing-settings', 'integrations', 'choose'];
139
140
 
140
- // What a person must fix on a job, in order: [{ label, text, todo, go }].
141
+ // What a person must fix on a job, in order: [{ label, text, todo, go }];
142
+ // `todo` is null when the buttons are the instruction.
141
143
  // Empty unless invoiceStage is Problem.
142
144
  export function fixes(o) {
143
145
  if (invoiceStage(o) !== 'problem') return [];
@@ -5,10 +5,24 @@
5
5
  // (test/job-view.test.js). server/ui/jobs.js draws the page.
6
6
 
7
7
  export const LAYOUTS = ['board', 'list'];
8
- export const GROUPS = { status: 'Status', stage: 'Invoice stage', client: 'Client', practitioner: 'Practitioner' };
9
- export const SORTS = { board: 'Lab system order', due: 'Date', client: 'Client', practitioner: 'Practitioner', total: 'Total' };
8
+ // 'none' is one plain list with no group heads. The board has a lane per
9
+ // group, so there it shows the status lanes.
10
+ export const GROUPS = { status: 'Status', stage: 'Invoice stage', client: 'Client', practitioner: 'Practitioner', none: 'None' };
11
+ export const SORTS = { board: 'Lab system order', due: 'Date', ordered: 'Requested', shipped: 'Shipped', client: 'Client', practitioner: 'Practitioner', total: 'Total' };
10
12
  export const DIRS = ['asc', 'desc'];
11
- export const DEFAULT_VIEW = Object.freeze({ layout: 'board', group: 'status', sort: 'board', dir: 'asc', hide: [], late: false, problems: false });
13
+ // The list's columns, in order. Client is always the first column. One
14
+ // column holds one field: each date has its own column. A custom field is
15
+ // a column too ('field:<field id>'). Patient shows only when the
16
+ // installation keeps patient names; Assignee only when the lab has a
17
+ // person field.
18
+ export const COLUMNS = {
19
+ case: 'Case', practitioner: 'Practitioner', patient: 'Patient', stage: 'Stage', assignee: 'Assignee',
20
+ ordered: 'Requested', due: 'Due', shipped: 'Shipped', total: 'Total',
21
+ };
22
+ export const DEFAULT_COLUMNS = Object.freeze(['practitioner', 'patient', 'stage', 'assignee', 'due', 'total']);
23
+ export const DEFAULT_VIEW = Object.freeze({
24
+ layout: 'board', group: 'status', sort: 'board', dir: 'asc', hide: [], late: false, problems: false, mine: false, columns: DEFAULT_COLUMNS,
25
+ });
12
26
 
13
27
  // Group by one of the lab's custom fields (docs/CUSTOM_FIELDS.md):
14
28
  // 'field:<field id>'. Only dropdown, person and checkbox fields make lanes.
@@ -17,6 +31,7 @@ export const fieldGroup = (fieldId) => `field:${fieldId}`;
17
31
  export const fieldIdOf = (group) => (FIELD_GROUP.test(group ?? '') ? group.slice(6) : null);
18
32
 
19
33
  const MAX_HIDDEN = 100;
34
+ const MAX_COLUMNS = 40;
20
35
  const MAX_LABEL = 120;
21
36
 
22
37
  // A view as Flow stores it: only the known keys, each checked. Anything else
@@ -35,27 +50,35 @@ export function normaliseJobsView(input) {
35
50
  hide,
36
51
  late: value.late === true,
37
52
  problems: value.problems === true,
53
+ mine: value.mine === true,
54
+ columns: Array.isArray(value.columns)
55
+ ? [...new Set(value.columns.filter((c) => Object.hasOwn(COLUMNS, c) || FIELD_GROUP.test(c ?? '')))].slice(0, MAX_COLUMNS)
56
+ : [...DEFAULT_COLUMNS],
38
57
  };
39
58
  }
40
59
 
41
60
  // How many filters are on: shown on the Filter button.
42
61
  export function filterCount(view) {
43
- return view.hide.length + (view.late ? 1 : 0) + (view.problems ? 1 : 0);
62
+ return view.hide.length + (view.late ? 1 : 0) + (view.problems ? 1 : 0) + (view.mine ? 1 : 0);
44
63
  }
45
64
 
46
65
  // The jobs the filter keeps. `status` is a job's status label; `late` and
47
- // `problem` say if a job is late or has a problem (jobs.js knows how).
48
- export function filterJobs(jobs, view, { status, late, problem }) {
66
+ // `problem` say if a job is late or has a problem; `mine` says if it is
67
+ // assigned to the person looking (jobs.js knows how).
68
+ export function filterJobs(jobs, view, { status, late, problem, mine = () => true }) {
49
69
  const hidden = new Set(view.hide);
50
70
  return jobs.filter((job) => !hidden.has(status(job))
51
71
  && (!view.late || late(job))
52
- && (!view.problems || problem(job)));
72
+ && (!view.problems || problem(job))
73
+ && (!view.mine || mine(job)));
53
74
  }
54
75
 
55
76
  const dayOf = (job) => job.dueOn ?? job.orderedOn ?? job.jobDate ?? null;
56
77
  const text = (value) => (value ?? '').toString().toLowerCase();
57
78
  const KEYS = {
58
79
  due: dayOf,
80
+ ordered: (job) => job.orderedOn ?? null,
81
+ shipped: (job) => job.shippedOn ?? null,
59
82
  client: (job) => text(job.clientName) || null,
60
83
  practitioner: (job) => text(job.practitioner) || null,
61
84
  total: (job) => job.totalCents ?? null,