@mindbill/angular 0.1.1 → 0.2.0

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 CHANGED
@@ -32,6 +32,8 @@ export class CaseBillingComponent {
32
32
 
33
33
  The component loads and refreshes status, searches the payer directory, saves bill edits, manages the explicit payer packet, submits the bill, shows EORs, and exposes the correct payment, review, correction, resubmission, and close actions for the current state.
34
34
 
35
+ Procedure entry always keeps one empty row after the entered lines. Starting a code, modifier, or non-default unit count opens the next row automatically; the empty row is never included in save or submit payloads.
36
+
35
37
  Add one authenticated server endpoint. It verifies that the signed-in user may access the bill, then mints a short-lived token bound to that bill and browser origin.
36
38
 
37
39
  ```ts
package/dist/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # @mindbill/angular
2
+
3
+ Native Angular billing UI with built-in session renewal and lifecycle API calls.
4
+
5
+ ```bash
6
+ npm install @mindbill/angular @mindbill/node
7
+ ```
8
+
9
+ Import the standalone component and give it the one value your application keeps: the bill ID.
10
+
11
+ ```ts
12
+ import { Component } from "@angular/core";
13
+ import { MindBillBillLifecycleComponent } from "@mindbill/angular";
14
+
15
+ @Component({
16
+ selector: "app-case-billing",
17
+ standalone: true,
18
+ imports: [MindBillBillLifecycleComponent],
19
+ template: `
20
+ <mindbill-bill-lifecycle
21
+ [billId]="billId"
22
+ sessionEndpoint="/api/mindbill/bill-session"
23
+ [appearance]="{ preset: 'clinical-blue' }"
24
+ (billIdChange)="billId = $event"
25
+ />
26
+ `,
27
+ })
28
+ export class CaseBillingComponent {
29
+ billId = "bill_123";
30
+ }
31
+ ```
32
+
33
+ The component loads and refreshes status, searches the payer directory, saves bill edits, manages the explicit payer packet, submits the bill, shows EORs, and exposes the correct payment, review, correction, resubmission, and close actions for the current state.
34
+
35
+ Procedure entry always keeps one empty row after the entered lines. Starting a code, modifier, or non-default unit count opens the next row automatically; the empty row is never included in save or submit payloads.
36
+
37
+ Add one authenticated server endpoint. It verifies that the signed-in user may access the bill, then mints a short-lived token bound to that bill and browser origin.
38
+
39
+ ```ts
40
+ import { MindBillClient } from "@mindbill/node";
41
+
42
+ const mindbill = new MindBillClient({ apiKey: process.env["MINDBILL_API_KEY"]! });
43
+
44
+ app.post("/api/mindbill/bill-session", requireUser, async (req, res) => {
45
+ await requireBillAccess(req.user, req.body.billId);
46
+ const session = await mindbill.createBrowserSession({
47
+ component: "bill-review",
48
+ billId: req.body.billId,
49
+ allowedOrigin: `${req.protocol}://${req.get("host")}`,
50
+ expiresIn: 900,
51
+ });
52
+ res.json(session);
53
+ });
54
+ ```
55
+
56
+ Your permanent API key never reaches Angular. Available presets are `mindbill`, `qme-companion`, `orange-bright`, and `clinical-blue`; every visual token can also be overridden.
@@ -52,8 +52,9 @@ class MindBillLifecycleStore {
52
52
  searchClaimsAdministrators(query, claimNumber) {
53
53
  return this.requireClient().searchClaimsAdministrators(query, claimNumber);
54
54
  }
55
+ getDeliveryOptions() { return this.requireClient().getDeliveryOptions(); }
55
56
  saveReview(input) { return this.mutate(() => this.requireClient().saveReview(input)); }
56
- submitBill(input, route) { return this.mutate(() => this.requireClient().submitBill(input, route)); }
57
+ submitBill(input, submission) { return this.mutate(() => this.requireClient().submitBill(input, submission)); }
57
58
  addAttachment(file, type, description) { return this.mutate(() => this.requireClient().addAttachment(file, type, description)); }
58
59
  removeAttachment(id) { return this.mutate(() => this.requireClient().removeAttachment(id)); }
59
60
  getAttachment(id) { return this.requireClient().getAttachment(id); }
@@ -103,6 +104,31 @@ class MindBillLifecycleStore {
103
104
  }
104
105
  }
105
106
 
107
+ const EMPTY_PROCEDURE_LINE = {
108
+ code: "",
109
+ modifiers: [],
110
+ units: 1,
111
+ charge: 0,
112
+ };
113
+ function isEmptyProcedureLine(line) {
114
+ return !line.code.trim()
115
+ && line.modifiers.length === 0
116
+ && line.units === 1
117
+ && !line.serviceDate
118
+ && !line.serviceDateEnd
119
+ && !(line.diagnosisPointers?.length)
120
+ && !line.charge;
121
+ }
122
+ /** Keeps completed/partial lines plus one keyboard-ready empty row. */
123
+ function ensureTrailingProcedureLine(lines) {
124
+ const entered = lines
125
+ .filter((line) => !isEmptyProcedureLine(line))
126
+ .map((line) => ({ ...line, modifiers: [...line.modifiers] }));
127
+ return entered.length < 50
128
+ ? [...entered, { ...EMPTY_PROCEDURE_LINE, modifiers: [] }]
129
+ : entered;
130
+ }
131
+
106
132
  const THEMES = {
107
133
  mindbill: { preset: "mindbill", accentColor: "#238dbd", accentTextColor: "#fff", backgroundColor: "#f3f8fa", surfaceColor: "#fff", textColor: "#203743", mutedColor: "#657982", borderColor: "#dbe6ea", borderRadius: "14px", controlRadius: "8px", fontFamily: "Inter,system-ui,sans-serif" },
108
134
  "qme-companion": { preset: "qme-companion", accentColor: "#53b5dc", accentTextColor: "#173542", backgroundColor: "#f2f8fb", surfaceColor: "#fff", textColor: "#1d3440", mutedColor: "#617783", borderColor: "#d7e5eb", borderRadius: "12px", controlRadius: "8px", fontFamily: "Inter,system-ui,sans-serif" },
@@ -114,6 +140,7 @@ const DOCUMENT_TYPES = [
114
140
  { value: "proof_of_service", label: "Proof of service" },
115
141
  { value: "letter_of_attestation", label: "Letter of attestation" },
116
142
  { value: "form_122", label: "Required form" },
143
+ { value: "return_to_work_voucher", label: "Return-to-work voucher" },
117
144
  { value: "w9", label: "W-9" },
118
145
  { value: "appeal", label: "Appeal support" },
119
146
  { value: "medical_records", label: "Medical records (intentional)" },
@@ -131,14 +158,17 @@ class MindBillBillLifecycleComponent {
131
158
  billingError = new EventEmitter();
132
159
  store = new MindBillLifecycleStore();
133
160
  documentTypes = DOCUMENT_TYPES;
134
- routes = [{ value: "ebill", label: "E-bill" }, { value: "fax", label: "Fax" }, { value: "mail", label: "Mail" }, { value: "email", label: "Email" }];
161
+ medLegalCodes = ["ML200", "ML201", "ML202", "ML203", "ML204", "ML205", "ML206", "MLPRR"];
135
162
  draft = null;
136
163
  dirty = false;
137
164
  payerQuery = "";
138
165
  payerResults = [];
139
166
  documentType = "other";
140
167
  pendingFile = null;
141
- route = "ebill";
168
+ deliveryOptions = null;
169
+ submission = { route: "ebill" };
170
+ showSubmit = false;
171
+ deliveryBusy = false;
142
172
  panel = "";
143
173
  notice = "";
144
174
  closeReason = "";
@@ -191,20 +221,71 @@ class MindBillBillLifecycleComponent {
191
221
  return; this.draft.injury.claimsAdminId = payer.id; this.draft.injury.claimsAdminName = payer.name; this.payerQuery = ""; this.payerResults = []; this.dirty = true; }
192
222
  clearPayer() { if (!this.draft)
193
223
  return; this.draft.injury.claimsAdminId = ""; this.draft.injury.claimsAdminName = ""; this.dirty = true; }
194
- addLine() { this.draft?.bill.lineItems.push({ code: "", modifiers: [], units: 1, charge: 0 }); this.dirty = true; }
195
- removeLine(index) { this.draft?.bill.lineItems.splice(index, 1); this.dirty = true; }
196
- setModifiers(line, value) { line.modifiers = value.split(",").map((item) => item.trim().replace(/^-/, "")).filter(Boolean); this.dirty = true; }
224
+ lineChanged() { this.normalizeLineItems(); this.dirty = true; }
225
+ removeLine(index) { this.draft?.bill.lineItems.splice(index, 1); this.normalizeLineItems(); this.dirty = true; }
226
+ setModifiers(line, value) { line.modifiers = value.split(",").map((item) => item.trim().replace(/^-/, "")).filter(Boolean); this.normalizeLineItems(); this.dirty = true; }
227
+ setDiagnosisPointers(line, value) { line.diagnosisPointers = value.split(",").map((item) => Number(item.trim())).filter((item) => Number.isInteger(item) && item >= 1 && item <= 12); this.normalizeLineItems(); this.dirty = true; }
228
+ applyPreset(kind) {
229
+ if (!this.draft)
230
+ return;
231
+ const modifier = kind === "ame" ? "94" : kind === "psych" ? "96" : "95";
232
+ const entered = this.draft.bill.lineItems.filter((line) => line.code.trim());
233
+ if (entered.length === 0)
234
+ entered.push({ code: "ML201", modifiers: [modifier], units: 1, charge: 0 });
235
+ else
236
+ entered.forEach((line) => { if (!line.modifiers.length)
237
+ line.modifiers = [modifier]; });
238
+ this.draft.bill.lineItems = ensureTrailingProcedureLine(entered);
239
+ this.dirty = true;
240
+ }
197
241
  fileSelected(event) { this.pendingFile = event.target.files?.[0] ?? null; }
198
242
  async attach() { if (!this.pendingFile)
199
243
  return; await this.store.addAttachment(this.pendingFile, this.documentType); this.pendingFile = null; this.notice = "Document attached."; }
200
244
  async removeAttachment(id) { await this.store.removeAttachment(id); this.notice = "Document removed."; }
201
245
  async openAttachment(id, filename) { this.openBlob(await this.store.getAttachment(id), filename); }
202
246
  async openEor(id, filename) { this.openBlob(await this.store.getEor(id), filename); }
203
- canSubmit() { const input = this.buildInput(); return Boolean(input && input.claimsAdminId && input.dos && input.billingProvider?.name && input.billingProvider.taxId && input.billingProvider.npi && input.renderingProvider?.name && input.renderingProvider.npi && input.lineItems.some((line) => line.code && line.units > 0)); }
247
+ canSubmit() {
248
+ const input = this.buildInput();
249
+ if (!input || !input.claimsAdminId || !input.billingProvider?.name || !input.billingProvider.taxId || !input.billingProvider.npi || !input.renderingProvider?.name || !input.renderingProvider.npi || !input.lineItems.length)
250
+ return false;
251
+ return input.lineItems.every((line) => line.code && line.units > 0 && (this.draft?.bill.billingMode !== "professional" || Boolean(line.serviceDate && line.charge && line.charge > 0)));
252
+ }
204
253
  async save() { const input = this.buildInput(); if (!input)
205
254
  return; const data = await this.store.saveReview(input); this.dirty = false; this.draft = this.makeDraft(data); this.notice = "Bill saved."; }
206
- async submit() { const input = this.buildInput(); if (!input || !this.canSubmit())
207
- return; const data = await this.store.submitBill(input, this.route); this.dirty = false; this.draft = this.makeDraft(data); this.notice = "Bill submitted."; this.submitted.emit(data); }
255
+ async openSubmit() {
256
+ if (!this.canSubmit())
257
+ return;
258
+ this.deliveryBusy = true;
259
+ try {
260
+ this.deliveryOptions = await this.store.getDeliveryOptions();
261
+ this.chooseDelivery(this.deliveryOptions.recommended);
262
+ this.showSubmit = true;
263
+ }
264
+ finally {
265
+ this.deliveryBusy = false;
266
+ }
267
+ }
268
+ closeSubmit() { this.showSubmit = false; }
269
+ selectedDelivery() { return this.deliveryOptions?.options.find((option) => option.route === this.submission.route); }
270
+ chooseDelivery(option) {
271
+ const destination = option.route === "fax" ? { faxNumber: option.target ?? this.deliveryOptions?.contacts.faxNumber ?? "" }
272
+ : option.route === "email" ? { email: option.target ?? this.deliveryOptions?.contacts.claimsEmail ?? "" }
273
+ : option.route === "mail" ? { mailingAddress: option.target ?? this.deliveryOptions?.contacts.mailingAddress ?? "" }
274
+ : undefined;
275
+ this.submission = { route: option.route, ...(destination ? { destination } : {}) };
276
+ }
277
+ setDestination(key, value) { this.submission = { ...this.submission, destination: { ...this.submission.destination, [key]: value } }; }
278
+ deliveryReady() {
279
+ if (this.submission.route === "fax")
280
+ return Boolean(this.submission.destination?.faxNumber?.trim());
281
+ if (this.submission.route === "email")
282
+ return Boolean(this.submission.destination?.email?.trim());
283
+ if (this.submission.route === "mail")
284
+ return Boolean(this.submission.destination?.mailingAddress?.trim());
285
+ return true;
286
+ }
287
+ async submit() { const input = this.buildInput(); if (!input || !this.canSubmit() || !this.deliveryReady())
288
+ return; const data = await this.store.submitBill(input, this.submission); this.showSubmit = false; this.dirty = false; this.draft = this.makeDraft(data); this.notice = "Bill submitted."; this.submitted.emit(data); }
208
289
  beginAction(action) { if (action === "post_payment")
209
290
  this.panel = "payment";
210
291
  else if (action === "second_review" || action === "independent_bill_review")
@@ -246,19 +327,25 @@ class MindBillBillLifecycleComponent {
246
327
  billingProvider: this.draft.billingProvider,
247
328
  renderingProvider: this.draft.clinician,
248
329
  placeOfService: this.draft.location,
249
- lineItems: this.draft.bill.lineItems.map((line) => ({
330
+ lineItems: this.draft.bill.lineItems.filter((line) => line.code.trim()).map((line) => ({
250
331
  ...(line.id ? { id: line.id } : {}),
251
332
  code: line.code,
252
333
  modifiers: line.modifiers,
253
334
  units: line.units,
335
+ ...(line.charge > 0 ? { charge: line.charge } : {}),
336
+ ...(line.serviceDate ? { serviceDate: line.serviceDate } : {}),
337
+ ...(line.serviceDateEnd ? { serviceDateEnd: line.serviceDateEnd } : {}),
338
+ ...(line.diagnosisPointers?.length ? { diagnosisPointers: line.diagnosisPointers } : {}),
254
339
  })),
255
340
  };
256
341
  }
257
342
  makeDraft(data) {
258
343
  const snapshot = data.bill.billingSnapshot ?? {};
259
344
  const names = data.patient.name.trim().split(/\s+/);
260
- return { patient: { ...data.patient, firstName: data.patient.firstName ?? names[0] ?? "", lastName: data.patient.lastName ?? names.slice(1).join(" ") }, injury: { ...data.injury }, bill: { ...data.bill, lineItems: data.bill.lineItems.map((line) => ({ ...line, modifiers: [...line.modifiers] })) }, billingProvider: snapshot.billingProvider ?? { name: "", taxId: "", npi: "", billType: "Professional" }, clinician: snapshot.renderingProvider ?? { name: "", specialty: "", npi: "" }, location: snapshot.placeOfService ?? { name: "", street: "", city: "", state: "", zip: "", posCode: "11" } };
345
+ return { patient: { ...data.patient, firstName: data.patient.firstName ?? names[0] ?? "", lastName: data.patient.lastName ?? names.slice(1).join(" ") }, injury: { ...data.injury }, bill: { ...data.bill, lineItems: ensureTrailingProcedureLine(data.bill.lineItems) }, billingProvider: snapshot.billingProvider ?? { name: "", taxId: "", npi: "", billType: "Professional" }, clinician: snapshot.renderingProvider ?? { name: "", specialty: "", npi: "" }, location: snapshot.placeOfService ?? { name: "", street: "", city: "", state: "", zip: "", posCode: "11" } };
261
346
  }
347
+ normalizeLineItems() { if (this.draft)
348
+ this.draft.bill.lineItems = ensureTrailingProcedureLine(this.draft.bill.lineItems); }
262
349
  openBlob(blob, filename) { const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.target = "_blank"; link.rel = "noopener"; link.download = filename; link.click(); setTimeout(() => URL.revokeObjectURL(url), 60_000); }
263
350
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillLifecycleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
264
351
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillLifecycleComponent, isStandalone: true, selector: "mindbill-bill-lifecycle", inputs: { billId: "billId", sessionEndpoint: "sessionEndpoint", apiBaseUrl: "apiBaseUrl", getSession: "getSession", refreshInterval: "refreshInterval", appearance: "appearance" }, outputs: { billIdChange: "billIdChange", submitted: "submitted", billingError: "billingError" }, usesOnChanges: true, ngImport: i0, template: `
@@ -324,8 +411,29 @@ class MindBillBillLifecycleComponent {
324
411
  </section>
325
412
 
326
413
  <section class="card">
327
- <div class="card-title"><div><h3>Charges</h3><p>Review procedure codes, modifiers, units, and allowed amounts.</p></div><button type="button" (click)="addLine()">+ Add line</button></div>
328
- <div class="lines">@for (line of draft.bill.lineItems; track $index; let i=$index) { <div class="line"><label><span>Procedure</span><input required [(ngModel)]="line.code" [name]="'code'+i" (ngModelChange)="dirty=true"></label><label><span>Modifiers</span><input [ngModel]="line.modifiers.join(', ')" (ngModelChange)="setModifiers(line,$any($event))" [name]="'modifiers'+i" placeholder="95, 93"></label><label><span>Units</span><input required type="number" min="1" [(ngModel)]="line.units" [name]="'units'+i" (ngModelChange)="dirty=true"></label><div class="allowed"><span>Allowed</span><strong>{{ line.charge | currency }}</strong></div><button type="button" class="remove" (click)="removeLine(i)" aria-label="Remove line">×</button></div> }</div>
414
+ <div class="card-title"><div><h3>Charges</h3><p>{{ draft.bill.billingMode === 'professional' ? 'Enter each treatment or professional service.' : 'Choose medical-legal codes, modifiers, units, and allowed amounts.' }} A keyboard-ready row appears automatically.</p></div></div>
415
+ @if (draft.bill.billingMode === 'med_legal') {
416
+ <div class="presets"><button type="button" (click)="applyPreset('qme')">QME</button><button type="button" (click)="applyPreset('ame')">AME</button><button type="button" (click)="applyPreset('psych')">Psych QME</button></div>
417
+ }
418
+ <div class="lines">@for (line of draft.bill.lineItems; track $index; let i=$index) {
419
+ <div class="line" [class.professional]="draft.bill.billingMode === 'professional'">
420
+ <label><span>Procedure</span>
421
+ @if (draft.bill.billingMode === 'med_legal') {
422
+ <select [(ngModel)]="line.code" [name]="'code'+i" (ngModelChange)="lineChanged()"><option value="">Choose code…</option>@for (code of medLegalCodes; track code) { <option [value]="code">{{ code }}</option> }</select>
423
+ } @else { <input [(ngModel)]="line.code" [name]="'code'+i" (ngModelChange)="lineChanged()" placeholder="e.g. 99204"> }
424
+ </label>
425
+ <label><span>Modifiers</span><input [ngModel]="line.modifiers.join(', ')" (ngModelChange)="setModifiers(line,$any($event))" [name]="'modifiers'+i" placeholder="e.g. 25, 95"></label>
426
+ @if (draft.bill.billingMode === 'professional') {
427
+ <label><span>Service date</span><input type="date" [(ngModel)]="line.serviceDate" [name]="'serviceDate'+i" (ngModelChange)="lineChanged()"></label>
428
+ <label><span>End date</span><input type="date" [(ngModel)]="line.serviceDateEnd" [name]="'serviceDateEnd'+i" (ngModelChange)="lineChanged()"></label>
429
+ <label><span>Charge</span><input type="number" min="0" step="0.01" [(ngModel)]="line.charge" [name]="'charge'+i" (ngModelChange)="lineChanged()"></label>
430
+ <label><span>Diagnosis pointers</span><input [ngModel]="(line.diagnosisPointers || []).join(', ')" (ngModelChange)="setDiagnosisPointers(line,$any($event))" [name]="'pointers'+i" placeholder="1, 2"></label>
431
+ }
432
+ <label><span>Units</span><input type="number" min="1" [(ngModel)]="line.units" [name]="'units'+i" (ngModelChange)="lineChanged()"></label>
433
+ <div class="allowed"><span>{{ draft.bill.billingMode === 'professional' ? 'Charge' : 'Allowed' }}</span><strong>{{ line.charge | currency }}</strong></div>
434
+ <button type="button" class="remove" (click)="removeLine(i)" aria-label="Remove line">×</button>
435
+ </div>
436
+ }</div>
329
437
  </section>
330
438
 
331
439
  <section class="card">
@@ -334,7 +442,25 @@ class MindBillBillLifecycleComponent {
334
442
  <div class="upload"><select [(ngModel)]="documentType" name="documentType">@for (type of documentTypes; track type.value) { <option [value]="type.value">{{ type.label }}</option> }</select><input #fileInput type="file" accept="application/pdf" (change)="fileSelected($event)"><button type="button" [disabled]="!pendingFile || store.mutating()" (click)="attach()">Attach document</button></div>
335
443
  </section>
336
444
 
337
- <section class="delivery"><div><span class="eyebrow">Delivery</span><h3>Submit this bill</h3><p>Submission and every later action remain available in this case.</p></div><div><div class="routes">@for (item of routes; track item.value) { <label><input type="radio" name="route" [value]="item.value" [(ngModel)]="route"> {{ item.label }}</label> }</div><div class="actions"><button type="button" (click)="save()" [disabled]="store.mutating()">Save changes</button><button class="primary" type="submit" [disabled]="store.mutating() || !canSubmit()">{{ store.mutating() ? 'Submitting…' : 'Submit bill' }}</button></div></div></section>
445
+ <section class="delivery"><div><span class="eyebrow">Delivery</span><h3>Submit this bill</h3><p>Review the exact electronic, fax, email, or mail destination before anything is sent.</p></div><div class="actions"><button type="button" (click)="save()" [disabled]="store.mutating()">Save changes</button><button class="primary" type="button" (click)="openSubmit()" [disabled]="store.mutating() || deliveryBusy || !canSubmit()">{{ deliveryBusy ? 'Loading routes…' : 'Review delivery' }}</button></div></section>
446
+
447
+ @if (showSubmit && deliveryOptions) {
448
+ <div class="modal-backdrop" (click)="closeSubmit()">
449
+ <section class="submit-modal" role="dialog" aria-modal="true" aria-labelledby="mb-submit-title" (click)="$event.stopPropagation()">
450
+ <header><div><span class="eyebrow">Send bill</span><h3 id="mb-submit-title">Choose a delivery route</h3><p>{{ deliveryOptions.payerName }}</p></div><button type="button" class="close" (click)="closeSubmit()" aria-label="Close">×</button></header>
451
+ <div class="route-list">@for (option of deliveryOptions.options; track option.route) {
452
+ <button type="button" class="route-option" [class.selected]="submission.route === option.route" (click)="chooseDelivery(option)"><span class="radio"></span><span><strong>{{ option.label }}</strong><small>{{ option.detail }}</small></span>@if (option.route === deliveryOptions.recommended.route) { <b>Recommended</b> }</button>
453
+ }</div>
454
+ @if (selectedDelivery(); as option) {
455
+ @if (option.route === 'fax') { <label><span>Fax number</span><input [ngModel]="submission.destination?.faxNumber || ''" (ngModelChange)="setDestination('faxNumber',$any($event))" name="submitFax"></label> }
456
+ @if (option.route === 'email') { <label><span>Email address</span><input type="email" [ngModel]="submission.destination?.email || ''" (ngModelChange)="setDestination('email',$any($event))" name="submitEmail"></label><label><span>Subject</span><input [(ngModel)]="submission.subject" name="submitSubject"></label> }
457
+ @if (option.route === 'mail') { <label><span>Mailing address</span><textarea [ngModel]="submission.destination?.mailingAddress || ''" (ngModelChange)="setDestination('mailingAddress',$any($event))" name="submitMail"></textarea></label> }
458
+ @if (option.route !== 'ebill') { <label><span>Attention (optional)</span><input [(ngModel)]="submission.attention" name="submitAttention"></label> }
459
+ }
460
+ <footer><button type="button" (click)="closeSubmit()">Cancel</button><button type="button" class="primary" (click)="submit()" [disabled]="store.mutating() || !deliveryReady()">{{ store.mutating() ? 'Submitting…' : 'Submit bill' }}</button></footer>
461
+ </section>
462
+ </div>
463
+ }
338
464
  </form>
339
465
  } @else {
340
466
  <section class="card lifecycle">
@@ -352,7 +478,7 @@ class MindBillBillLifecycleComponent {
352
478
  }
353
479
  <datalist id="mb-states"><option value="CA"></option><option value="AZ"></option><option value="NV"></option><option value="OR"></option><option value="WA"></option><option value="TX"></option><option value="NY"></option></datalist>
354
480
  </section>
355
- `, isInline: true, styles: [":host{display:block}.mb{--a:#238dbd;--ac:#fff;--bg:#f3f8fa;--s:#fff;--t:#203743;--m:#657982;--b:#dbe6ea;--r:12px;--cr:8px;display:grid;gap:16px;color:var(--t);font:14px/1.45 var(--font,Inter,system-ui,sans-serif)}*{box-sizing:border-box}h2,h3,h4,p{margin:0}.summary,.card,.delivery,.state{border:1px solid var(--b);border-radius:var(--r);background:var(--s)}.summary{display:flex;justify-content:space-between;gap:20px;padding:20px}.summary h2{font-size:24px}.summary p,.card p,.delivery p,small{color:var(--m)}.eyebrow{color:var(--m);font-size:11px;font-weight:800;letter-spacing:.13em;text-transform:uppercase}.money{text-align:right}.money span{display:block;font-size:28px;font-weight:800}.money small{font-size:12px}.review{display:grid;gap:16px}.card{padding:20px}.card-title{display:flex;align-items:start;justify-content:space-between;gap:16px;margin-bottom:18px}.card-title h3,.delivery h3{font-size:18px}.card-title>span{border-radius:999px;background:var(--bg);padding:5px 9px;color:var(--m);font-size:11px;font-weight:800;text-transform:uppercase}.grid{display:grid;gap:14px}.grid.four{grid-template-columns:repeat(4,minmax(0,1fr))}.grid.three{grid-template-columns:repeat(3,minmax(0,1fr))}.grid.two{grid-template-columns:repeat(2,minmax(0,1fr))}label{display:grid;gap:6px;color:var(--t);font-size:12px;font-weight:700}input,select,textarea,button{font:inherit}input,select,textarea{width:100%;min-height:42px;border:1px solid var(--b);border-radius:var(--cr);background:#fff;color:var(--t);padding:9px 11px}textarea{min-height:90px;resize:vertical}button{min-height:38px;border:1px solid var(--b);border-radius:var(--cr);background:#fff;color:var(--t);padding:8px 13px;cursor:pointer;font-weight:700}button.primary{border-color:var(--a);background:var(--a);color:var(--ac)}button.danger{border-color:#d4380d;background:#d4380d;color:#fff}button:disabled{cursor:not-allowed;opacity:.5}.subhead{margin:18px 0 9px;border-top:1px solid var(--b);padding-top:15px;font-size:12px;font-weight:800}.selected{display:flex;align-items:center;justify-content:space-between;border:1px solid var(--b);border-radius:var(--cr);padding:12px}.selected div{display:grid}.results{position:relative;z-index:3;display:grid;margin-top:5px;border:1px solid var(--b);border-radius:var(--cr);background:#fff;box-shadow:0 12px 30px #1f2d3d1f;overflow:hidden}.results button{display:grid;gap:2px;text-align:left;border:0;border-bottom:1px solid var(--b);border-radius:0;padding:12px}.results span{color:var(--m);font-weight:400}.lines{display:grid;gap:10px}.line{display:grid;grid-template-columns:1.2fr 1.2fr 110px 120px 40px;align-items:end;gap:10px;border-radius:var(--cr);background:var(--bg);padding:12px}.allowed{display:grid;gap:7px;text-align:right}.allowed span{color:var(--m);font-size:11px;font-weight:800;text-transform:uppercase}.allowed strong{font-size:17px}.remove{border:0;background:transparent;font-size:20px}.docs{list-style:none;margin:0;padding:0}.docs li{display:flex;align-items:center;gap:8px;border-top:1px solid var(--b);padding:11px 0}.docs li>div{display:grid;flex:1}.upload{display:grid;grid-template-columns:240px 1fr auto;gap:10px;margin-top:12px;border-radius:var(--cr);background:var(--bg);padding:12px}.delivery{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:20px;background:color-mix(in srgb,var(--a) 5%,white)}.routes,.actions,.actionbar{display:flex;justify-content:flex-end;gap:8px}.routes{margin-bottom:10px}.routes label{display:flex;align-items:center;border:1px solid var(--b);border-radius:var(--cr);background:#fff;padding:9px 12px}.routes input{width:auto;min-height:auto}.lifecycle{display:grid;gap:16px}.actionbar{flex-wrap:wrap}.eors{display:grid;gap:8px}.eors>button{display:flex;align-items:center;justify-content:space-between;text-align:left}.eors span{display:grid}.panel{display:grid;gap:14px;border-top:1px solid var(--b);padding-top:16px}.notice,.state{padding:13px 15px}.notice{border-radius:var(--cr);background:#edf8f2;color:#23734c}.notice.error,.state.error{background:#fff2f0;color:#b42318}.state{display:grid;gap:10px}.state button{justify-self:start}.powered{text-align:right;color:var(--m);font-size:11px}@media(max-width:900px){.grid.four,.grid.three{grid-template-columns:repeat(2,minmax(0,1fr))}.line{grid-template-columns:1fr 1fr 90px}.allowed{grid-column:1/-2;text-align:left}.upload{grid-template-columns:1fr}.delivery{align-items:stretch;flex-direction:column}.routes,.actions{justify-content:flex-start}}@media(max-width:600px){.summary,.card-title{align-items:stretch;flex-direction:column}.money{text-align:left}.grid.four,.grid.three,.grid.two,.line{grid-template-columns:1fr}.allowed{grid-column:auto}.routes{flex-wrap:wrap}.docs li{align-items:flex-start;flex-wrap:wrap}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
481
+ `, isInline: true, styles: [":host{display:block}.mb{--a:#238dbd;--ac:#fff;--bg:#f3f8fa;--s:#fff;--t:#203743;--m:#657982;--b:#dbe6ea;--r:12px;--cr:8px;display:grid;gap:16px;color:var(--t);font:14px/1.45 var(--font,Inter,system-ui,sans-serif)}*{box-sizing:border-box}h2,h3,h4,p{margin:0}.summary,.card,.delivery,.state{border:1px solid var(--b);border-radius:var(--r);background:var(--s)}.summary{display:flex;justify-content:space-between;gap:20px;padding:20px}.summary h2{font-size:24px}.summary p,.card p,.delivery p,small{color:var(--m)}.eyebrow{color:var(--m);font-size:11px;font-weight:800;letter-spacing:.13em;text-transform:uppercase}.money{text-align:right}.money span{display:block;font-size:28px;font-weight:800}.money small{font-size:12px}.review{display:grid;gap:16px}.card{padding:20px}.card-title{display:flex;align-items:start;justify-content:space-between;gap:16px;margin-bottom:18px}.card-title h3,.delivery h3{font-size:18px}.card-title>span{border-radius:999px;background:var(--bg);padding:5px 9px;color:var(--m);font-size:11px;font-weight:800;text-transform:uppercase}.grid{display:grid;gap:14px}.grid.four{grid-template-columns:repeat(4,minmax(0,1fr))}.grid.three{grid-template-columns:repeat(3,minmax(0,1fr))}.grid.two{grid-template-columns:repeat(2,minmax(0,1fr))}label{display:grid;gap:6px;color:var(--t);font-size:12px;font-weight:700}input,select,textarea,button{font:inherit}input,select,textarea{width:100%;min-height:42px;border:1px solid var(--b);border-radius:var(--cr);background:#fff;color:var(--t);padding:9px 11px}textarea{min-height:90px;resize:vertical}button{min-height:38px;border:1px solid var(--b);border-radius:var(--cr);background:#fff;color:var(--t);padding:8px 13px;cursor:pointer;font-weight:700}button.primary{border-color:var(--a);background:var(--a);color:var(--ac)}button.danger{border-color:#d4380d;background:#d4380d;color:#fff}button:disabled{cursor:not-allowed;opacity:.5}.subhead{margin:18px 0 9px;border-top:1px solid var(--b);padding-top:15px;font-size:12px;font-weight:800}.selected{display:flex;align-items:center;justify-content:space-between;border:1px solid var(--b);border-radius:var(--cr);padding:12px}.selected div{display:grid}.results{position:relative;z-index:3;display:grid;margin-top:5px;border:1px solid var(--b);border-radius:var(--cr);background:#fff;box-shadow:0 12px 30px #1f2d3d1f;overflow:hidden}.results button{display:grid;gap:2px;text-align:left;border:0;border-bottom:1px solid var(--b);border-radius:0;padding:12px}.results span{color:var(--m);font-weight:400}.presets{display:flex;gap:8px;margin:-4px 0 12px}.lines{display:grid;gap:10px}.line{display:grid;grid-template-columns:1.2fr 1.2fr 110px 120px 40px;align-items:end;gap:10px;border-radius:var(--cr);background:var(--bg);padding:12px}.line.professional{grid-template-columns:1.1fr 1fr 1fr 1fr 110px 1fr 90px 110px 40px}.allowed{display:grid;gap:7px;text-align:right}.allowed span{color:var(--m);font-size:11px;font-weight:800;text-transform:uppercase}.allowed strong{font-size:17px}.remove{border:0;background:transparent;font-size:20px}.docs{list-style:none;margin:0;padding:0}.docs li{display:flex;align-items:center;gap:8px;border-top:1px solid var(--b);padding:11px 0}.docs li>div{display:grid;flex:1}.upload{display:grid;grid-template-columns:240px 1fr auto;gap:10px;margin-top:12px;border-radius:var(--cr);background:var(--bg);padding:12px}.delivery{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:20px;background:color-mix(in srgb,var(--a) 5%,white)}.actions,.actionbar{display:flex;justify-content:flex-end;gap:8px}.lifecycle{display:grid;gap:16px}.actionbar{flex-wrap:wrap}.eors{display:grid;gap:8px}.eors>button{display:flex;align-items:center;justify-content:space-between;text-align:left}.eors span{display:grid}.panel{display:grid;gap:14px;border-top:1px solid var(--b);padding-top:16px}.notice,.state{padding:13px 15px}.notice{border-radius:var(--cr);background:#edf8f2;color:#23734c}.notice.error,.state.error{background:#fff2f0;color:#b42318}.state{display:grid;gap:10px}.state button{justify-self:start}.powered{text-align:right;color:var(--m);font-size:11px}.modal-backdrop{position:fixed;z-index:1000;inset:0;display:grid;place-items:center;background:#0f20288f;padding:20px}.submit-modal{display:grid;gap:18px;width:min(680px,100%);max-height:calc(100vh - 40px);overflow:auto;border:1px solid var(--b);border-radius:var(--r);background:var(--s);padding:22px;box-shadow:0 24px 80px #0f202847}.submit-modal>header,.submit-modal>footer{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.submit-modal>footer{justify-content:flex-end}.submit-modal .close{border:0;background:transparent;font-size:26px}.route-list{display:grid;gap:8px}.route-option{display:grid;grid-template-columns:20px 1fr auto;align-items:center;gap:12px;text-align:left;padding:13px}.route-option>span:nth-child(2){display:grid}.route-option.selected{border-color:var(--a);box-shadow:0 0 0 2px color-mix(in srgb,var(--a) 18%,transparent)}.route-option .radio{width:16px;height:16px;border:2px solid var(--b);border-radius:50%}.route-option.selected .radio{border:5px solid var(--a)}.route-option b{color:var(--a);font-size:11px;text-transform:uppercase}@media(max-width:1100px){.line.professional{grid-template-columns:repeat(3,minmax(0,1fr))}.line.professional .allowed{grid-column:auto}}@media(max-width:900px){.grid.four,.grid.three{grid-template-columns:repeat(2,minmax(0,1fr))}.line{grid-template-columns:1fr 1fr 90px}.allowed{grid-column:1/-2;text-align:left}.upload{grid-template-columns:1fr}.delivery{align-items:stretch;flex-direction:column}.actions{justify-content:flex-start}}@media(max-width:600px){.summary,.card-title{align-items:stretch;flex-direction:column}.money{text-align:left}.grid.four,.grid.three,.grid.two,.line,.line.professional{grid-template-columns:1fr}.allowed{grid-column:auto}.docs li{align-items:flex-start;flex-wrap:wrap}.modal-backdrop{padding:8px}.submit-modal{max-height:calc(100vh - 16px);padding:16px}.route-option{grid-template-columns:20px 1fr}.route-option b{grid-column:2}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
356
482
  }
357
483
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillLifecycleComponent, decorators: [{
358
484
  type: Component,
@@ -419,8 +545,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
419
545
  </section>
420
546
 
421
547
  <section class="card">
422
- <div class="card-title"><div><h3>Charges</h3><p>Review procedure codes, modifiers, units, and allowed amounts.</p></div><button type="button" (click)="addLine()">+ Add line</button></div>
423
- <div class="lines">@for (line of draft.bill.lineItems; track $index; let i=$index) { <div class="line"><label><span>Procedure</span><input required [(ngModel)]="line.code" [name]="'code'+i" (ngModelChange)="dirty=true"></label><label><span>Modifiers</span><input [ngModel]="line.modifiers.join(', ')" (ngModelChange)="setModifiers(line,$any($event))" [name]="'modifiers'+i" placeholder="95, 93"></label><label><span>Units</span><input required type="number" min="1" [(ngModel)]="line.units" [name]="'units'+i" (ngModelChange)="dirty=true"></label><div class="allowed"><span>Allowed</span><strong>{{ line.charge | currency }}</strong></div><button type="button" class="remove" (click)="removeLine(i)" aria-label="Remove line">×</button></div> }</div>
548
+ <div class="card-title"><div><h3>Charges</h3><p>{{ draft.bill.billingMode === 'professional' ? 'Enter each treatment or professional service.' : 'Choose medical-legal codes, modifiers, units, and allowed amounts.' }} A keyboard-ready row appears automatically.</p></div></div>
549
+ @if (draft.bill.billingMode === 'med_legal') {
550
+ <div class="presets"><button type="button" (click)="applyPreset('qme')">QME</button><button type="button" (click)="applyPreset('ame')">AME</button><button type="button" (click)="applyPreset('psych')">Psych QME</button></div>
551
+ }
552
+ <div class="lines">@for (line of draft.bill.lineItems; track $index; let i=$index) {
553
+ <div class="line" [class.professional]="draft.bill.billingMode === 'professional'">
554
+ <label><span>Procedure</span>
555
+ @if (draft.bill.billingMode === 'med_legal') {
556
+ <select [(ngModel)]="line.code" [name]="'code'+i" (ngModelChange)="lineChanged()"><option value="">Choose code…</option>@for (code of medLegalCodes; track code) { <option [value]="code">{{ code }}</option> }</select>
557
+ } @else { <input [(ngModel)]="line.code" [name]="'code'+i" (ngModelChange)="lineChanged()" placeholder="e.g. 99204"> }
558
+ </label>
559
+ <label><span>Modifiers</span><input [ngModel]="line.modifiers.join(', ')" (ngModelChange)="setModifiers(line,$any($event))" [name]="'modifiers'+i" placeholder="e.g. 25, 95"></label>
560
+ @if (draft.bill.billingMode === 'professional') {
561
+ <label><span>Service date</span><input type="date" [(ngModel)]="line.serviceDate" [name]="'serviceDate'+i" (ngModelChange)="lineChanged()"></label>
562
+ <label><span>End date</span><input type="date" [(ngModel)]="line.serviceDateEnd" [name]="'serviceDateEnd'+i" (ngModelChange)="lineChanged()"></label>
563
+ <label><span>Charge</span><input type="number" min="0" step="0.01" [(ngModel)]="line.charge" [name]="'charge'+i" (ngModelChange)="lineChanged()"></label>
564
+ <label><span>Diagnosis pointers</span><input [ngModel]="(line.diagnosisPointers || []).join(', ')" (ngModelChange)="setDiagnosisPointers(line,$any($event))" [name]="'pointers'+i" placeholder="1, 2"></label>
565
+ }
566
+ <label><span>Units</span><input type="number" min="1" [(ngModel)]="line.units" [name]="'units'+i" (ngModelChange)="lineChanged()"></label>
567
+ <div class="allowed"><span>{{ draft.bill.billingMode === 'professional' ? 'Charge' : 'Allowed' }}</span><strong>{{ line.charge | currency }}</strong></div>
568
+ <button type="button" class="remove" (click)="removeLine(i)" aria-label="Remove line">×</button>
569
+ </div>
570
+ }</div>
424
571
  </section>
425
572
 
426
573
  <section class="card">
@@ -429,7 +576,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
429
576
  <div class="upload"><select [(ngModel)]="documentType" name="documentType">@for (type of documentTypes; track type.value) { <option [value]="type.value">{{ type.label }}</option> }</select><input #fileInput type="file" accept="application/pdf" (change)="fileSelected($event)"><button type="button" [disabled]="!pendingFile || store.mutating()" (click)="attach()">Attach document</button></div>
430
577
  </section>
431
578
 
432
- <section class="delivery"><div><span class="eyebrow">Delivery</span><h3>Submit this bill</h3><p>Submission and every later action remain available in this case.</p></div><div><div class="routes">@for (item of routes; track item.value) { <label><input type="radio" name="route" [value]="item.value" [(ngModel)]="route"> {{ item.label }}</label> }</div><div class="actions"><button type="button" (click)="save()" [disabled]="store.mutating()">Save changes</button><button class="primary" type="submit" [disabled]="store.mutating() || !canSubmit()">{{ store.mutating() ? 'Submitting…' : 'Submit bill' }}</button></div></div></section>
579
+ <section class="delivery"><div><span class="eyebrow">Delivery</span><h3>Submit this bill</h3><p>Review the exact electronic, fax, email, or mail destination before anything is sent.</p></div><div class="actions"><button type="button" (click)="save()" [disabled]="store.mutating()">Save changes</button><button class="primary" type="button" (click)="openSubmit()" [disabled]="store.mutating() || deliveryBusy || !canSubmit()">{{ deliveryBusy ? 'Loading routes…' : 'Review delivery' }}</button></div></section>
580
+
581
+ @if (showSubmit && deliveryOptions) {
582
+ <div class="modal-backdrop" (click)="closeSubmit()">
583
+ <section class="submit-modal" role="dialog" aria-modal="true" aria-labelledby="mb-submit-title" (click)="$event.stopPropagation()">
584
+ <header><div><span class="eyebrow">Send bill</span><h3 id="mb-submit-title">Choose a delivery route</h3><p>{{ deliveryOptions.payerName }}</p></div><button type="button" class="close" (click)="closeSubmit()" aria-label="Close">×</button></header>
585
+ <div class="route-list">@for (option of deliveryOptions.options; track option.route) {
586
+ <button type="button" class="route-option" [class.selected]="submission.route === option.route" (click)="chooseDelivery(option)"><span class="radio"></span><span><strong>{{ option.label }}</strong><small>{{ option.detail }}</small></span>@if (option.route === deliveryOptions.recommended.route) { <b>Recommended</b> }</button>
587
+ }</div>
588
+ @if (selectedDelivery(); as option) {
589
+ @if (option.route === 'fax') { <label><span>Fax number</span><input [ngModel]="submission.destination?.faxNumber || ''" (ngModelChange)="setDestination('faxNumber',$any($event))" name="submitFax"></label> }
590
+ @if (option.route === 'email') { <label><span>Email address</span><input type="email" [ngModel]="submission.destination?.email || ''" (ngModelChange)="setDestination('email',$any($event))" name="submitEmail"></label><label><span>Subject</span><input [(ngModel)]="submission.subject" name="submitSubject"></label> }
591
+ @if (option.route === 'mail') { <label><span>Mailing address</span><textarea [ngModel]="submission.destination?.mailingAddress || ''" (ngModelChange)="setDestination('mailingAddress',$any($event))" name="submitMail"></textarea></label> }
592
+ @if (option.route !== 'ebill') { <label><span>Attention (optional)</span><input [(ngModel)]="submission.attention" name="submitAttention"></label> }
593
+ }
594
+ <footer><button type="button" (click)="closeSubmit()">Cancel</button><button type="button" class="primary" (click)="submit()" [disabled]="store.mutating() || !deliveryReady()">{{ store.mutating() ? 'Submitting…' : 'Submit bill' }}</button></footer>
595
+ </section>
596
+ </div>
597
+ }
433
598
  </form>
434
599
  } @else {
435
600
  <section class="card lifecycle">
@@ -447,7 +612,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
447
612
  }
448
613
  <datalist id="mb-states"><option value="CA"></option><option value="AZ"></option><option value="NV"></option><option value="OR"></option><option value="WA"></option><option value="TX"></option><option value="NY"></option></datalist>
449
614
  </section>
450
- `, styles: [":host{display:block}.mb{--a:#238dbd;--ac:#fff;--bg:#f3f8fa;--s:#fff;--t:#203743;--m:#657982;--b:#dbe6ea;--r:12px;--cr:8px;display:grid;gap:16px;color:var(--t);font:14px/1.45 var(--font,Inter,system-ui,sans-serif)}*{box-sizing:border-box}h2,h3,h4,p{margin:0}.summary,.card,.delivery,.state{border:1px solid var(--b);border-radius:var(--r);background:var(--s)}.summary{display:flex;justify-content:space-between;gap:20px;padding:20px}.summary h2{font-size:24px}.summary p,.card p,.delivery p,small{color:var(--m)}.eyebrow{color:var(--m);font-size:11px;font-weight:800;letter-spacing:.13em;text-transform:uppercase}.money{text-align:right}.money span{display:block;font-size:28px;font-weight:800}.money small{font-size:12px}.review{display:grid;gap:16px}.card{padding:20px}.card-title{display:flex;align-items:start;justify-content:space-between;gap:16px;margin-bottom:18px}.card-title h3,.delivery h3{font-size:18px}.card-title>span{border-radius:999px;background:var(--bg);padding:5px 9px;color:var(--m);font-size:11px;font-weight:800;text-transform:uppercase}.grid{display:grid;gap:14px}.grid.four{grid-template-columns:repeat(4,minmax(0,1fr))}.grid.three{grid-template-columns:repeat(3,minmax(0,1fr))}.grid.two{grid-template-columns:repeat(2,minmax(0,1fr))}label{display:grid;gap:6px;color:var(--t);font-size:12px;font-weight:700}input,select,textarea,button{font:inherit}input,select,textarea{width:100%;min-height:42px;border:1px solid var(--b);border-radius:var(--cr);background:#fff;color:var(--t);padding:9px 11px}textarea{min-height:90px;resize:vertical}button{min-height:38px;border:1px solid var(--b);border-radius:var(--cr);background:#fff;color:var(--t);padding:8px 13px;cursor:pointer;font-weight:700}button.primary{border-color:var(--a);background:var(--a);color:var(--ac)}button.danger{border-color:#d4380d;background:#d4380d;color:#fff}button:disabled{cursor:not-allowed;opacity:.5}.subhead{margin:18px 0 9px;border-top:1px solid var(--b);padding-top:15px;font-size:12px;font-weight:800}.selected{display:flex;align-items:center;justify-content:space-between;border:1px solid var(--b);border-radius:var(--cr);padding:12px}.selected div{display:grid}.results{position:relative;z-index:3;display:grid;margin-top:5px;border:1px solid var(--b);border-radius:var(--cr);background:#fff;box-shadow:0 12px 30px #1f2d3d1f;overflow:hidden}.results button{display:grid;gap:2px;text-align:left;border:0;border-bottom:1px solid var(--b);border-radius:0;padding:12px}.results span{color:var(--m);font-weight:400}.lines{display:grid;gap:10px}.line{display:grid;grid-template-columns:1.2fr 1.2fr 110px 120px 40px;align-items:end;gap:10px;border-radius:var(--cr);background:var(--bg);padding:12px}.allowed{display:grid;gap:7px;text-align:right}.allowed span{color:var(--m);font-size:11px;font-weight:800;text-transform:uppercase}.allowed strong{font-size:17px}.remove{border:0;background:transparent;font-size:20px}.docs{list-style:none;margin:0;padding:0}.docs li{display:flex;align-items:center;gap:8px;border-top:1px solid var(--b);padding:11px 0}.docs li>div{display:grid;flex:1}.upload{display:grid;grid-template-columns:240px 1fr auto;gap:10px;margin-top:12px;border-radius:var(--cr);background:var(--bg);padding:12px}.delivery{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:20px;background:color-mix(in srgb,var(--a) 5%,white)}.routes,.actions,.actionbar{display:flex;justify-content:flex-end;gap:8px}.routes{margin-bottom:10px}.routes label{display:flex;align-items:center;border:1px solid var(--b);border-radius:var(--cr);background:#fff;padding:9px 12px}.routes input{width:auto;min-height:auto}.lifecycle{display:grid;gap:16px}.actionbar{flex-wrap:wrap}.eors{display:grid;gap:8px}.eors>button{display:flex;align-items:center;justify-content:space-between;text-align:left}.eors span{display:grid}.panel{display:grid;gap:14px;border-top:1px solid var(--b);padding-top:16px}.notice,.state{padding:13px 15px}.notice{border-radius:var(--cr);background:#edf8f2;color:#23734c}.notice.error,.state.error{background:#fff2f0;color:#b42318}.state{display:grid;gap:10px}.state button{justify-self:start}.powered{text-align:right;color:var(--m);font-size:11px}@media(max-width:900px){.grid.four,.grid.three{grid-template-columns:repeat(2,minmax(0,1fr))}.line{grid-template-columns:1fr 1fr 90px}.allowed{grid-column:1/-2;text-align:left}.upload{grid-template-columns:1fr}.delivery{align-items:stretch;flex-direction:column}.routes,.actions{justify-content:flex-start}}@media(max-width:600px){.summary,.card-title{align-items:stretch;flex-direction:column}.money{text-align:left}.grid.four,.grid.three,.grid.two,.line{grid-template-columns:1fr}.allowed{grid-column:auto}.routes{flex-wrap:wrap}.docs li{align-items:flex-start;flex-wrap:wrap}}\n"] }]
615
+ `, styles: [":host{display:block}.mb{--a:#238dbd;--ac:#fff;--bg:#f3f8fa;--s:#fff;--t:#203743;--m:#657982;--b:#dbe6ea;--r:12px;--cr:8px;display:grid;gap:16px;color:var(--t);font:14px/1.45 var(--font,Inter,system-ui,sans-serif)}*{box-sizing:border-box}h2,h3,h4,p{margin:0}.summary,.card,.delivery,.state{border:1px solid var(--b);border-radius:var(--r);background:var(--s)}.summary{display:flex;justify-content:space-between;gap:20px;padding:20px}.summary h2{font-size:24px}.summary p,.card p,.delivery p,small{color:var(--m)}.eyebrow{color:var(--m);font-size:11px;font-weight:800;letter-spacing:.13em;text-transform:uppercase}.money{text-align:right}.money span{display:block;font-size:28px;font-weight:800}.money small{font-size:12px}.review{display:grid;gap:16px}.card{padding:20px}.card-title{display:flex;align-items:start;justify-content:space-between;gap:16px;margin-bottom:18px}.card-title h3,.delivery h3{font-size:18px}.card-title>span{border-radius:999px;background:var(--bg);padding:5px 9px;color:var(--m);font-size:11px;font-weight:800;text-transform:uppercase}.grid{display:grid;gap:14px}.grid.four{grid-template-columns:repeat(4,minmax(0,1fr))}.grid.three{grid-template-columns:repeat(3,minmax(0,1fr))}.grid.two{grid-template-columns:repeat(2,minmax(0,1fr))}label{display:grid;gap:6px;color:var(--t);font-size:12px;font-weight:700}input,select,textarea,button{font:inherit}input,select,textarea{width:100%;min-height:42px;border:1px solid var(--b);border-radius:var(--cr);background:#fff;color:var(--t);padding:9px 11px}textarea{min-height:90px;resize:vertical}button{min-height:38px;border:1px solid var(--b);border-radius:var(--cr);background:#fff;color:var(--t);padding:8px 13px;cursor:pointer;font-weight:700}button.primary{border-color:var(--a);background:var(--a);color:var(--ac)}button.danger{border-color:#d4380d;background:#d4380d;color:#fff}button:disabled{cursor:not-allowed;opacity:.5}.subhead{margin:18px 0 9px;border-top:1px solid var(--b);padding-top:15px;font-size:12px;font-weight:800}.selected{display:flex;align-items:center;justify-content:space-between;border:1px solid var(--b);border-radius:var(--cr);padding:12px}.selected div{display:grid}.results{position:relative;z-index:3;display:grid;margin-top:5px;border:1px solid var(--b);border-radius:var(--cr);background:#fff;box-shadow:0 12px 30px #1f2d3d1f;overflow:hidden}.results button{display:grid;gap:2px;text-align:left;border:0;border-bottom:1px solid var(--b);border-radius:0;padding:12px}.results span{color:var(--m);font-weight:400}.presets{display:flex;gap:8px;margin:-4px 0 12px}.lines{display:grid;gap:10px}.line{display:grid;grid-template-columns:1.2fr 1.2fr 110px 120px 40px;align-items:end;gap:10px;border-radius:var(--cr);background:var(--bg);padding:12px}.line.professional{grid-template-columns:1.1fr 1fr 1fr 1fr 110px 1fr 90px 110px 40px}.allowed{display:grid;gap:7px;text-align:right}.allowed span{color:var(--m);font-size:11px;font-weight:800;text-transform:uppercase}.allowed strong{font-size:17px}.remove{border:0;background:transparent;font-size:20px}.docs{list-style:none;margin:0;padding:0}.docs li{display:flex;align-items:center;gap:8px;border-top:1px solid var(--b);padding:11px 0}.docs li>div{display:grid;flex:1}.upload{display:grid;grid-template-columns:240px 1fr auto;gap:10px;margin-top:12px;border-radius:var(--cr);background:var(--bg);padding:12px}.delivery{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:20px;background:color-mix(in srgb,var(--a) 5%,white)}.actions,.actionbar{display:flex;justify-content:flex-end;gap:8px}.lifecycle{display:grid;gap:16px}.actionbar{flex-wrap:wrap}.eors{display:grid;gap:8px}.eors>button{display:flex;align-items:center;justify-content:space-between;text-align:left}.eors span{display:grid}.panel{display:grid;gap:14px;border-top:1px solid var(--b);padding-top:16px}.notice,.state{padding:13px 15px}.notice{border-radius:var(--cr);background:#edf8f2;color:#23734c}.notice.error,.state.error{background:#fff2f0;color:#b42318}.state{display:grid;gap:10px}.state button{justify-self:start}.powered{text-align:right;color:var(--m);font-size:11px}.modal-backdrop{position:fixed;z-index:1000;inset:0;display:grid;place-items:center;background:#0f20288f;padding:20px}.submit-modal{display:grid;gap:18px;width:min(680px,100%);max-height:calc(100vh - 40px);overflow:auto;border:1px solid var(--b);border-radius:var(--r);background:var(--s);padding:22px;box-shadow:0 24px 80px #0f202847}.submit-modal>header,.submit-modal>footer{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.submit-modal>footer{justify-content:flex-end}.submit-modal .close{border:0;background:transparent;font-size:26px}.route-list{display:grid;gap:8px}.route-option{display:grid;grid-template-columns:20px 1fr auto;align-items:center;gap:12px;text-align:left;padding:13px}.route-option>span:nth-child(2){display:grid}.route-option.selected{border-color:var(--a);box-shadow:0 0 0 2px color-mix(in srgb,var(--a) 18%,transparent)}.route-option .radio{width:16px;height:16px;border:2px solid var(--b);border-radius:50%}.route-option.selected .radio{border:5px solid var(--a)}.route-option b{color:var(--a);font-size:11px;text-transform:uppercase}@media(max-width:1100px){.line.professional{grid-template-columns:repeat(3,minmax(0,1fr))}.line.professional .allowed{grid-column:auto}}@media(max-width:900px){.grid.four,.grid.three{grid-template-columns:repeat(2,minmax(0,1fr))}.line{grid-template-columns:1fr 1fr 90px}.allowed{grid-column:1/-2;text-align:left}.upload{grid-template-columns:1fr}.delivery{align-items:stretch;flex-direction:column}.actions{justify-content:flex-start}}@media(max-width:600px){.summary,.card-title{align-items:stretch;flex-direction:column}.money{text-align:left}.grid.four,.grid.three,.grid.two,.line,.line.professional{grid-template-columns:1fr}.allowed{grid-column:auto}.docs li{align-items:flex-start;flex-wrap:wrap}.modal-backdrop{padding:8px}.submit-modal{max-height:calc(100vh - 16px);padding:16px}.route-option{grid-template-columns:20px 1fr}.route-option b{grid-column:2}}\n"] }]
451
616
  }], ctorParameters: () => [], propDecorators: { billId: [{
452
617
  type: Input,
453
618
  args: [{ required: true }]
@@ -473,5 +638,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
473
638
  * Generated bundle index. Do not edit.
474
639
  */
475
640
 
476
- export { MindBillBillLifecycleComponent, MindBillLifecycleStore };
641
+ export { MindBillBillLifecycleComponent, MindBillLifecycleStore, ensureTrailingProcedureLine, isEmptyProcedureLine };
477
642
  //# sourceMappingURL=mindbill-angular.mjs.map