@arcforgelabs/flow 2026.10.30 → 2026.10.32
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 +2 -0
- package/npm-shrinkwrap.json +188 -2
- package/package.json +2 -1
- package/server/api/alerts.js +49 -0
- package/server/api/invoicing.js +125 -7
- package/server/app.js +29 -2
- package/server/core/alerts.js +157 -0
- package/server/core/automations.js +17 -3
- package/server/core/history.js +65 -0
- package/server/core/invoice-pdf.js +266 -0
- package/server/core/invoices.js +2 -2
- package/server/core/invoicing-rules.js +26 -1
- package/server/core/job-board.js +25 -3
- package/server/core/ledger.js +3 -2
- package/server/core/line-mapping.js +36 -10
- package/server/core/pipeline.js +76 -4
- package/server/core/price-list.js +348 -0
- package/server/core/references.js +99 -0
- package/server/core/sending.js +11 -5
- package/server/db/commands.js +42 -0
- package/server/db/doctor.js +168 -0
- package/server/db/migrations/0014_price_list.sql +34 -0
- package/server/db/migrations/0015_alert_dismissals.sql +17 -0
- package/server/db/migrations/0016_invoice_reference.sql +1 -0
- package/server/db/migrations/meta/0014_snapshot.json +3773 -0
- package/server/db/migrations/meta/0015_snapshot.json +3854 -0
- package/server/db/migrations/meta/0016_snapshot.json +3861 -0
- package/server/db/migrations/meta/_journal.json +21 -0
- package/server/db/schema.js +46 -0
- package/server/flow-server.js +20 -1
- package/server/index.js +2 -1
- package/server/integrations/call-guard.js +362 -0
- package/server/integrations/port.js +21 -1
- package/server/integrations/seazona/feed.js +31 -2
- package/server/integrations/seazona/governor.js +50 -122
- package/server/integrations/seazona/history/backfill.js +208 -0
- package/server/integrations/seazona/history/csv.js +65 -0
- package/server/integrations/seazona/history/index.js +584 -0
- package/server/integrations/seazona/history/read.js +196 -0
- package/server/integrations/seazona/index.js +46 -1
- package/server/integrations/seazona/lanes.js +187 -51
- package/server/integrations/seazona/migrations/0003_call_guard.sql +2 -0
- package/server/integrations/seazona/migrations/0004_history_import.sql +68 -0
- package/server/integrations/seazona/migrations/meta/0003_snapshot.json +895 -0
- package/server/integrations/seazona/migrations/meta/0004_snapshot.json +1323 -0
- package/server/integrations/seazona/migrations/meta/_journal.json +14 -0
- package/server/integrations/seazona/patients.js +30 -0
- package/server/integrations/seazona/routes.js +145 -0
- package/server/integrations/seazona/schema.js +94 -0
- package/server/integrations/seazona/settings.js +14 -3
- package/server/integrations/seazona/sync-once.js +1 -1
- package/server/integrations/seazona/tables.js +4 -0
- package/server/integrations/xero/client.js +39 -7
- package/server/integrations/xero/drafts.js +46 -8
- package/server/integrations/xero/import.js +131 -17
- package/server/integrations/xero/index.js +38 -7
- package/server/integrations/xero/lookup.js +26 -0
- package/server/integrations/xero/migrations/0006_xero_call_guard.sql +26 -0
- package/server/integrations/xero/migrations/0007_xero_webhooks.sql +30 -0
- package/server/integrations/xero/migrations/meta/_journal.json +14 -0
- package/server/integrations/xero/mirror.js +27 -10
- package/server/integrations/xero/oauth.js +4 -1
- package/server/integrations/xero/reconcile.js +173 -0
- package/server/integrations/xero/routes.js +55 -0
- package/server/integrations/xero/runtime.js +45 -4
- package/server/integrations/xero/settings.js +3 -0
- package/server/integrations/xero/webhooks.js +311 -0
- package/server/release-channel.js +3 -3
- package/server/ui/alerts.js +77 -0
- package/server/ui/app-page.js +1 -1
- package/server/ui/app.js +63 -21
- package/server/ui/clerk-appearance.js +42 -9
- package/server/ui/flow.css +78 -9
- package/server/ui/invoice-notes.js +26 -0
- package/server/ui/invoicing-settings.js +4 -2
- package/server/ui/invoicing.js +241 -49
- package/server/ui/line-mappings.js +3 -2
- package/server/ui/price-list.js +247 -0
- package/server/ui/routes.js +1 -1
|
@@ -11,6 +11,9 @@ const gearIcon = {
|
|
|
11
11
|
'&& > *': { visibility: 'hidden' },
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
// A small down chevron after the name, as in other apps' account buttons.
|
|
15
|
+
const CHEVRON = `url("data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="none" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4.5L6 7.5L9 4.5"/></svg>')}")`;
|
|
16
|
+
|
|
14
17
|
// Matches the narrow-screen rule in flow.css, where the sidebar becomes a bar.
|
|
15
18
|
const NARROW = '@media (max-width: 820px)';
|
|
16
19
|
|
|
@@ -88,24 +91,54 @@ export function flowClerkAppearance({ embedded = false, publicSignUp = false } =
|
|
|
88
91
|
badge: { '&&': { color: dim, background: `color-mix(in srgb, ${text} 5%, transparent)`, borderColor: line } },
|
|
89
92
|
formButtonPrimary: { boxShadow: 'none', textTransform: 'none', fontWeight: 650 },
|
|
90
93
|
socialButtonsBlockButton: controlEdge,
|
|
91
|
-
// The account button in the sidebar foot
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
94
|
+
// The account button in the sidebar foot: photo or initial, then the
|
|
95
|
+
// name (Clerk shows the email when there is no name), clipped with an
|
|
96
|
+
// ellipsis to fit, then a small chevron. At rest it is plain; on hover,
|
|
97
|
+
// keyboard focus or while its menu is open it is an outlined pill. The
|
|
98
|
+
// top bar on narrow screens has no room for the name, so there it is the
|
|
99
|
+
// photo or initial alone.
|
|
95
100
|
userButtonTrigger: {
|
|
96
101
|
'&&': {
|
|
97
|
-
width: '100%', minWidth: 0, justifyContent: 'flex-start', padding: '4px
|
|
98
|
-
'
|
|
99
|
-
[
|
|
102
|
+
width: 'auto', maxWidth: '100%', minWidth: 0, justifyContent: 'flex-start', padding: '4px 8px 4px 4px',
|
|
103
|
+
borderRadius: token('--t-radius-lg', '10px'), background: 'transparent', boxShadow: 'none',
|
|
104
|
+
'&:hover, &:focus-visible, &[aria-expanded="true"]': { background: `color-mix(in srgb, ${text} 5%, transparent)`, boxShadow: `inset 0 0 0 1px ${line}` },
|
|
105
|
+
'&:focus-visible': { boxShadow: `inset 0 0 0 1px ${line}, ${focus}` },
|
|
106
|
+
[NARROW]: { padding: 0, borderRadius: '50%', '&:hover, &[aria-expanded="true"]': { background: 'transparent', boxShadow: 'none' } },
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
userButtonBox: {
|
|
110
|
+
flexDirection: 'row-reverse', justifyContent: 'flex-end', minWidth: 0, gap: '6px',
|
|
111
|
+
// The box lays out right to left, so ::before lands after the name.
|
|
112
|
+
'&::before': {
|
|
113
|
+
content: '""', flex: 'none', width: '10px', height: '10px', background: dim,
|
|
114
|
+
mask: `${CHEVRON} center / contain no-repeat`, WebkitMask: `${CHEVRON} center / contain no-repeat`,
|
|
100
115
|
},
|
|
116
|
+
[NARROW]: { '&::before': { display: 'none' } },
|
|
101
117
|
},
|
|
102
|
-
userButtonBox: { flexDirection: 'row-reverse', justifyContent: 'flex-end', minWidth: 0, gap: '8px' },
|
|
103
118
|
userButtonOuterIdentifier: {
|
|
104
119
|
minWidth: 0, paddingInlineStart: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
|
105
120
|
color: token('--t-strong', text), fontSize: '14px', fontWeight: 600,
|
|
106
121
|
[NARROW]: { display: 'none' },
|
|
107
122
|
},
|
|
108
|
-
|
|
123
|
+
// The account menu is as wide as its rows, between 220px and 280px, with
|
|
124
|
+
// compact rows. A long name or email ends with an ellipsis and does not
|
|
125
|
+
// make the menu wider.
|
|
126
|
+
userButtonPopoverCard: {
|
|
127
|
+
'&&': { width: 'max-content', minWidth: '220px', maxWidth: 'min(280px, calc(100vw - 16px))', border: `1px solid ${line}`, boxShadow: shadow },
|
|
128
|
+
},
|
|
129
|
+
userPreview__userButton: { '&&': { padding: '10px 12px', gap: '10px' } },
|
|
130
|
+
userPreviewAvatarContainer__userButton: { '&&, && .cl-userPreviewAvatarBox': { width: '28px', height: '28px' } },
|
|
131
|
+
userPreviewTextContainer__userButton: { '&&': { minWidth: 0, overflow: 'hidden' } },
|
|
132
|
+
userPreviewMainIdentifier__userButton: { '&&': { minWidth: 0, fontSize: '13px' } },
|
|
133
|
+
userPreviewMainIdentifierText__userButton: { '&&': { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } },
|
|
134
|
+
userPreviewSecondaryIdentifier__userButton: {
|
|
135
|
+
'&&': { display: 'block', minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: '12px', color: dim },
|
|
136
|
+
},
|
|
137
|
+
userButtonPopoverActionButton: { '&&': { padding: '8px 12px', gap: '10px', fontSize: '13px', color: text } },
|
|
138
|
+
userButtonPopoverActionButtonIconBox: { '&&': { flex: '0 0 28px', width: '28px' } },
|
|
139
|
+
userButtonPopoverCustomItemButton: { '&&': { padding: '8px 12px', gap: '10px', fontSize: '13px', color: text } },
|
|
140
|
+
userButtonPopoverCustomItemButtonIconBox: { '&&': { flex: '0 0 28px', width: '28px' } },
|
|
141
|
+
userButtonPopoverFooter: { '&&': { minWidth: 0 } },
|
|
109
142
|
userButtonPopoverActionButtonIcon__manageAccount: gearIcon,
|
|
110
143
|
footer: embedded ? { display: 'none' } : undefined,
|
|
111
144
|
},
|
package/server/ui/flow.css
CHANGED
|
@@ -113,10 +113,17 @@ body:is([data-state="off"], [data-state="error"], [data-state="no-access"]) .fx-
|
|
|
113
113
|
display: flex; flex-direction: column; gap: 4px; flex: none;
|
|
114
114
|
width: 232px; height: 100vh; padding: 16px 12px 12px;
|
|
115
115
|
background: var(--t-s1); border-right: 1px solid var(--fx-line);
|
|
116
|
+
transition: width .18s ease;
|
|
116
117
|
}
|
|
117
118
|
.fx-side__head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 0 0 14px 8px; }
|
|
118
119
|
.fx-side__head .fx-brand { min-width: 0; font-size: 19px; }
|
|
119
120
|
.fx-side__head .fx-mark { width: 26px; height: 26px; }
|
|
121
|
+
/* The sidebar button. Open, it shows the sidebar icon beside the brand.
|
|
122
|
+
Folded or narrow, the brand is hidden and the button shows the logo, which
|
|
123
|
+
turns into the sidebar icon on hover or focus (and while the drawer is
|
|
124
|
+
open), so the two never sit side by side. */
|
|
125
|
+
.fx-toggle__mark { display: none; }
|
|
126
|
+
.fx-toggle__mark .fx-mark { display: block; }
|
|
120
127
|
.fx-nav { display: flex; flex-direction: column; gap: 2px; flex: 1; min-height: 0; overflow-y: auto; }
|
|
121
128
|
.fx-nav__group { padding: 16px 10px 6px; color: var(--t-dim); font: var(--t-label-weight) var(--t-label-size)/1.3 var(--t-body); letter-spacing: var(--t-label-spacing); text-transform: var(--t-label-case); }
|
|
122
129
|
.fx-nav__group:first-child { padding-top: 4px; }
|
|
@@ -132,7 +139,7 @@ body:is([data-state="off"], [data-state="error"], [data-state="no-access"]) .fx-
|
|
|
132
139
|
.fx-nav__count { margin-left: auto; color: var(--t-dim); font-size: 12px; font-weight: 700; }
|
|
133
140
|
.fx-ico { display: inline-flex; flex: none; }
|
|
134
141
|
.fx-ico svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
|
135
|
-
.fx-side__end { display: flex; align-items: center; justify-content: space-between; gap:
|
|
142
|
+
.fx-side__end { display: flex; align-items: center; justify-content: space-between; gap: 4px; padding: 12px 0 0; border-top: 1px solid var(--fx-line); }
|
|
136
143
|
.fx-side__user { display: flex; min-width: 32px; min-height: 32px; align-items: center; }
|
|
137
144
|
/* Clerk replaces the mount's classes with its root box's, so this uses the id. */
|
|
138
145
|
#user-button { flex: 1 1 auto; min-width: 0; }
|
|
@@ -141,14 +148,14 @@ body:is([data-state="off"], [data-state="error"], [data-state="no-access"]) .fx-
|
|
|
141
148
|
@media (min-width: 821px) {
|
|
142
149
|
.fx-shell[data-folded] .fx-side { width: 64px; padding-inline: 10px; }
|
|
143
150
|
.fx-shell[data-folded] .fx-side__head { flex-direction: column; gap: 10px; padding: 0 0 14px; }
|
|
144
|
-
.fx-shell[data-folded] .fx-brand
|
|
151
|
+
.fx-shell[data-folded] .fx-side__head .fx-brand { display: none; }
|
|
145
152
|
.fx-shell[data-folded] .fx-nav__group { height: 1px; margin: 10px 6px; padding: 0; overflow: hidden; background: var(--fx-line); color: transparent; }
|
|
146
153
|
.fx-shell[data-folded] :is(.fx-nav a, .fx-nav__report) { justify-content: center; padding: 9px 0; }
|
|
147
154
|
.fx-shell[data-folded] .fx-nav__label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
|
|
148
155
|
.fx-shell[data-folded] .fx-side__end { flex-direction: column; padding: 12px 0 0; }
|
|
149
156
|
.fx-shell[data-folded] #user-button { flex: none; }
|
|
150
157
|
.fx-shell[data-folded] #user-button .cl-userButtonTrigger { width: auto; padding: 0; border-radius: 50%; background: transparent; box-shadow: none; }
|
|
151
|
-
.fx-shell[data-folded] #user-button .cl-userButtonOuterIdentifier { display: none; }
|
|
158
|
+
.fx-shell[data-folded] #user-button .cl-userButtonOuterIdentifier, .fx-shell[data-folded] #user-button .cl-userButtonBox::before { display: none; }
|
|
152
159
|
}
|
|
153
160
|
.fx-main { flex: 1; min-width: 0; max-width: 1180px; margin: 0 auto; padding: 32px clamp(16px, 4vw, 48px) 64px; display: grid; gap: 20px; align-content: start; }
|
|
154
161
|
.fx-main:focus { outline: none; box-shadow: none; }
|
|
@@ -269,6 +276,7 @@ body:is([data-state="off"], [data-state="error"], [data-state="no-access"]) .fx-
|
|
|
269
276
|
.fx-int-hero__actions { position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 6px; }
|
|
270
277
|
.fx-int-page__grid { display: grid; grid-template-columns: minmax(0, 1fr) 240px; gap: 32px; align-items: start; }
|
|
271
278
|
.fx-int-page__main { display: grid; gap: 28px; }
|
|
279
|
+
.fx-int-page__main > .fx-alerts { margin: 0; }
|
|
272
280
|
.fx-int-section { display: grid; gap: 14px; padding-top: 20px; border-top: 1px solid var(--fx-line); }
|
|
273
281
|
.fx-int-section__title { color: var(--t-strong); font: 650 17px/1.3 var(--t-body); }
|
|
274
282
|
.fx-int-section .fx-btn { justify-self: start; }
|
|
@@ -328,6 +336,25 @@ a.fx-strong:hover { text-decoration: underline; }
|
|
|
328
336
|
/* Invoices: the Send all ready bar above the Ready list. */
|
|
329
337
|
.fx-sendbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin: 0 0 12px; padding: 12px 16px; border: 1px solid var(--fx-line); border-radius: 8px; background: var(--t-paper); margin-top: 12px; }
|
|
330
338
|
.fx-sendbar[hidden] { display: none; }
|
|
339
|
+
|
|
340
|
+
/* Quiet alerts (server/ui/alerts.js): one slim line under the page header,
|
|
341
|
+
only while something on the page needs this person. */
|
|
342
|
+
.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; }
|
|
343
|
+
.fx-alerts[hidden] { display: none; }
|
|
344
|
+
.fx-alerts[data-tone="danger"] { border-left-color: var(--t-danger); }
|
|
345
|
+
.fx-alerts__text { flex: 1; min-width: 0; margin: 0; color: var(--t-strong); }
|
|
346
|
+
.fx-alerts__money { color: var(--t-dim); }
|
|
347
|
+
.fx-alerts__action { flex: none; padding: 0; border: 0; background: none; font: inherit; font-weight: 600; color: var(--t-strong); text-decoration: underline; text-underline-offset: 2px; cursor: pointer; }
|
|
348
|
+
.fx-alerts__more { flex: none; font-size: 12.5px; color: var(--t-dim); white-space: nowrap; }
|
|
349
|
+
.fx-alerts__close { flex: none; display: grid; place-items: center; width: 28px; height: 28px; padding: 0; border: 0; border-radius: var(--t-radius); background: none; color: var(--t-dim); cursor: pointer; }
|
|
350
|
+
.fx-alerts__close:hover { background: var(--fx-hover); color: var(--t-strong); }
|
|
351
|
+
.fx-alerts__close svg { width: 14px; height: 14px; }
|
|
352
|
+
.fx-alerts__action:focus-visible, .fx-alerts__close:focus-visible { outline: 2px solid var(--t-ring); outline-offset: 2px; }
|
|
353
|
+
@media (max-width: 560px) {
|
|
354
|
+
.fx-alerts { flex-wrap: wrap; }
|
|
355
|
+
.fx-alerts__text { flex-basis: calc(100% - 52px); }
|
|
356
|
+
.fx-alerts__action, .fx-alerts__more { order: 2; }
|
|
357
|
+
}
|
|
331
358
|
.fx-sendbar p { margin: 0; }
|
|
332
359
|
.fx-problem { color: var(--t-danger); }
|
|
333
360
|
.fx-sendbar__actions { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
@@ -587,6 +614,7 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
587
614
|
.fx-setting__text { display: grid; gap: 3px; min-width: 0; }
|
|
588
615
|
.fx-setting__name { color: var(--t-strong); font: 500 14.5px/1.35 var(--t-body); }
|
|
589
616
|
.fx-setting__hint { margin: 0; color: var(--t-dim); font-size: 13px; line-height: 1.45; }
|
|
617
|
+
.fx-setting[data-tone="danger"] .fx-setting__hint { color: var(--t-danger); }
|
|
590
618
|
.fx-setting__control { display: flex; justify-content: flex-end; min-width: 0; }
|
|
591
619
|
.fx-setting__control > .fx-input, .fx-setting__control > .fx-select, .fx-setting__stack { width: 320px; max-width: 100%; }
|
|
592
620
|
.fx-setting__control .fx-swatches { flex-wrap: nowrap; }
|
|
@@ -638,19 +666,41 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
638
666
|
.fx-shell { flex-direction: column; }
|
|
639
667
|
.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); }
|
|
640
668
|
.fx-side__head { flex-direction: row-reverse; gap: 6px; padding: 0; }
|
|
641
|
-
.fx-side__head .fx-brand
|
|
642
|
-
/* The menu
|
|
669
|
+
.fx-side__head .fx-brand { display: none; }
|
|
670
|
+
/* The menu is a drawer: it slides in from the left, below the bar, over
|
|
671
|
+
a dimmed page. A click on the dimmed page closes it (app.js). */
|
|
643
672
|
.fx-nav {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
background: var(--t-s1); border-
|
|
673
|
+
position: fixed; top: 60px; bottom: 0; left: 0; z-index: 2;
|
|
674
|
+
width: min(280px, 85vw); padding: 8px 12px 12px;
|
|
675
|
+
background: var(--t-s1); border-right: 1px solid var(--fx-line); box-shadow: var(--t-shadow);
|
|
676
|
+
transform: translateX(-100%); visibility: hidden;
|
|
677
|
+
transition: transform .2s ease, visibility .2s;
|
|
678
|
+
}
|
|
679
|
+
.fx-shell[data-menu-open] .fx-nav { transform: none; visibility: visible; }
|
|
680
|
+
.fx-shell[data-menu-open]::after {
|
|
681
|
+
content: ""; position: fixed; inset: 60px 0 0; z-index: 9;
|
|
682
|
+
background: rgba(0, 0, 0, .32);
|
|
647
683
|
}
|
|
648
|
-
.fx-shell[data-menu-open] .fx-nav { display: flex; }
|
|
649
684
|
.fx-nav__report { margin-top: 8px; }
|
|
650
685
|
.fx-side__end { margin-left: auto; padding: 0; border-top: 0; }
|
|
651
686
|
#user-button { flex: none; }
|
|
652
687
|
}
|
|
653
688
|
|
|
689
|
+
/* Collapsed (folded on wide screens, or narrow): logo at rest, sidebar icon
|
|
690
|
+
on hover, focus or while open. */
|
|
691
|
+
@media (min-width: 821px) {
|
|
692
|
+
.fx-shell[data-folded] .fx-toggle__mark { display: block; }
|
|
693
|
+
.fx-shell[data-folded] .fx-toggle__icon { display: none; }
|
|
694
|
+
.fx-shell[data-folded] #menu-toggle:is(:hover, :focus-visible) .fx-toggle__mark { display: none; }
|
|
695
|
+
.fx-shell[data-folded] #menu-toggle:is(:hover, :focus-visible) .fx-toggle__icon { display: inline-flex; }
|
|
696
|
+
}
|
|
697
|
+
@media (max-width: 820px) {
|
|
698
|
+
.fx-toggle__mark { display: block; }
|
|
699
|
+
.fx-toggle__icon { display: none; }
|
|
700
|
+
#menu-toggle:is(:hover, :focus-visible, [aria-expanded="true"]) .fx-toggle__mark { display: none; }
|
|
701
|
+
#menu-toggle:is(:hover, :focus-visible, [aria-expanded="true"]) .fx-toggle__icon { display: inline-flex; }
|
|
702
|
+
}
|
|
703
|
+
|
|
654
704
|
/* ── Phones ─────────────────────────────────────────────────────────────── */
|
|
655
705
|
@media (max-width: 640px) {
|
|
656
706
|
.fx-main { padding-top: 20px; }
|
|
@@ -688,6 +738,14 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
688
738
|
.fx-set__control .fx-input[type="number"] { width: 96px; }
|
|
689
739
|
.fx-set__control .fx-select { min-width: 220px; }
|
|
690
740
|
.fx-set + .fx-set { margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--fx-line); }
|
|
741
|
+
/* Price list (price-list.js): a code's price and its systems' chips. */
|
|
742
|
+
.fx-price__hint { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; }
|
|
743
|
+
.fx-price__head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 16px; }
|
|
744
|
+
.fx-price__head .fx-search { margin: 0; }
|
|
745
|
+
@media (max-width: 560px) { .fx-price__head .fx-search { width: 100%; } }
|
|
746
|
+
.fx-price__systems { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 10px; }
|
|
747
|
+
.fx-set__control .fx-price__link { width: 260px; min-width: 0; }
|
|
748
|
+
@media (max-width: 560px) { .fx-set__control .fx-price__link { width: 150px; } }
|
|
691
749
|
/* Invoice rules (invoice-rules.js): a row's Edit button and switch, and the
|
|
692
750
|
rule dialog's condition lines. */
|
|
693
751
|
.fx-rule__controls { align-items: center; gap: 10px; }
|
|
@@ -749,3 +807,14 @@ body[data-state="loading"] .fx-loading { display: grid; justify-items: center; }
|
|
|
749
807
|
color-scheme: light;
|
|
750
808
|
}
|
|
751
809
|
[data-mode="dark"] #sentry-feedback { --interactive-filter: brightness(150%); color-scheme: dark; }
|
|
810
|
+
|
|
811
|
+
/* The bar above one invoice: Back, its place in the list, previous and next.
|
|
812
|
+
Quiet: no frame, dim text, round icon buttons. */
|
|
813
|
+
.fx-pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 12px; min-height: 32px; }
|
|
814
|
+
.fx-pager__back { justify-self: start; background: none; border: 0; padding: 4px 0; color: var(--t-dim); font: inherit; font-size: 13.5px; cursor: pointer; transition: color 120ms var(--fx-ease); }
|
|
815
|
+
.fx-pager__back:hover, .fx-pager__back:focus-visible { color: var(--t-strong); }
|
|
816
|
+
.fx-pager__count { grid-column: 2; color: var(--t-dim); font-size: 13px; font-variant-numeric: tabular-nums; }
|
|
817
|
+
.fx-pager__steps { grid-column: 3; justify-self: end; display: flex; gap: 4px; }
|
|
818
|
+
.fx-pager__step { display: inline-grid; place-items: center; width: 30px; height: 30px; padding: 0; border: 1px solid var(--fx-line-soft); border-radius: 999px; background: transparent; color: var(--t-text); cursor: pointer; transition: background 120ms var(--fx-ease), border-color 120ms var(--fx-ease); }
|
|
819
|
+
.fx-pager__step:hover:not(:disabled) { background: var(--fx-hover); border-color: var(--fx-line); }
|
|
820
|
+
.fx-pager__step:disabled { opacity: 0.35; cursor: default; }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The notes on the Invoices page and on one invoice (invoicing.js). One fact
|
|
2
|
+
// per sentence; they follow the accounting integration's settings. Kept apart
|
|
3
|
+
// so tests can load them without a browser.
|
|
4
|
+
|
|
5
|
+
// What happens to a ready invoice in the accounting system, one fact per
|
|
6
|
+
// sentence. It follows the integration's settings: its connection, whether it
|
|
7
|
+
// takes writes, and the send mode on its page. `what` names the invoices:
|
|
8
|
+
// "each invoice" on the list, "this invoice" on one invoice.
|
|
9
|
+
export function sendModeNote(to, sendMode, what = 'each invoice') {
|
|
10
|
+
if (!to) return 'No accounting system is on. Flow keeps each invoice here only.';
|
|
11
|
+
if (sendMode && !sendMode.connected) return `${to} is not connected. Connect it in Integrations before you send.`;
|
|
12
|
+
if (sendMode && !sendMode.writesAllowed) return `${to} does not take invoices from Flow yet. Turn this on in Integrations, ${to}.`;
|
|
13
|
+
const text = sendMode?.mode === 'approve_and_email'
|
|
14
|
+
? [`Flow sends ${what} to ${to} as approved.`, `${to} emails it to the customer at once.`]
|
|
15
|
+
: [`Flow sends ${what} to ${to} as a draft.`, `You approve and send it in ${to}.`];
|
|
16
|
+
const others = sendMode?.otherCustomers ?? 0;
|
|
17
|
+
if (others) text.push(`${others} ${others === 1 ? 'customer has' : 'customers have'} a different setting.`);
|
|
18
|
+
return text.join(' ');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The line under a draft's title, one fact per sentence.
|
|
22
|
+
export function draftNote(to, invoice, ready) {
|
|
23
|
+
if (!to) return ready ? 'A person checked it. A saved change keeps it ready.' : 'Flow gives it a number when you issue it.';
|
|
24
|
+
if (!ready) return `Mark it ready, then send it to ${to}.`;
|
|
25
|
+
return sendModeNote(to, invoice?.sendMode, 'this invoice');
|
|
26
|
+
}
|
|
@@ -23,7 +23,7 @@ const SEND_MODES = [
|
|
|
23
23
|
export async function renderInvoicingSettings(main, ui) {
|
|
24
24
|
const { h, flow, toast, notice, pageHeader, me } = ui;
|
|
25
25
|
const host = h('div', { class: 'fx-card', style: { padding: '8px 20px 16px' } }, h('div', { class: 'fx-skeleton' }));
|
|
26
|
-
main.append(pageHeader('
|
|
26
|
+
main.append(pageHeader('Invoice rules', { subtitle: 'The rules Flow uses to draft each invoice.' }), host);
|
|
27
27
|
let body;
|
|
28
28
|
try {
|
|
29
29
|
body = await flow('/api/invoicing/settings');
|
|
@@ -66,7 +66,9 @@ export async function renderInvoicingSettings(main, ui) {
|
|
|
66
66
|
host.replaceChildren(...[
|
|
67
67
|
disabled ? notice('Only an owner or admin can change these.', 'info') : null,
|
|
68
68
|
section('Drafts',
|
|
69
|
-
row('Payment terms', 'Days from invoice to due date.', days('paymentTermsDays', 'Payment terms in days'))
|
|
69
|
+
row('Payment terms', 'Days from invoice to due date.', days('paymentTermsDays', 'Payment terms in days')),
|
|
70
|
+
row('Reference', 'The Reference on each invoice. Use {practitioner} and {patient}.',
|
|
71
|
+
box('referenceTemplate', 'Reference rule', { placeholder: '{practitioner} - {patient}' }))),
|
|
70
72
|
lineMappingSection(ui),
|
|
71
73
|
automationSection(ui, 'invoicing', 'Automations'),
|
|
72
74
|
rulesSection(ui),
|