@arcforgelabs/flow 2026.10.43 → 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/release-channel.js +5 -2
- package/server/ui/app.js +11 -172
- package/server/ui/columns.js +268 -0
- package/server/ui/flow.css +203 -30
- package/server/ui/invoice-rules.js +3 -3
- package/server/ui/invoicing-settings.js +52 -29
- package/server/ui/invoicing.js +190 -69
- package/server/ui/job-view.js +255 -60
- package/server/ui/jobs.js +133 -137
- package/server/ui/kit.js +278 -0
- package/server/ui/list-views.js +73 -0
- package/server/ui/person.js +29 -0
- 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/view-bar.js +176 -70
package/server/ui/flow.css
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
:root {
|
|
9
9
|
--fx-line: color-mix(in srgb, var(--t-text) 12%, transparent);
|
|
10
|
+
/* The side space of every page. Pages use the whole width with this slim
|
|
11
|
+
edge, as ClickUp and HubSpot do; the title, toolbar and content start at
|
|
12
|
+
one place on every page. */
|
|
13
|
+
--fx-gutter: clamp(16px, 2vw, 24px);
|
|
10
14
|
--fx-line-soft: color-mix(in srgb, var(--t-text) 7%, transparent);
|
|
11
15
|
--fx-hover: color-mix(in srgb, var(--t-text) 5%, transparent);
|
|
12
16
|
--fx-focus: 0 0 0 3px color-mix(in srgb, var(--t-ring) 40%, transparent);
|
|
@@ -90,6 +94,9 @@ body:is([data-state="off"], [data-state="error"], [data-state="no-access"]) .fx-
|
|
|
90
94
|
}
|
|
91
95
|
.fx-input:hover, .fx-select:hover { border-color: var(--t-text); }
|
|
92
96
|
.fx-input:focus, .fx-select:focus { outline: none; border-color: var(--t-text); box-shadow: var(--fx-focus); }
|
|
97
|
+
.fx-input[aria-invalid="true"], .fx-select[aria-invalid="true"] {
|
|
98
|
+
border-color: var(--t-danger); box-shadow: 0 0 0 3px color-mix(in srgb, var(--t-danger) 28%, transparent);
|
|
99
|
+
}
|
|
93
100
|
.fx-select {
|
|
94
101
|
width: auto; min-width: 112px; padding-right: 30px; appearance: none; cursor: pointer;
|
|
95
102
|
background-image: linear-gradient(45deg, transparent 50%, var(--t-dim) 50%), linear-gradient(135deg, var(--t-dim) 50%, transparent 50%);
|
|
@@ -167,7 +174,7 @@ body:is([data-state="off"], [data-state="error"], [data-state="no-access"]) .fx-
|
|
|
167
174
|
.fx-shell[data-folded] #user-button .cl-userButtonTrigger { width: auto; padding: 0; border-radius: 50%; background: transparent; box-shadow: none; }
|
|
168
175
|
.fx-shell[data-folded] #user-button .cl-userButtonOuterIdentifier, .fx-shell[data-folded] #user-button .cl-userButtonBox::before { display: none; }
|
|
169
176
|
}
|
|
170
|
-
.fx-main { flex: 1; min-width: 0;
|
|
177
|
+
.fx-main { flex: 1; min-width: 0; padding: 32px var(--fx-gutter) 64px; display: grid; gap: 20px; align-content: start; }
|
|
171
178
|
.fx-main:focus { outline: none; box-shadow: none; }
|
|
172
179
|
|
|
173
180
|
/* Page header: the title only (no line under it), then the actions,
|
|
@@ -331,6 +338,25 @@ a.fx-link:hover, a.fx-link:focus-visible { border-color: var(--t-strong); }
|
|
|
331
338
|
background: #fff; box-shadow: inset 0 0 0 1px var(--fx-line);
|
|
332
339
|
}
|
|
333
340
|
.fx-logo--icon { background: var(--t-accent-bg); color: var(--t-accent); box-shadow: none; }
|
|
341
|
+
.fx-tiles { display: grid; gap: 12px; }
|
|
342
|
+
.fx-tiles + .fx-tiles { margin-top: 16px; }
|
|
343
|
+
.fx-tiles__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
344
|
+
.fx-tiles__head h2 { color: var(--t-strong); font: 600 15px/1.3 var(--t-body); }
|
|
345
|
+
.fx-tiles__more { display: inline-flex; align-items: center; gap: 4px; color: var(--t-dim); font-size: 13.5px; text-decoration: none; }
|
|
346
|
+
.fx-tiles__more:hover, .fx-tiles__more:focus-visible { color: var(--t-strong); }
|
|
347
|
+
.fx-tiles__more .fx-lucide { width: 15px; height: 15px; }
|
|
348
|
+
.fx-tiles__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr)); gap: 12px; }
|
|
349
|
+
.fx-tile {
|
|
350
|
+
display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 14px; align-items: center; padding: 12px 14px 12px 12px;
|
|
351
|
+
border: 1px solid var(--fx-line); border-radius: var(--t-radius-lg); background: var(--t-paper); color: var(--t-text); text-decoration: none;
|
|
352
|
+
transition: border-color 140ms var(--fx-ease), background-color 140ms var(--fx-ease);
|
|
353
|
+
}
|
|
354
|
+
.fx-tile:hover, .fx-tile:focus-visible { border-color: var(--t-control); background: var(--fx-hover); }
|
|
355
|
+
.fx-tile .fx-logo { width: 48px; height: 48px; border-radius: var(--t-radius); }
|
|
356
|
+
.fx-tile .fx-logo--icon { background: var(--fx-hover); color: var(--t-strong); box-shadow: inset 0 0 0 1px var(--fx-line); }
|
|
357
|
+
.fx-tile h3 { overflow: hidden; color: var(--t-strong); font: 600 14.5px/1.35 var(--t-body); text-overflow: ellipsis; white-space: nowrap; }
|
|
358
|
+
.fx-tile__meta { color: var(--t-dim); font-size: 12.5px; white-space: nowrap; }
|
|
359
|
+
.fx-logo--icon .fx-lucide { width: 22px; height: 22px; }
|
|
334
360
|
.fx-logo img { width: 28px; height: 28px; object-fit: contain; }
|
|
335
361
|
.fx-logo--lg { width: 52px; height: 52px; }
|
|
336
362
|
.fx-logo--lg img { width: 34px; height: 34px; }
|
|
@@ -407,6 +433,28 @@ a.fx-strong:hover { text-decoration: underline; }
|
|
|
407
433
|
.fx-tabs__count { padding: 0 7px; border-radius: 999px; background: var(--fx-hover); color: var(--t-dim); font-size: 12px; line-height: 20px; }
|
|
408
434
|
.fx-tabs button[data-tone="danger"] .fx-tabs__count { background: var(--t-danger-bg); color: var(--t-danger); }
|
|
409
435
|
|
|
436
|
+
/* Saved views (saved-views.js): the row of named view tabs over a list, as
|
|
437
|
+
ClickUp's. The chosen tab is underlined; its ⋯ shows on hover and when
|
|
438
|
+
chosen. Save and Revert sit at the right while the list differs. */
|
|
439
|
+
.fx-views { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; min-height: 32px; margin-bottom: -8px; }
|
|
440
|
+
.fx-views:empty { display: none; }
|
|
441
|
+
.fx-views__item { display: inline-flex; align-items: center; border-bottom: 2px solid transparent; }
|
|
442
|
+
.fx-views__item[data-on="true"] { border-bottom-color: var(--t-brand); }
|
|
443
|
+
.fx-views__tab { display: inline-flex; align-items: center; gap: 6px; max-width: 220px; padding: 6px 4px 6px 8px; border: 0; background: none; color: var(--t-dim); font: 600 13px var(--t-body); cursor: pointer; }
|
|
444
|
+
.fx-views__tab:hover, .fx-views__item[data-on="true"] .fx-views__tab { color: var(--t-strong); }
|
|
445
|
+
.fx-views__tab:focus-visible { outline: 2px solid var(--t-ring, var(--t-accent)); outline-offset: 1px; }
|
|
446
|
+
.fx-views__tab .fx-lucide { width: 14px; height: 14px; flex: none; }
|
|
447
|
+
.fx-views__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
448
|
+
.fx-views__dot { flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--t-accent); }
|
|
449
|
+
.fx-views__item .fx-cfm__more { width: 22px; height: 22px; opacity: 0; }
|
|
450
|
+
.fx-views__item:hover .fx-cfm__more, .fx-views__item[data-on="true"] .fx-cfm__more, .fx-views__item .fx-cfm__more[aria-expanded="true"], .fx-views__item .fx-cfm__more:focus-visible { opacity: 1; }
|
|
451
|
+
.fx-views__item .fx-cfm__more .fx-lucide { width: 14px; height: 14px; }
|
|
452
|
+
.fx-views__add { display: inline-flex; align-items: center; gap: 4px; padding: 6px 8px; border: 0; background: none; color: var(--t-dim); font: 500 13px var(--t-body); cursor: pointer; }
|
|
453
|
+
.fx-views__add:hover { color: var(--t-strong); }
|
|
454
|
+
.fx-views__add .fx-lucide { width: 14px; height: 14px; }
|
|
455
|
+
.fx-views__end { display: inline-flex; gap: 6px; margin-left: auto; }
|
|
456
|
+
@media (hover: none) { .fx-views__item .fx-cfm__more { opacity: 1; } }
|
|
457
|
+
|
|
410
458
|
/* Quiet alerts (server/ui/alerts.js): one slim line under the page header,
|
|
411
459
|
only while something on the page needs this person. */
|
|
412
460
|
.fx-alerts { display: flex; align-items: center; gap: 12px; margin: 0 0 12px; padding: 6px 6px 6px 14px; border: 1px solid var(--fx-line); border-left: 3px solid var(--t-warn); border-radius: var(--t-radius); background: var(--t-paper); font-size: 14px; }
|
|
@@ -492,8 +540,6 @@ a.fx-strong:hover { text-decoration: underline; }
|
|
|
492
540
|
}
|
|
493
541
|
|
|
494
542
|
/* ── Jobs board ─────────────────────────────────────────────────────────── */
|
|
495
|
-
/* Every page shares .fx-main's width; only the board's lanes use the whole window. */
|
|
496
|
-
.fx-main--board { max-width: none; }
|
|
497
543
|
.fx-main > * { min-width: 0; }
|
|
498
544
|
.fx-toolbar--plain { border-bottom: 0; align-items: center; }
|
|
499
545
|
.fx-toolbar--plain .fx-search { margin-bottom: 0; }
|
|
@@ -544,14 +590,16 @@ a.fx-strong:hover { text-decoration: underline; }
|
|
|
544
590
|
.fx-chipset--on .fx-chip__clear { border-color: var(--t-accent-brd, var(--t-accent)); background: var(--t-accent-bg); color: var(--t-accent); }
|
|
545
591
|
/* Group by, Sort and Filters: rows of selects with a bin at the end, as
|
|
546
592
|
ClickUp lays them out, in Flow's own look. */
|
|
547
|
-
.fx-pop.fx-pop--rows { width: max-content; min-width: 360px; max-width: min(
|
|
593
|
+
.fx-pop.fx-pop--rows { width: max-content; min-width: 360px; max-width: min(760px, calc(100vw - 16px)); max-height: min(520px, calc(100vh - 32px)); padding: 12px; }
|
|
548
594
|
.fx-rows__title { margin: 0 0 10px; color: var(--t-strong); font: 600 14px/1.3 var(--t-body); }
|
|
549
595
|
.fx-rows { display: grid; gap: 8px; }
|
|
550
|
-
.fx-rows__row { display: grid; grid-template-columns:
|
|
596
|
+
.fx-rows__row { display: grid; grid-template-columns: 64px minmax(140px, 1fr) minmax(120px, auto) minmax(170px, 1.3fr) 32px; gap: 8px; align-items: start; }
|
|
551
597
|
.fx-rows__row--sort { grid-template-columns: 78px minmax(150px, 1fr) minmax(140px, auto) 32px; }
|
|
552
598
|
.fx-rows__row--group { grid-template-columns: minmax(170px, 1fr) minmax(130px, auto) 32px; }
|
|
553
599
|
.fx-rows .fx-select, .fx-rows .fx-input { width: 100%; min-width: 0; min-height: 32px; font-size: 13px; }
|
|
554
|
-
|
|
600
|
+
/* Rows align at the top, so a value on two lines (a date range) keeps the
|
|
601
|
+
bin beside its first line. */
|
|
602
|
+
.fx-rows__lead { display: flex; align-items: center; min-height: 32px; color: var(--t-dim); font: 500 13px/1.2 var(--t-body); }
|
|
555
603
|
.fx-rows__value { overflow: hidden; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
|
|
556
604
|
.fx-rows__gone { grid-column: span 3; color: var(--t-dim); font-size: 13px; font-style: italic; }
|
|
557
605
|
.fx-rows__bin {
|
|
@@ -565,13 +613,51 @@ a.fx-strong:hover { text-decoration: underline; }
|
|
|
565
613
|
.fx-rows__foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 12px; }
|
|
566
614
|
.fx-rows__add { display: inline-flex; align-items: center; gap: 6px; }
|
|
567
615
|
.fx-rows__add .fx-lucide { width: 14px; height: 14px; }
|
|
616
|
+
/* The filter panel's groups, as ClickUp's: each group a shaded box with its
|
|
617
|
+
own rows and "Add nested filter"; groups stack with And / Or at the left. */
|
|
618
|
+
.fx-fgroup { display: grid; gap: 8px; padding: 10px; border-radius: var(--t-radius-lg); background: var(--fx-hover); }
|
|
619
|
+
.fx-fgroups { display: grid; gap: 8px; }
|
|
620
|
+
.fx-fgroups__row { display: grid; grid-template-columns: 64px minmax(0, 1fr); gap: 8px; align-items: start; }
|
|
621
|
+
.fx-fgroups__row > .fx-rows__lead, .fx-fgroups__row > span { padding-top: 16px; }
|
|
622
|
+
.fx-fgroup__add { justify-self: start; padding: 2px 4px; border: 0; background: none; color: var(--t-dim); font: 500 12.5px/1.3 var(--t-body); cursor: pointer; }
|
|
623
|
+
.fx-fgroup__add:hover, .fx-fgroup__add:focus-visible { color: var(--t-strong); text-decoration: underline; outline: none; }
|
|
624
|
+
.fx-choice { display: inline-flex; align-items: center; gap: 6px; min-width: 0; text-align: left; }
|
|
625
|
+
.fx-choice .fx-lucide { flex: none; width: 14px; height: 14px; color: var(--t-dim); }
|
|
626
|
+
.fx-choice__text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
627
|
+
.fx-rows .fx-rows__join { min-width: 0; width: 100%; padding-left: 8px; font: 600 12px/1 var(--t-data, var(--t-body)); letter-spacing: .04em; }
|
|
628
|
+
.fx-rows__word { font: 600 12px/1 var(--t-data, var(--t-body)); letter-spacing: .04em; padding-left: 9px; }
|
|
629
|
+
.fx-fgroups__row .fx-rows__join { min-width: 0; width: 64px; padding-left: 8px; font: 600 12px/1 var(--t-data, var(--t-body)); letter-spacing: .04em; min-height: 32px; }
|
|
630
|
+
.fx-rows__date { display: flex; flex-wrap: wrap; gap: 6px; overflow: visible; }
|
|
631
|
+
.fx-rows__date > .fx-choice { flex: 1 1 130px; }
|
|
632
|
+
.fx-rows__date .fx-input { width: auto; flex: 1 1 130px; }
|
|
633
|
+
.fx-rows__days { display: inline-flex; flex: 1 1 130px; align-items: center; gap: 6px; }
|
|
634
|
+
.fx-rows__days .fx-input[type="number"] { width: 72px; flex: none; }
|
|
635
|
+
.fx-pop__bar { display: flex; align-items: center; justify-content: space-between; }
|
|
636
|
+
.fx-pop__link { padding: 4px 8px; border: 0; background: none; color: var(--t-accent); font: 500 12.5px/1.3 var(--t-body); cursor: pointer; }
|
|
637
|
+
.fx-pop__link:hover, .fx-pop__link:focus-visible { text-decoration: underline; outline: none; }
|
|
568
638
|
.fx-pop.fx-pop--choices { min-width: 200px; max-width: 300px; padding: 6px; }
|
|
639
|
+
/* A value list row, as ClickUp's: mark (when there is one), name, tick box
|
|
640
|
+
at the right. No empty space at the left. */
|
|
641
|
+
.fx-pop__item--check { gap: 8px; }
|
|
642
|
+
.fx-pop__item--check .fx-pop__text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
643
|
+
.fx-pop__box { display: inline-grid; place-items: center; flex: none; width: 16px; height: 16px; margin-left: auto; border: 1px solid var(--t-control); border-radius: var(--t-radius); color: var(--t-on-primary); }
|
|
644
|
+
.fx-pop__item--check[aria-checked="true"] .fx-pop__box { border-color: var(--t-primary); background: var(--t-primary); }
|
|
645
|
+
.fx-pop__box svg { width: 12px; height: 12px; }
|
|
646
|
+
.fx-dot { flex: none; width: 9px; height: 9px; border-radius: 50%; background: var(--cf, var(--t-dim)); }
|
|
647
|
+
.fx-dot--stage { --stage: var(--t-accent); background: var(--stage); }
|
|
648
|
+
.fx-dot--stage[data-rank="0"] { --stage: var(--t-mute); }
|
|
649
|
+
.fx-dot--stage[data-rank="3"], .fx-dot--stage[data-rank="4"] { --stage: color-mix(in srgb, var(--t-accent) 45%, var(--t-success)); }
|
|
650
|
+
.fx-dot--stage[data-rank="5"], .fx-dot--stage[data-rank="6"] { --stage: var(--t-success); }
|
|
651
|
+
.fx-dot--stage[data-rank="7"] { --stage: var(--t-dim); }
|
|
569
652
|
.fx-pop--choices .fx-input { min-height: 32px; margin-bottom: 4px; font-size: 13px; }
|
|
570
653
|
.fx-pop__end { display: inline-grid; margin-left: auto; color: var(--t-accent); }
|
|
571
654
|
@media (max-width: 640px) {
|
|
572
655
|
.fx-pop.fx-pop--rows { min-width: 0; width: calc(100vw - 16px); }
|
|
573
656
|
.fx-rows__row, .fx-rows__row--sort { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 32px; }
|
|
574
|
-
.fx-rows__row > .fx-rows__lead, .fx-rows__row > .fx-select:first-child { grid-column: 1 / -1; }
|
|
657
|
+
.fx-rows__row > .fx-rows__lead, .fx-rows__row > .fx-select:first-child, .fx-rows__row > .fx-rows__join { grid-column: 1 / -1; }
|
|
658
|
+
.fx-rows__row > .fx-rows__join { width: 72px; }
|
|
659
|
+
.fx-fgroups__row { grid-template-columns: minmax(0, 1fr); }
|
|
660
|
+
.fx-fgroups__row > .fx-rows__lead, .fx-fgroups__row > span { padding-top: 0; }
|
|
575
661
|
.fx-rows__row > .fx-rows__value { grid-column: 1 / 3; }
|
|
576
662
|
.fx-rows__row > .fx-rows__value--none { display: none; }
|
|
577
663
|
}
|
|
@@ -610,6 +696,42 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
|
|
|
610
696
|
.fx-th-sort .fx-lucide { width: 13px; height: 13px; }
|
|
611
697
|
.fx-th-sort__n { font: 700 10px/1 var(--t-body); }
|
|
612
698
|
@media (hover: none) { .fx-th-sort { opacity: 1; } }
|
|
699
|
+
/* A sized list (columns.js): each column has its width, the table is as
|
|
700
|
+
wide as its columns (never stretched; space left over stays empty) and
|
|
701
|
+
scrolls sideways past the page. A head's right
|
|
702
|
+
edge is a grip that resizes the column; a head drags to move it, and a
|
|
703
|
+
line shows where it lands. */
|
|
704
|
+
.fx-table-scroll { overflow-x: auto; scrollbar-width: thin; }
|
|
705
|
+
.fx-table.fx-table--sized { table-layout: fixed; }
|
|
706
|
+
.fx-table--sized th, .fx-table--sized td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
707
|
+
.fx-table--sized th { position: relative; }
|
|
708
|
+
/* Every cell is one line: a long value ends in "…" and the row keeps its
|
|
709
|
+
height, as HubSpot's. A wrapper in a cell (a client with its warning
|
|
710
|
+
mark, a stage pill) shrinks its text, never its mark. */
|
|
711
|
+
.fx-table--sized td > * { max-width: 100%; }
|
|
712
|
+
.fx-table--sized .fx-job__cell { display: flex; min-width: 0; }
|
|
713
|
+
.fx-table--sized .fx-job__cell > * { flex: none; }
|
|
714
|
+
.fx-table--sized .fx-job__cell > .fx-strong,
|
|
715
|
+
.fx-table--sized .fx-status { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
716
|
+
.fx-table--sized .fx-job__cell > .fx-strong { flex: 0 1 auto; }
|
|
717
|
+
.fx-table--sized .fx-th-cell { max-width: 100%; }
|
|
718
|
+
.fx-table--sized .fx-th { overflow: hidden; text-overflow: ellipsis; }
|
|
719
|
+
.fx-th--drag .fx-th { cursor: grab; touch-action: none; }
|
|
720
|
+
.fx-th--drag[data-dragging] { opacity: .5; }
|
|
721
|
+
.fx-th--drag[data-dragging] .fx-th { cursor: grabbing; }
|
|
722
|
+
.fx-table--sized th[data-drop]::after { content: ""; position: absolute; top: 6px; bottom: 6px; width: 2px; border-radius: 1px; background: var(--t-accent); }
|
|
723
|
+
.fx-table--sized th[data-drop="before"]::after { left: 0; }
|
|
724
|
+
.fx-table--sized th[data-drop="after"]::after { right: 0; }
|
|
725
|
+
.fx-th-grip { position: absolute; top: 0; right: 0; bottom: 0; width: 8px; cursor: col-resize; touch-action: none; }
|
|
726
|
+
.fx-th-grip::after { content: ""; position: absolute; top: 25%; bottom: 25%; right: 3px; width: 2px; border-radius: 1px; background: transparent; }
|
|
727
|
+
th:hover > .fx-th-grip::after { background: var(--t-brd-hi); }
|
|
728
|
+
.fx-th-grip:hover::after, .fx-th-grip:focus-visible::after, .fx-table--sized[data-resizing] .fx-th-grip:active::after { background: var(--t-accent); }
|
|
729
|
+
.fx-th-grip:focus-visible { outline: none; }
|
|
730
|
+
.fx-table--sized[data-resizing] { cursor: col-resize; user-select: none; }
|
|
731
|
+
.fx-table--sized .fx-tick-cell, .fx-table--sized .fx-th-add, .fx-table--sized .fx-c-end { overflow: visible; text-overflow: clip; }
|
|
732
|
+
.fx-table--sized .fx-tick-cell { padding-right: 0; }
|
|
733
|
+
/* + stays at the right edge while the table scrolls sideways. */
|
|
734
|
+
.fx-table--sized .fx-th-add { position: sticky; right: 0; z-index: 1; padding-left: 0; padding-right: 12px; }
|
|
613
735
|
.fx-avatar--sm { width: 24px; height: 24px; font-size: 10px; }
|
|
614
736
|
.fx-c-date time { font-variant-numeric: tabular-nums; }
|
|
615
737
|
.fx-stage[data-rank="plain"] { --stage: var(--t-dim); }
|
|
@@ -624,9 +746,41 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
|
|
|
624
746
|
.fx-board__head { display: flex; flex: none; align-items: center; gap: 8px; min-width: 0; padding: 2px 4px 4px 2px; }
|
|
625
747
|
.fx-board__sum { flex: none; margin-left: auto; color: var(--t-dim); font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
626
748
|
.fx-board__cards { display: grid; gap: 8px; align-content: start; min-height: 0; max-height: 70vh; padding-right: 2px; overflow-y: auto; scrollbar-width: thin; }
|
|
749
|
+
/* listFrame() in kit.js: the window of a list or board (Jobs, Invoices,
|
|
750
|
+
Reconcile). Its body scrolls both ways; the foot holds the count. A table
|
|
751
|
+
or panel inside drops its own frame. On a wide, tall screen the page is
|
|
752
|
+
the window's height and does not scroll itself: the frame fills it, and
|
|
753
|
+
on the board each lane scrolls inside itself. */
|
|
754
|
+
.fx-frame { display: flex; flex-direction: column; min-width: 0; border: 1px solid var(--fx-line); border-radius: var(--t-radius-lg); background: var(--t-paper); box-shadow: var(--t-shadow); overflow: hidden; }
|
|
755
|
+
.fx-frame__view { display: flex; flex-direction: column; min-height: 0; }
|
|
756
|
+
.fx-frame__body { min-height: 0; overflow: auto; scrollbar-width: thin; }
|
|
757
|
+
/* edgeFade() in kit.js: a soft fade over each edge with more content past
|
|
758
|
+
it, in the colour under the rows (--fx-fade). */
|
|
759
|
+
.fx-fades { position: relative; --fx-fade: var(--t-paper); }
|
|
760
|
+
.fx-fade { position: absolute; z-index: 3; pointer-events: none; opacity: 0; transition: opacity .15s; }
|
|
761
|
+
.fx-fade[data-edge="top"] { inset: 0 0 auto; height: 24px; background: linear-gradient(to bottom, var(--fx-fade), transparent); }
|
|
762
|
+
.fx-fade[data-edge="bottom"] { inset: auto 0 0; height: 24px; background: linear-gradient(to top, var(--fx-fade), transparent); }
|
|
763
|
+
.fx-fade[data-edge="left"] { inset: 0 auto 0 0; width: 24px; background: linear-gradient(to right, var(--fx-fade), transparent); }
|
|
764
|
+
.fx-fade[data-edge="right"] { inset: 0 0 0 auto; width: 24px; background: linear-gradient(to left, var(--fx-fade), transparent); }
|
|
765
|
+
.fx-fades[data-fade~="top"] > .fx-fade[data-edge="top"], .fx-fades[data-fade~="bottom"] > .fx-fade[data-edge="bottom"],
|
|
766
|
+
.fx-fades[data-fade~="left"] > .fx-fade[data-edge="left"], .fx-fades[data-fade~="right"] > .fx-fade[data-edge="right"] { opacity: 1; }
|
|
767
|
+
.fx-main--board .fx-frame__view { --fx-fade: var(--t-bg); }
|
|
768
|
+
.fx-board__col.fx-fades { --fx-fade: var(--t-s1); }
|
|
769
|
+
.fx-board__col > .fx-fade[data-edge="bottom"] { border-radius: 0 0 var(--t-radius-lg) var(--t-radius-lg); }
|
|
770
|
+
.fx-frame__body .fx-panel { border: 0; border-radius: 0; box-shadow: none; overflow: visible; }
|
|
771
|
+
.fx-frame__body .fx-empty { padding: 24px; }
|
|
772
|
+
.fx-frame .fx-table-scroll { overflow: visible; }
|
|
773
|
+
.fx-frame thead th { position: sticky; top: 0; z-index: 2; }
|
|
774
|
+
.fx-frame__foot { display: flex; flex: none; align-items: center; justify-content: space-between; gap: 12px; min-height: 44px; padding: 6px 12px; border-top: 1px solid var(--fx-line); color: var(--t-dim); font-size: 13px; font-variant-numeric: tabular-nums; }
|
|
775
|
+
.fx-frame__tools { display: flex; align-items: center; gap: 8px; }
|
|
776
|
+
.fx-main--board .fx-frame__body { padding: 12px; background: var(--t-bg); }
|
|
627
777
|
@media (min-width: 821px) and (min-height: 560px) {
|
|
628
|
-
.fx-main--
|
|
629
|
-
.fx-main--
|
|
778
|
+
.fx-main--frame { height: 100vh; height: 100dvh; padding-bottom: 16px; display: flex; flex-direction: column; }
|
|
779
|
+
.fx-main--frame > * { flex: none; }
|
|
780
|
+
.fx-main--frame > .fx-frame { flex: 1; min-height: 0; }
|
|
781
|
+
.fx-main--frame .fx-frame__view, .fx-main--frame .fx-frame__body { flex: 1; }
|
|
782
|
+
.fx-main--board .fx-frame__body { display: flex; overflow: hidden; }
|
|
783
|
+
.fx-main--board .fx-jobs { flex: 1; min-width: 0; min-height: 0; }
|
|
630
784
|
.fx-main--board .fx-board { height: 100%; overflow-y: hidden; align-items: stretch; }
|
|
631
785
|
.fx-main--board .fx-board__cards { flex: 1; max-height: none; }
|
|
632
786
|
}
|
|
@@ -695,9 +849,9 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
|
|
|
695
849
|
.fx-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; color: var(--t-dim); overflow-wrap: anywhere; }
|
|
696
850
|
.fx-dialog.fx-task { width: min(1040px, calc(100vw - 32px)); max-height: calc(100vh - 48px); overflow-y: auto; }
|
|
697
851
|
.fx-task__grid { display: grid; grid-template-columns: minmax(0, 1fr) 300px; }
|
|
698
|
-
/* No sidecar: the main area takes the room.
|
|
852
|
+
/* No sidecar: the main area takes the room. Fix is wider for the change table. */
|
|
699
853
|
.fx-task__grid:has(> .fx-sidecar[hidden]) { grid-template-columns: minmax(0, 1fr); }
|
|
700
|
-
.fx-task__grid:has(> .fx-sidecar [data-tab="
|
|
854
|
+
.fx-task__grid:has(> .fx-sidecar [data-tab="fix"][aria-selected="true"]) { grid-template-columns: minmax(0, 1fr) 400px; }
|
|
701
855
|
.fx-task__side { background: var(--t-s1); }
|
|
702
856
|
.fx-task__tools { display: flex; align-items: center; gap: 4px; }
|
|
703
857
|
.fx-task__tools .fx-lucide { width: 16px; height: 16px; }
|
|
@@ -715,16 +869,22 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
|
|
|
715
869
|
.fx-task__late { color: var(--t-danger) !important; }
|
|
716
870
|
.fx-task__link { color: var(--t-accent); text-decoration: none; border-bottom: 1px solid color-mix(in srgb, var(--t-accent) 40%, transparent); }
|
|
717
871
|
.fx-task__link:hover { border-bottom-color: currentColor; }
|
|
718
|
-
.fx-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
.fx-
|
|
722
|
-
.fx-
|
|
723
|
-
.fx-
|
|
724
|
-
.fx-
|
|
725
|
-
.fx-
|
|
726
|
-
|
|
727
|
-
.fx-
|
|
872
|
+
.fx-task__lines h3 { margin: 0; color: var(--t-dim); font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
|
|
873
|
+
/* What to fix: one toggle chip a problem beside the stage; its problem opens
|
|
874
|
+
in the sidecar's Fix tab (jobs.js openJob). One layout with or without. */
|
|
875
|
+
.fx-task__stage { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
|
876
|
+
.fx-task__fixchip { all: unset; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; height: 24px; padding: 0 10px; border: 1px solid var(--t-warn-brd, var(--t-warn)); border-radius: 999px; background: var(--t-warn-bg); color: var(--t-warn); font-size: 12.5px; font-weight: 600; }
|
|
877
|
+
.fx-task__fixchip .fx-lucide { width: 14px; height: 14px; }
|
|
878
|
+
.fx-task__fixchip:hover { border-color: var(--t-warn); }
|
|
879
|
+
.fx-task__fixchip:focus-visible { box-shadow: var(--fx-focus); }
|
|
880
|
+
/* Pressed: its problem is open in the sidecar's Fix tab. */
|
|
881
|
+
.fx-task__fixchip[aria-pressed="true"] { border-color: var(--t-warn); background: var(--t-warn); color: var(--t-paper); }
|
|
882
|
+
.fx-task__fixitem { display: grid; gap: 8px; justify-items: start; font-size: 13.5px; }
|
|
883
|
+
.fx-task__fixitem > strong { color: var(--t-strong); }
|
|
884
|
+
.fx-task__fixitem > p { margin: 0; }
|
|
885
|
+
.fx-task__fixitem > .fx-task__review { justify-self: stretch; }
|
|
886
|
+
.fx-task__fixgo .fx-lucide { width: 14px; height: 14px; }
|
|
887
|
+
.fx-task__fixgo { display: inline-flex; align-items: center; gap: 6px; }
|
|
728
888
|
.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; }
|
|
729
889
|
.fx-task__choose > svg { flex: none; width: 20px; height: 20px; color: var(--t-warn); }
|
|
730
890
|
.fx-task__choose > span { flex: 1 1 200px; }
|
|
@@ -736,7 +896,6 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
|
|
|
736
896
|
invoice opened from Invoices (the pager on top). */
|
|
737
897
|
.fx-rec { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
|
|
738
898
|
.fx-rec .fx-lucide { width: 16px; height: 16px; }
|
|
739
|
-
.fx-rec__gap { max-width: 1180px; }
|
|
740
899
|
.fx-rec__gap .fx-toolbar .fx-rows__title { margin: 0; }
|
|
741
900
|
.fx-rec__gap .fx-ph__actions .fx-btn, .fx-rec__acts .fx-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; }
|
|
742
901
|
.fx-rec__results { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
|
|
@@ -804,7 +963,7 @@ th:hover .fx-th-sort, .fx-th-sort:focus-visible, .fx-th-sort--on { opacity: 1; }
|
|
|
804
963
|
.fx-task__grid { grid-template-columns: minmax(0, 1fr); }
|
|
805
964
|
.fx-task__main { padding: 16px; }
|
|
806
965
|
.fx-task__fields { grid-template-columns: minmax(0, 1fr); }
|
|
807
|
-
.fx-task__grid:has(> .fx-sidecar [data-tab="
|
|
966
|
+
.fx-task__grid:has(> .fx-sidecar [data-tab="fix"][aria-selected="true"]) { grid-template-columns: minmax(0, 1fr); }
|
|
808
967
|
.fx-task__side { border-top: 1px solid var(--fx-line); border-left: 0; }
|
|
809
968
|
}
|
|
810
969
|
|
|
@@ -945,6 +1104,8 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
945
1104
|
/* ── Narrow screens: the sidebar becomes a bar across the top. ──────────── */
|
|
946
1105
|
@media (max-width: 820px) {
|
|
947
1106
|
.fx-shell { flex-direction: column; }
|
|
1107
|
+
/* In a column, the page takes the full width, not its content's. */
|
|
1108
|
+
.fx-main { width: 100%; }
|
|
948
1109
|
.fx-side { flex-direction: row; align-items: center; width: auto; height: 60px; padding: 0 12px; border-right: 0; border-bottom: 1px solid var(--fx-line); }
|
|
949
1110
|
.fx-side__head { flex-direction: row-reverse; gap: 6px; padding: 0; }
|
|
950
1111
|
.fx-side__head .fx-brand { display: none; }
|
|
@@ -987,6 +1148,9 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
987
1148
|
.fx-main { padding-top: 20px; }
|
|
988
1149
|
.fx-title { font-size: 22px; }
|
|
989
1150
|
.fx-table thead { display: none; }
|
|
1151
|
+
.fx-table.fx-table--sized { width: 100% !important; min-width: 0; table-layout: auto; }
|
|
1152
|
+
.fx-table--sized td { white-space: normal; }
|
|
1153
|
+
.fx-table--sized colgroup { display: none; }
|
|
990
1154
|
.fx-table, .fx-table tbody, .fx-table tr, .fx-table td { display: block; width: 100%; }
|
|
991
1155
|
.fx-table tr { padding: 12px 14px; border-bottom: 1px solid var(--fx-line-soft); }
|
|
992
1156
|
.fx-table td { padding: 4px 0; border: 0; }
|
|
@@ -1014,7 +1178,7 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1014
1178
|
.fx-table--jobs tr.fx-row-link > td.fx-c-client { grid-area: 1 / 2; }
|
|
1015
1179
|
.fx-table--jobs tr.fx-row-link > td.fx-c-total { grid-area: 1 / 3; }
|
|
1016
1180
|
.fx-table--jobs td.fx-c-end { display: none; }
|
|
1017
|
-
.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; }
|
|
1181
|
+
.fx-table--jobs tr.fx-row-link > td[data-label]:not(.fx-c-client, .fx-c-total, .fx-c-stage)::before { content: attr(data-label) " "; color: var(--t-dim); font-size: 12px; }
|
|
1018
1182
|
.fx-table--jobs tr.fx-table__group { grid-template-columns: 28px minmax(0, 1fr); background: var(--t-s1); }
|
|
1019
1183
|
.fx-table--jobs tr.fx-table__group > td.fx-tick-cell { grid-row: 1; align-self: center; padding: 0; }
|
|
1020
1184
|
.fx-table--jobs tr.fx-table__group > td { background: none; }
|
|
@@ -1217,7 +1381,7 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1217
1381
|
.fx-cfs__where .fx-lucide { width: 16px; height: 16px; color: var(--t-dim); }
|
|
1218
1382
|
.fx-cfs__foot { display: flex; align-items: center; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--fx-line-soft); }
|
|
1219
1383
|
.fx-cfs__delete { color: var(--t-danger); background: color-mix(in srgb, var(--t-danger) 12%, transparent); }
|
|
1220
|
-
.fx-cfs__delete:hover { background: color-mix(in srgb, var(--t-danger)
|
|
1384
|
+
.fx-cfs__delete:hover { background: color-mix(in srgb, var(--t-danger) 28%, transparent); }
|
|
1221
1385
|
.fx-cfs__delete .fx-lucide { width: 16px; height: 16px; }
|
|
1222
1386
|
@media (max-width: 720px) {
|
|
1223
1387
|
.fx-cfm { grid-template-columns: minmax(0, 1fr) 36px 32px; column-gap: 8px; }
|
|
@@ -1314,12 +1478,21 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1314
1478
|
/* The Invoices list head (invoicing.js): tabs, search and the send bar stay
|
|
1315
1479
|
at the top while rows scroll up under them; a shadow shows once they do.
|
|
1316
1480
|
Narrow, the app bar (60px) sits above it. */
|
|
1317
|
-
|
|
1481
|
+
/* listHead() in kit.js: the toolbar row of every list. One height, with
|
|
1482
|
+
chips, tabs or a Board/List switch in it, so the rows start at one place. */
|
|
1483
|
+
.fx-listhead > .fx-toolbar { min-height: 40px; }
|
|
1484
|
+
.fx-listhead .fx-toolbar__end { flex: 1 1 auto; }
|
|
1485
|
+
.fx-listhead > .fx-toolbar--plain { align-items: center; }
|
|
1486
|
+
.fx-listhead[data-sticky] { position: sticky; top: 0; z-index: 5; margin: -12px calc(-1 * var(--fx-gutter)) 0; padding: 12px var(--fx-gutter) 0; background: var(--t-bg); transition: box-shadow .15s; }
|
|
1318
1487
|
.fx-listhead[data-stuck] { box-shadow: 0 10px 12px -12px rgba(0, 0, 0, .45); }
|
|
1319
|
-
|
|
1320
|
-
|
|
1488
|
+
/* The edge tells listHead when the rows reach the toolbar. In the page's
|
|
1489
|
+
grid it takes a row and a gap of its own, so the toolbar moves up by both. */
|
|
1490
|
+
.fx-listhead__edge { height: 0; }
|
|
1491
|
+
.fx-listhead__edge + .fx-listhead[data-sticky] { margin-top: -32px; }
|
|
1492
|
+
.fx-listhead > .fx-toolbar > .fx-tabs button { padding-block: 9px; }
|
|
1493
|
+
@media (max-width: 820px) { .fx-listhead[data-sticky] { top: 60px; } }
|
|
1321
1494
|
/* On a phone the head would hide too many rows: it scrolls away. */
|
|
1322
|
-
@media (max-width: 560px) { .fx-listhead { position: static; } }
|
|
1495
|
+
@media (max-width: 560px) { .fx-listhead[data-sticky] { position: static; } }
|
|
1323
1496
|
|
|
1324
1497
|
/* The bulk action bar (bulk-bar.js): floats at the foot while rows are
|
|
1325
1498
|
ticked. "N selected ×", then icon actions in groups, then More. It takes
|
|
@@ -1364,7 +1537,7 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
1364
1537
|
/* Xero › Import (server/ui/xero-import.js): Review compares one fact per row
|
|
1365
1538
|
with ✓ or ≠ between the sides; the list and history tick rows for the bulk
|
|
1366
1539
|
bar. */
|
|
1367
|
-
.fx-xi { display: flex; flex-direction: column; gap: 16px; min-width: 0;
|
|
1540
|
+
.fx-xi { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
|
|
1368
1541
|
.fx-xi .fx-lucide { width: 16px; height: 16px; }
|
|
1369
1542
|
.fx-xi__views { display: inline-flex; border: 1px solid var(--fx-line); }
|
|
1370
1543
|
.fx-xi__view { display: inline-grid; place-items: center; width: 40px; height: 36px; border: 0; background: transparent; color: var(--t-dim); cursor: pointer; }
|
|
@@ -12,10 +12,10 @@ const TEST_LABELS = {
|
|
|
12
12
|
more_than: 'is more than', at_most: 'is at most', is: 'is', is_not: 'is not', is_one_of: 'is one of',
|
|
13
13
|
is_empty: 'is empty', is_not_empty: 'is not empty', has: 'has', has_not: 'does not have',
|
|
14
14
|
is_not_one_of: 'is not one of', contains: 'contains', does_not_contain: 'does not contain',
|
|
15
|
-
starts_with: 'starts with', does_not_start_with: 'does not start with', has_number: 'has a number', one_word: 'is one word',
|
|
15
|
+
starts_with: 'starts with', does_not_start_with: 'does not start with', has_number: 'has a number', number_in_words: 'has a number beside words', one_word: 'is one word',
|
|
16
16
|
};
|
|
17
17
|
const ACTION_LABELS = { wait: 'Wait for a person', skip: 'Never draft', pending: 'Pending, with a note' };
|
|
18
|
-
const NO_VALUE = ['is_empty', 'is_not_empty', 'has_number', 'one_word'];
|
|
18
|
+
const NO_VALUE = ['is_empty', 'is_not_empty', 'has_number', 'number_in_words', 'one_word'];
|
|
19
19
|
const MANY = ['is_one_of', 'is_not_one_of'];
|
|
20
20
|
const TYPED = ['contains', 'does_not_contain', 'starts_with', 'does_not_start_with'];
|
|
21
21
|
|
|
@@ -158,7 +158,7 @@ export function rulesSection(ui, title = 'Rules') {
|
|
|
158
158
|
// A pending rule's note: what a person must fix. It shows on the draft.
|
|
159
159
|
const note = h('input', {
|
|
160
160
|
class: 'fx-input', maxlength: '200', value: draft?.action.note ?? '', 'aria-label': 'Note',
|
|
161
|
-
placeholder: '
|
|
161
|
+
placeholder: 'Type a note',
|
|
162
162
|
});
|
|
163
163
|
const noteField = h('div', { class: 'fx-field' }, h('span', { class: 'fx-label' }, 'Note on the draft'), note);
|
|
164
164
|
const showNote = () => { noteField.hidden = action.value !== 'pending'; };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// Settings, Rules (/invoicing-settings): a
|
|
2
|
-
//
|
|
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>).
|
|
3
4
|
// Drafts holds payment terms and the Reference; Price codes the lines with no
|
|
4
5
|
// price item (line-mappings.js); Invoices the checks a ready job passes
|
|
5
6
|
// before Flow drafts its invoice (invoice-rules.js, docs/INVOICE_RULES.md).
|
|
@@ -19,43 +20,65 @@ const SEND_MODES = [
|
|
|
19
20
|
{ value: 'approve_and_email', label: 'Approved and emailed' },
|
|
20
21
|
];
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// Each card: its page, its mark, and its state once the data is in. No
|
|
25
|
-
// blurb: the name and the state say enough.
|
|
23
|
+
// The parts of Rules, each on its own page (/invoicing-settings/<key>).
|
|
26
24
|
const PARTS = [
|
|
27
|
-
{ key: '
|
|
28
|
-
{ key: '
|
|
29
|
-
|
|
30
|
-
const { labels } = await flow('/api/invoicing/line-labels');
|
|
31
|
-
const waiting = labels.filter((l) => l.unmapped > 0).length;
|
|
32
|
-
return waiting ? { status: { label: `${waiting} wait`, tone: 'warn' }, action: 'Fix' } : { status: { label: 'All mapped', tone: 'success' } };
|
|
33
|
-
} },
|
|
34
|
-
{ key: 'invoices', title: 'Invoices', mark: 'rules', mount: (ui) => rulesSection(ui, 'Invoices'),
|
|
35
|
-
summary: async (flow) => ({ status: onCount((await flow('/api/invoicing/rules')).rules) }) },
|
|
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) },
|
|
36
28
|
];
|
|
37
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
38
|
export function renderInvoicingSettings(main, ui, id) {
|
|
39
|
-
const { h, flow, notice, pageHeader, tileCard, crumbs } = ui;
|
|
39
|
+
const { h, flow, notice, pageHeader, tileCard, tileSection, crumbs } = ui;
|
|
40
40
|
if (id) {
|
|
41
41
|
const part = PARTS.find((p) => p.key === id);
|
|
42
42
|
main.append(crumbs(['/invoicing-settings', 'Rules'], part?.title ?? 'Not found'),
|
|
43
43
|
part ? h('div', { class: 'fx-card', style: { padding: '8px 20px 16px' } }, part.mount(ui)) : notice('No such part of Rules.', 'danger'));
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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;
|
|
57
61
|
}
|
|
58
|
-
|
|
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
|
+
})();
|
|
59
82
|
}
|
|
60
83
|
|
|
61
84
|
// Payment terms and the Reference template.
|
|
@@ -104,7 +127,7 @@ function draftsSection(ui) {
|
|
|
104
127
|
disabled ? notice('Only an owner or admin can change these.', 'info') : null,
|
|
105
128
|
row('Payment terms', 'Days from invoice to due date.', box('paymentTermsDays', 'Payment terms in days', { type: 'number', read: Number })),
|
|
106
129
|
row('Reference', 'The Reference on each invoice. Use {practitioner} and {patient}.',
|
|
107
|
-
box('referenceTemplate', 'Reference rule', { placeholder: '
|
|
130
|
+
box('referenceTemplate', 'Reference rule', { placeholder: 'Type a reference rule' })),
|
|
108
131
|
].filter(Boolean));
|
|
109
132
|
})();
|
|
110
133
|
return section;
|