@mindbill/angular 0.12.0 → 0.14.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.
@@ -1,6 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, computed, Input, Component, EventEmitter, effect, Output, Directive, inject, ChangeDetectorRef, HostListener } from '@angular/core';
3
- import { createBillLifecycleClient, BILL_TASKS_AGING_BUCKETS, createBillReferenceClient, createBillSubmissionClient, createOrganizationClient } from '@mindbill/browser';
2
+ import { signal, computed, Input, Component, EventEmitter, Output, inject, ChangeDetectorRef, HostListener, effect, Directive } from '@angular/core';
3
+ import { createBillLifecycleClient, createBillReferenceClient, createBillSubmissionClient, REPORT_BILL_STATUS_OPTIONS, reportBillStatusContacts, BILL_TASKS_AGING_BUCKETS, createOrganizationClient } from '@mindbill/browser';
4
+ export { REPORT_BILL_STATUS_OPTIONS, reportBillStatusContacts } from '@mindbill/browser';
4
5
  import * as i1 from '@angular/common';
5
6
  import { CommonModule } from '@angular/common';
6
7
  import * as i2 from '@angular/forms';
@@ -54,7 +55,10 @@ class MindBillLifecycleStore {
54
55
  getEor(id) { return this.requireClient().getEor(id); }
55
56
  closeBill(input) { return this.mutate(() => this.requireClient().closeBill(input)); }
56
57
  postPayment(input) { return this.mutate(() => this.requireClient().postPayment(input)); }
58
+ resubmitBill(input) { return this.mutate(() => this.requireClient().resubmitBill(input)); }
57
59
  submitSecondReview(input) { return this.mutate(() => this.requireClient().submitSecondReview(input)); }
60
+ sendDuplicateBill(input) { return this.mutate(() => this.requireClient().sendDuplicateBill(input)); }
61
+ reportBillStatus(input) { return this.mutate(() => this.requireClient().reportBillStatus(input)); }
58
62
  requireClient() {
59
63
  if (!this.client)
60
64
  throw new Error("Connect the lifecycle store before using it.");
@@ -188,363 +192,430 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
188
192
  type: Input
189
193
  }] } });
190
194
 
191
- class MindBillBillLifecycleComponent {
192
- billId = "";
193
- sessionEndpoint = "/api/mindbill/session";
194
- apiBaseUrl = "https://app.mindbill.org";
195
- getSession;
196
- refreshInterval = 60_000;
197
- appearance = { preset: "mindbill" };
198
- billingError = new EventEmitter();
199
- store = new MindBillLifecycleStore();
200
- lifecycleStages = [
201
- { id: "submitted", label: "Submitted" }, { id: "accepted", label: "Accepted" },
202
- { id: "response", label: "Payer response" }, { id: "follow_up", label: "Follow-up" },
203
- { id: "closed", label: "Closed" },
204
- ];
205
- panel = "";
206
- notice = "";
207
- closeReason = "";
208
- payment = { amount: 0, method: "check", depositDate: new Date().toISOString().slice(0, 10) };
209
- review = { reason: "", payerClaimControlNumber: "", disputedAmount: 0 };
210
- constructor() { effect(() => { const error = this.store.error(); if (error)
211
- this.billingError.emit(error); }); }
212
- ngOnChanges(changes) {
213
- if (changes["billId"] || changes["sessionEndpoint"] || changes["apiBaseUrl"] || changes["getSession"]) {
214
- if (!this.billId.trim()) {
215
- this.store.disconnect();
216
- return;
195
+ /** Date helpers shared by the Angular submission surfaces. Mirrors the React
196
+ * package's parseBillSubmissionDate/formatBillSubmissionDate exactly so both
197
+ * frameworks accept and render the same values. */
198
+ function parseMindBillSubmissionDate(value) {
199
+ const trimmed = value.trim();
200
+ const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
201
+ const us = /^(\d{1,2})[/-](\d{1,2})[/-](\d{4})$/.exec(trimmed);
202
+ const digits = /^(\d{2})(\d{2})(\d{4})$/.exec(trimmed.replace(/\D/g, ""));
203
+ const match = iso ? [iso[2], iso[3], iso[1]] : us ? [us[1], us[2], us[3]] : digits ? [digits[1], digits[2], digits[3]] : null;
204
+ if (!match)
205
+ return undefined;
206
+ const month = Number(match[0]);
207
+ const day = Number(match[1]);
208
+ const year = Number(match[2]);
209
+ const date = new Date(Date.UTC(year, month - 1, day));
210
+ if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day)
211
+ return undefined;
212
+ return `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
213
+ }
214
+ function formatMindBillSubmissionDate(value) {
215
+ const parsed = value ? parseMindBillSubmissionDate(value) : undefined;
216
+ if (!parsed)
217
+ return "";
218
+ const [year, month, day] = parsed.split("-");
219
+ return `${month}/${day}/${year}`;
220
+ }
221
+ /** Accepts complete CPT (5 digits), HCPCS (letter + 4 digits), or California
222
+ * medical-legal codes typed directly into the procedure picker. */
223
+ function mindBillCustomProcedureOption(query) {
224
+ const code = query.trim().toUpperCase().replace(/\s+/g, "");
225
+ if (!/^(?:\d{5}|[A-Z]\d{4}|ML(?:20[0-5]|PRR))$/.test(code))
226
+ return null;
227
+ return { id: code, label: code, detail: "Use this CPT, HCPCS, or medical-legal code" };
228
+ }
229
+
230
+ /** Styled searchable dropdown shared by the submission form's procedure,
231
+ * modifier, taxonomy, payer, and diagnosis pickers. Mirrors the React
232
+ * package's ComboBox: query filtering, optional custom-code creation,
233
+ * loading states, and infinite scroll via (endReached). */
234
+ class MindBillComboBoxComponent {
235
+ value = "";
236
+ placeholder = "";
237
+ options = [];
238
+ disabled = false;
239
+ loading = false;
240
+ loadingMore = false;
241
+ invalid = false;
242
+ preserveValueOnOpen = false;
243
+ ariaLabel = "";
244
+ createOption;
245
+ opened = new EventEmitter();
246
+ queryChange = new EventEmitter();
247
+ endReached = new EventEmitter();
248
+ selected = new EventEmitter();
249
+ open = false;
250
+ query = "";
251
+ closeTimer;
252
+ get visible() {
253
+ const q = this.query.trim().toLowerCase();
254
+ const matches = q
255
+ ? this.options.filter((option) => `${option.id} ${option.label} ${option.detail ?? ""}`.toLowerCase().includes(q))
256
+ : this.options;
257
+ const custom = this.createOption?.(this.query) ?? null;
258
+ return custom && !matches.some((option) => option.id.toUpperCase() === custom.id.toUpperCase())
259
+ ? [...matches, custom]
260
+ : matches;
261
+ }
262
+ get showMenu() {
263
+ return this.open && (this.loading || this.visible.length > 0 || this.query.trim().length >= 2);
264
+ }
265
+ onFocus() {
266
+ clearTimeout(this.closeTimer);
267
+ const next = this.preserveValueOnOpen ? this.value : "";
268
+ this.open = true;
269
+ this.query = next;
270
+ this.opened.emit();
271
+ if (next)
272
+ this.queryChange.emit(next);
273
+ }
274
+ onInput(event) {
275
+ this.open = true;
276
+ this.query = event.target.value;
277
+ this.queryChange.emit(this.query);
278
+ }
279
+ onBlur() {
280
+ this.closeTimer = setTimeout(() => { this.open = false; }, 120);
281
+ }
282
+ onScroll(event) {
283
+ const menu = event.currentTarget;
284
+ if (menu.scrollHeight - menu.scrollTop - menu.clientHeight < 120)
285
+ this.endReached.emit();
286
+ }
287
+ choose(option) {
288
+ this.selected.emit(option);
289
+ this.open = false;
290
+ this.query = "";
291
+ }
292
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillComboBoxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
293
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillComboBoxComponent, isStandalone: true, selector: "mindbill-combo-box", inputs: { value: "value", placeholder: "placeholder", options: "options", disabled: "disabled", loading: "loading", loadingMore: "loadingMore", invalid: "invalid", preserveValueOnOpen: "preserveValueOnOpen", ariaLabel: "ariaLabel", createOption: "createOption" }, outputs: { opened: "opened", queryChange: "queryChange", endReached: "endReached", selected: "selected" }, ngImport: i0, template: `
294
+ <div class="combo">
295
+ <input
296
+ type="text"
297
+ role="combobox"
298
+ autocomplete="off"
299
+ [attr.aria-label]="ariaLabel"
300
+ [attr.aria-invalid]="invalid || null"
301
+ [attr.aria-expanded]="showMenu"
302
+ [disabled]="disabled"
303
+ [placeholder]="placeholder"
304
+ [value]="open ? query : value"
305
+ (focus)="onFocus()"
306
+ (click)="onFocus()"
307
+ (input)="onInput($event)"
308
+ (blur)="onBlur()"
309
+ />
310
+ @if (showMenu) {
311
+ <div class="menu" role="listbox" (scroll)="onScroll($event)">
312
+ @if (loading && !visible.length) { <div class="status">Loading…</div> }
313
+ @else if (visible.length) {
314
+ @for (option of visible; track option.id) {
315
+ <button type="button" role="option" (mousedown)="$event.preventDefault()" (click)="choose(option)">
316
+ <strong>{{ option.label }}</strong>
317
+ @if (option.detail) { <small>{{ option.detail }}</small> }
318
+ </button>
217
319
  }
218
- this.store.connect({ billId: this.billId, sessionEndpoint: this.sessionEndpoint, apiBaseUrl: this.apiBaseUrl, ...(this.getSession ? { getSession: this.getSession } : {}) }, this.refreshInterval);
320
+ } @else { <div class="status">No matches</div> }
321
+ @if (loadingMore) { <div class="status">Loading more…</div> }
322
+ </div>
323
+ }
324
+ </div>
325
+ `, isInline: true, styles: [":host{display:block}.combo{position:relative}input{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}input[aria-invalid=true]{border-color:#c83c3c}.menu{position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:30;max-height:320px;overflow:auto;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);box-shadow:0 14px 35px #172b3730}.menu button{display:grid;width:100%;gap:3px;border:0;border-bottom:1px solid color-mix(in srgb,var(--b) 60%,transparent);background:var(--s);padding:11px 14px;text-align:left;color:var(--t);font:inherit;cursor:pointer}.menu button:last-child{border-bottom:0}.menu button:hover{background:color-mix(in srgb,var(--a) 7%,var(--s))}.menu strong{font-weight:720}.menu small{color:var(--m);font-size:12px}.status{padding:11px 14px;color:var(--m);font-size:13px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
326
+ }
327
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillComboBoxComponent, decorators: [{
328
+ type: Component,
329
+ args: [{ selector: "mindbill-combo-box", standalone: true, imports: [CommonModule], template: `
330
+ <div class="combo">
331
+ <input
332
+ type="text"
333
+ role="combobox"
334
+ autocomplete="off"
335
+ [attr.aria-label]="ariaLabel"
336
+ [attr.aria-invalid]="invalid || null"
337
+ [attr.aria-expanded]="showMenu"
338
+ [disabled]="disabled"
339
+ [placeholder]="placeholder"
340
+ [value]="open ? query : value"
341
+ (focus)="onFocus()"
342
+ (click)="onFocus()"
343
+ (input)="onInput($event)"
344
+ (blur)="onBlur()"
345
+ />
346
+ @if (showMenu) {
347
+ <div class="menu" role="listbox" (scroll)="onScroll($event)">
348
+ @if (loading && !visible.length) { <div class="status">Loading…</div> }
349
+ @else if (visible.length) {
350
+ @for (option of visible; track option.id) {
351
+ <button type="button" role="option" (mousedown)="$event.preventDefault()" (click)="choose(option)">
352
+ <strong>{{ option.label }}</strong>
353
+ @if (option.detail) { <small>{{ option.detail }}</small> }
354
+ </button>
355
+ }
356
+ } @else { <div class="status">No matches</div> }
357
+ @if (loadingMore) { <div class="status">Loading more…</div> }
358
+ </div>
359
+ }
360
+ </div>
361
+ `, styles: [":host{display:block}.combo{position:relative}input{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}input[aria-invalid=true]{border-color:#c83c3c}.menu{position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:30;max-height:320px;overflow:auto;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);box-shadow:0 14px 35px #172b3730}.menu button{display:grid;width:100%;gap:3px;border:0;border-bottom:1px solid color-mix(in srgb,var(--b) 60%,transparent);background:var(--s);padding:11px 14px;text-align:left;color:var(--t);font:inherit;cursor:pointer}.menu button:last-child{border-bottom:0}.menu button:hover{background:color-mix(in srgb,var(--a) 7%,var(--s))}.menu strong{font-weight:720}.menu small{color:var(--m);font-size:12px}.status{padding:11px 14px;color:var(--m);font-size:13px}\n"] }]
362
+ }], propDecorators: { value: [{
363
+ type: Input
364
+ }], placeholder: [{
365
+ type: Input
366
+ }], options: [{
367
+ type: Input
368
+ }], disabled: [{
369
+ type: Input
370
+ }], loading: [{
371
+ type: Input
372
+ }], loadingMore: [{
373
+ type: Input
374
+ }], invalid: [{
375
+ type: Input
376
+ }], preserveValueOnOpen: [{
377
+ type: Input
378
+ }], ariaLabel: [{
379
+ type: Input
380
+ }], createOption: [{
381
+ type: Input
382
+ }], opened: [{
383
+ type: Output
384
+ }], queryChange: [{
385
+ type: Output
386
+ }], endReached: [{
387
+ type: Output
388
+ }], selected: [{
389
+ type: Output
390
+ }] } });
391
+ /** MM/DD/YYYY masked date input. Emits only parseable ISO dates (or "" when
392
+ * cleared) and reformats the display once a complete date round-trips, exactly
393
+ * like the React package's TextDateInput. */
394
+ class MindBillDateInputComponent {
395
+ value;
396
+ disabled = false;
397
+ ariaLabel = "";
398
+ valueChange = new EventEmitter();
399
+ display = "";
400
+ invalid = false;
401
+ ngOnChanges(changes) {
402
+ if (changes["value"]) {
403
+ this.display = formatMindBillSubmissionDate(this.value);
404
+ this.invalid = false;
219
405
  }
220
406
  }
221
- ngOnDestroy() { this.store.disconnect(); }
222
- get themeStyle() {
223
- return mindBillAngularAppearanceStyle(this.appearance);
407
+ onInput(event) {
408
+ const next = event.target.value;
409
+ const parsed = parseMindBillSubmissionDate(next);
410
+ this.display = next;
411
+ this.invalid = Boolean(next && !parsed);
412
+ if (!next.trim())
413
+ this.valueChange.emit("");
414
+ else if (parsed)
415
+ this.valueChange.emit(parsed);
224
416
  }
225
- stateLabel(data) { return data.lifecycle.state.replace(/_/g, " ").replace(/\b\w/g, (value) => value.toUpperCase()); }
226
- lifecycleDetail(data) { return data.lifecycle.submittedAt ? `Submitted ${new Date(data.lifecycle.submittedAt).toLocaleDateString()}${data.lifecycle.agingDays != null ? ` · ${data.lifecycle.agingDays} days old` : ""}` : "Submitted bill"; }
227
- currentStageIndex(data) { const value = data.lifecycle.state.toLowerCase(); if (value.includes("closed") || value.includes("written_off"))
228
- return 4; if (value.includes("second_review") || value.includes("ibr") || value.includes("lien") || value.includes("appeal"))
229
- return 3; if (value.includes("processed") || value.includes("paid") || value.includes("denied") || value.includes("rejected") || value.includes("response"))
230
- return 2; if (value.includes("accepted"))
231
- return 1; return 0; }
232
- activityLabel(type) { return type.replace(/[._-]+/g, " ").replace(/\b\w/g, (value) => value.toUpperCase()); }
233
- historyExpanded = new Set();
234
- toggleHistory(id) { if (this.historyExpanded.has(id))
235
- this.historyExpanded.delete(id);
236
- else
237
- this.historyExpanded.add(id); }
238
- isHistoryOpen(id) { return this.historyExpanded.has(id); }
239
- historyDate(iso) { const parsed = new Date(iso); return Number.isNaN(parsed.getTime()) ? iso : parsed.toLocaleDateString("en-US", { month: "2-digit", day: "2-digit", year: "numeric" }); }
240
- payerContact(data) { const contacts = data.delivery.contacts; return contacts.adjusterName || contacts.adjusterPhone || contacts.adjusterEmail || contacts.claimsEmail || contacts.faxNumber || contacts.mailingAddress || "No payer contact details are available."; }
241
- beginAction(action, data) { if (action === "post_payment")
242
- this.panel = "payment";
243
- else if (action === "second_review" || action === "independent_bill_review")
244
- this.panel = "review";
245
- else if (action === "close")
246
- this.panel = "close";
247
- else if (action === "view_eor" && data.eors[0])
248
- void this.openEor(data.eors[0].id, data.eors[0].filename); }
249
- async openAttachment(id, filename) { this.openBlob(await this.store.getAttachment(id), filename); }
250
- async openEor(id, filename) { this.openBlob(await this.store.getEor(id), filename); }
251
- async postPayment() { await this.store.postPayment(this.payment); this.panel = ""; this.notice = "Payment posted."; }
252
- async submitReview() { await this.store.submitSecondReview({ ...this.review, disputedAmount: this.review.disputedAmount > 0 ? this.review.disputedAmount : undefined, attachmentIds: this.store.data()?.bill.attachments.map((document) => document.id) ?? [], route: "ebill" }); this.panel = ""; this.notice = "Second Review submitted."; }
253
- async closeBill() { if (!this.closeReason.trim())
254
- return; await this.store.closeBill({ reason: this.closeReason }); this.panel = ""; this.notice = "Bill closed."; }
255
- 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); }
256
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillLifecycleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
257
- 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: { billingError: "billingError" }, usesOnChanges: true, ngImport: i0, template: `
258
- <section class="mb" [ngStyle]="themeStyle">
259
- @if (!billId) {
260
- <div class="state error"><strong>A submitted bill ID is required.</strong></div>
261
- } @else if (store.loading() && !store.data()) {
262
- <div class="state">Loading billing…</div>
263
- } @else if (store.error() && !store.data()) {
264
- <div class="state error"><strong>Billing is temporarily unavailable</strong><span>{{ store.error()?.message }}</span><button type="button" (click)="store.refresh()">Try again</button></div>
265
- } @else if (store.data(); as data) {
266
- <header class="summary">
267
- <div><span class="eyebrow">Bill {{ data.bill.billNumber }}</span><h2>{{ stateLabel(data) }}</h2><p>{{ lifecycleDetail(data) }}</p></div>
268
- <div class="money"><span>{{ data.bill.balanceDue | currency }}</span><small>balance</small></div>
269
- </header>
270
-
271
- @if (data.lifecycle.state.toLowerCase() === 'rejected' && data.rejection) {
272
- <mindbill-bill-rejection-notice
273
- class="rejection"
274
- [rejection]="data.rejection"
275
- [submittedAt]="data.lifecycle.submittedAt ?? null"
276
- [appearance]="appearance"
277
- />
278
- } @else {
279
- <section class="card progress" aria-label="Bill lifecycle">
280
- <ol>@for (stage of lifecycleStages; track stage.id; let i=$index) { <li [class.complete]="i < currentStageIndex(data)" [class.current]="i === currentStageIndex(data)"><b>{{ i < currentStageIndex(data) ? '✓' : i + 1 }}</b><span>{{ stage.label }}</span></li> }</ol>
281
- </section>
282
- }
283
-
284
- <section class="card snapshot" aria-label="Bill snapshot">
285
- <div class="card-title"><div><h3>Bill snapshot</h3><p>The immutable values submitted to the payer.</p></div></div>
286
- <dl><div><dt>Patient</dt><dd>{{ data.patient.name }}</dd></div><div><dt>Claim</dt><dd>{{ data.injury.claimNumber || '—' }}</dd></div><div><dt>Payer</dt><dd>{{ data.delivery.payerName || data.injury.claimsAdminName || '—' }}</dd></div><div><dt>Date of service</dt><dd>{{ data.bill.dos || '—' }}</dd></div><div><dt>Charged</dt><dd>{{ data.bill.totalCharge | currency }}</dd></div><div><dt>Balance</dt><dd>{{ data.bill.balanceDue | currency }}</dd></div></dl>
287
- </section>
288
-
289
- <div class="columns">
290
- <section class="card"><div class="card-title"><div><h3>Remittance</h3><p>Amounts reported by the payer and posted to this bill.</p></div></div><dl><div><dt>Billed</dt><dd>{{ data.remittance.billedAmount | currency }}</dd></div><div><dt>Payer reported</dt><dd>{{ data.remittance.payerReportedPaid == null ? '—' : (data.remittance.payerReportedPaid | currency) }}</dd></div><div><dt>Principal posted</dt><dd>{{ data.remittance.postedPrincipal | currency }}</dd></div><div><dt>Penalty &amp; interest</dt><dd>{{ data.remittance.postedAdditional | currency }}</dd></div><div><dt>Total received</dt><dd>{{ data.remittance.totalPostedCash | currency }}</dd></div><div><dt>Balance</dt><dd>{{ data.remittance.balanceDue | currency }}</dd></div></dl></section>
291
- <section class="card"><div class="card-title"><div><h3>{{ data.delivery.payerName || 'Payer' }}</h3><p>Billing and follow-up contacts.</p></div></div><p class="muted">{{ payerContact(data) }}</p></section>
292
- </div>
293
-
294
- <section class="card"><div class="card-title"><div><h3>Documents</h3><p>Documents preserved with the submitted bill.</p></div><span>{{ data.bill.attachments.length }}</span></div>@if (data.bill.attachments.length) { <ul class="rows">@for (doc of data.bill.attachments; track doc.id) { <li><div><strong>{{ doc.filename }}</strong><small>{{ doc.description || doc.documentType }}</small></div><button type="button" (click)="openAttachment(doc.id, doc.filename)">View</button></li> }</ul> } @else { <p class="muted">No documents available.</p> }</section>
295
-
296
- <section class="card"><div class="card-title"><div><h3>Payments</h3><p>Payments posted to this bill.</p></div><span>{{ data.payments.length }}</span></div>@if (data.payments.length) { <ul class="rows">@for (payment of data.payments; track payment.id) { <li><div><strong>{{ payment.amount | currency }}</strong><small>{{ payment.method | uppercase }} · {{ payment.depositDate || payment.postedAt }}</small></div></li> }</ul> } @else { <p class="muted">No payments posted.</p> }</section>
297
-
298
- <section class="card"><div class="card-title"><div><h3>Bill history</h3><p>Submissions, payer responses, follow-up, and payments.</p></div><span>{{ data.history?.length ?? data.activity.length }}</span></div>
299
- @if (data.history?.length) {
300
- <!-- daisyBill-style Date/Action/User/Details table: submissions are pale-blue
301
- expandable rows (documents + compliance due dates), notes pale-yellow,
302
- 277 responses expand into decoded status-code sentences. Rows arrive
303
- pre-worded from the MindBill API (BillLifecycleData.history). -->
304
- <div class="history">
305
- <div class="history-head" aria-hidden="true"><span>Date</span><span>Action</span><span>User</span><span>Details</span></div>
306
- <ul class="history-rows">
307
- @for (entry of data.history; track entry.id) {
308
- <li [class.is-submission]="entry.tone === 'submission'" [class.is-note]="entry.tone === 'note'" [class.is-problem]="entry.tone === 'problem'">
309
- <button type="button" class="history-line" [disabled]="!entry.details" [attr.aria-expanded]="entry.details ? isHistoryOpen(entry.id) : null" (click)="toggleHistory(entry.id)">
310
- <span class="h-date">{{ historyDate(entry.date) }}</span>
311
- <span class="h-action">{{ entry.action }}</span>
312
- <span class="h-actor">{{ entry.actor || '—' }}</span>
313
- <span class="h-summary">@if (entry.details) { <i [class.open]="isHistoryOpen(entry.id)" aria-hidden="true"></i> }{{ entry.summary }}</span>
314
- </button>
315
- @if (entry.details; as details) {
316
- @if (isHistoryOpen(entry.id)) {
317
- <div class="history-details">
318
- @if (details.rows?.length) { <dl class="h-kv">@for (row of details.rows ?? []; track row.label) { <div><dt>{{ row.label }}</dt><dd>{{ row.value }}</dd></div> }</dl> }
319
- @if (details.documents?.length) { <dl class="h-kv"><div><dt>Documents</dt><dd>@for (doc of details.documents ?? []; track doc.id) { <span class="h-doc">{{ doc.filename }}</span> }</dd></div></dl> }
320
- @if (details.codes?.length) { <dl class="h-codes">@for (code of details.codes ?? []; track code.code) { <div><dt>{{ code.code }}</dt><dd>{{ code.text }}</dd></div> }</dl> }
321
- @if (details.complianceDueDates?.length) { <div class="h-due"><em>Compliance Due Dates</em><dl>@for (due of details.complianceDueDates ?? []; track due.text) { <div><dt>{{ historyDate(due.date) }}</dt><dd>{{ due.text }}</dd></div> }</dl></div> }
322
- @if (details.text) { <p class="h-text">{{ details.text }}</p> }
323
- </div>
324
- }
325
- }
326
- </li>
327
- }
328
- </ul>
329
- </div>
330
- } @else {
331
- <ul class="timeline">@for (event of data.activity; track event.id) { <li><i></i><div><strong>{{ activityLabel(event.type) }}</strong><p>{{ event.description }}</p><small>{{ event.createdAt | date:'medium' }}</small></div></li> }</ul>
332
- }
333
- </section>
334
-
335
- @if (notice) { <p class="notice">{{ notice }}</p> }
336
- <div class="actions">@for (action of data.lifecycle.actions; track action.id) { @if (action.id !== 'view_eor' || data.eors.length) { <button type="button" [class.primary]="action.primary" [disabled]="!action.enabled || store.mutating()" (click)="beginAction(action.id, data)">{{ action.label }}</button> } }</div>
337
-
338
- @if (panel === 'payment') { <form class="card action-form" (submit)="$event.preventDefault(); postPayment()"><h3>Post payment</h3><label>Amount<input required type="number" min="0.01" step="0.01" [(ngModel)]="payment.amount" name="amount"></label><label>Method<select [(ngModel)]="payment.method" name="method"><option value="check">Check</option><option value="eft">EFT</option></select></label><label>Deposit date<input required type="date" [(ngModel)]="payment.depositDate" name="depositDate"></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Post payment</button></div></form> }
339
- @if (panel === 'review') { <form class="card action-form" (submit)="$event.preventDefault(); submitReview()"><h3>Submit Second Bill Review</h3><label>Reason<textarea required [(ngModel)]="review.reason" name="reason"></textarea></label><label>Payer claim control number<input required [(ngModel)]="review.payerClaimControlNumber" name="control"></label><label>Disputed amount<input type="number" min="0" step="0.01" [(ngModel)]="review.disputedAmount" name="disputed"></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Submit review</button></div></form> }
340
- @if (panel === 'close') { <form class="card action-form" (submit)="$event.preventDefault(); closeBill()"><h3>Close bill</h3><label>Reason<textarea required [(ngModel)]="closeReason" name="closeReason"></textarea></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Close bill</button></div></form> }
341
- }
342
- </section>
343
- `, isInline: true, styles: [":host{display:block}.mb{font-family:var(--font);color:var(--t);background:var(--bg);padding:20px}.summary,.card,.columns,.rejection{display:block;max-width:1120px;margin:0 auto 16px}.summary{display:flex;justify-content:space-between;align-items:end}.summary h2{margin:4px 0}.summary p,.muted{color:var(--m)}.eyebrow,dt{font-size:12px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--m)}.money{text-align:right}.money span{display:block;font-size:28px;font-weight:800}.money small{color:var(--m)}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:20px}.card-title{display:flex;justify-content:space-between;gap:20px}.card-title h3{margin:0}.card-title p{margin:5px 0;color:var(--m)}.progress ol{display:flex;list-style:none;padding:0;margin:0}.progress li{flex:1;text-align:center;position:relative;color:var(--m)}.progress li:before{content:\"\";position:absolute;top:16px;left:0;right:0;border-top:2px solid var(--b)}.progress li:first-child:before{left:50%}.progress li:last-child:before{right:50%}.progress b{position:relative;z-index:1;display:grid;place-items:center;width:32px;height:32px;margin:auto;border:2px solid var(--b);border-radius:50%;background:var(--s)}.progress .complete b,.progress .current b{border-color:var(--a);background:var(--a);color:var(--ac)}.progress span{display:block;margin-top:8px;font-size:12px;font-weight:700}.snapshot dl,.columns dl{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}.snapshot dd,.columns dd{font-weight:700;margin:6px 0}.columns{display:grid;grid-template-columns:1fr 1fr;gap:16px}.columns .card{margin:0}.rows,.timeline{list-style:none;margin:12px 0 0;padding:0}.rows li{display:flex;justify-content:space-between;align-items:center;padding:12px 0;border-top:1px solid var(--b)}.rows small{display:block;color:var(--m);margin-top:3px}.timeline li{display:flex;gap:14px;padding:10px 0}.timeline i{width:10px;height:10px;margin-top:5px;border:3px solid var(--a);border-radius:50%}.timeline p{margin:4px 0}.timeline small{color:var(--m)}button,input,select,textarea{font:inherit;border:1px solid var(--b);border-radius:var(--cr);padding:9px 12px;background:var(--s);color:var(--t)}button{cursor:pointer;font-weight:700}.primary{background:var(--a);border-color:var(--a);color:var(--ac)}.actions{max-width:1120px;margin:0 auto;display:flex;justify-content:flex-end;gap:10px}.action-form{display:grid;gap:12px}.action-form label{display:grid;gap:5px;font-weight:700}.action-form div{display:flex;justify-content:flex-end;gap:10px}.state{max-width:720px;margin:auto;padding:32px;text-align:center}.state span{display:block;margin:8px}.error{color:#9b1c1c}.notice{max-width:1120px;margin:0 auto 12px;color:var(--m)}@media(max-width:760px){.mb{padding:12px}.summary,.columns{display:block}.money{text-align:left;margin-top:12px}.columns .card{margin-bottom:12px}.snapshot dl,.columns dl{grid-template-columns:1fr 1fr}.progress span{font-size:10px}.card{padding:15px}}.history{border:1px solid var(--b);border-radius:10px;overflow:hidden;margin-top:12px}.history-head{display:grid;grid-template-columns:96px 150px 130px minmax(0,1fr);gap:12px;padding:8px 14px;background:color-mix(in srgb,var(--b) 26%,var(--s));color:var(--m);font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}.history-rows{list-style:none;margin:0;padding:0}.history-rows>li{border-top:1px solid var(--b)}.history-rows>li.is-submission{background:color-mix(in srgb,var(--a) 9%,var(--s))}.history-rows>li.is-note{background:color-mix(in srgb,#f2c344 13%,var(--s))}.history-line{display:grid;grid-template-columns:96px 150px 130px minmax(0,1fr);gap:12px;width:100%;padding:10px 14px;border:0;border-radius:0;background:transparent;text-align:left;font-weight:400;cursor:default}.history-line:not(:disabled){cursor:pointer}.history-line:not(:disabled):hover .h-summary{text-decoration:underline}.h-date,.h-actor{color:var(--m);font-size:13px;overflow-wrap:anywhere}.h-action{font-weight:700;font-size:13.5px}.is-problem .h-action{color:#9b1c1c}.h-summary{display:flex;gap:7px;min-width:0;font-size:13.5px;line-height:1.45;overflow-wrap:anywhere}.h-summary i{flex:0 0 auto;margin-top:5px;width:0;height:0;border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:6px solid var(--m);border-radius:0;transition:transform .12s ease}.h-summary i.open{transform:rotate(90deg)}.history-details{padding:4px 14px 14px 122px;font-size:13.5px;line-height:1.5}.h-kv,.h-codes,.h-due dl{display:grid;gap:4px;margin:0}.h-kv>div{display:grid;grid-template-columns:180px minmax(0,1fr);gap:12px;border-top:1px solid color-mix(in srgb,var(--b) 60%,transparent);padding-top:4px}.h-kv dt,.h-codes dt,.h-due dt{font-size:13px;font-weight:700;letter-spacing:0;text-transform:none;color:var(--t)}.h-kv dd,.h-codes dd,.h-due dd{margin:0;overflow-wrap:anywhere}.h-doc{display:block}.h-codes>div{display:grid;grid-template-columns:56px minmax(0,1fr);gap:12px}.h-codes dt{font-weight:800}.h-due{margin-top:8px}.h-due em{display:block;color:var(--m);font-size:12.5px;margin-bottom:4px}.h-due dl>div{display:grid;grid-template-columns:96px minmax(0,1fr);gap:12px;border-top:1px solid color-mix(in srgb,var(--b) 60%,transparent);padding-top:4px}.h-text{margin:8px 0 0;white-space:pre-wrap}@media(max-width:760px){.history-head{display:none}.history-line{grid-template-columns:1fr;gap:2px;padding:10px 12px}.history-details{padding:4px 12px 12px}.h-kv>div,.h-due dl>div{grid-template-columns:1fr}}\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.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: "component", type: MindBillBillRejectionNoticeComponent, selector: "mindbill-bill-rejection-notice", inputs: ["rejection", "submittedAt", "title", "appearance"] }, { kind: "pipe", type: i1.UpperCasePipe, name: "uppercase" }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }, { kind: "pipe", type: i1.DatePipe, name: "date" }] });
417
+ onBlur() {
418
+ this.invalid = Boolean(this.display && !parseMindBillSubmissionDate(this.display));
419
+ }
420
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillDateInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
421
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillDateInputComponent, isStandalone: true, selector: "mindbill-date-input", inputs: { value: "value", disabled: "disabled", ariaLabel: "ariaLabel" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
422
+ <input
423
+ type="text"
424
+ inputmode="numeric"
425
+ autocomplete="off"
426
+ placeholder="MM/DD/YYYY"
427
+ [attr.aria-label]="ariaLabel"
428
+ [attr.aria-invalid]="invalid || null"
429
+ [disabled]="disabled"
430
+ [value]="display"
431
+ (input)="onInput($event)"
432
+ (blur)="onBlur()"
433
+ />
434
+ @if (invalid) { <small>Use MM/DD/YYYY</small> }
435
+ `, isInline: true, styles: [":host{display:block}input{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}input[aria-invalid=true]{border-color:#c83c3c}small{display:block;margin-top:6px;color:#c83c3c;font-weight:450}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
344
436
  }
345
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillLifecycleComponent, decorators: [{
437
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillDateInputComponent, decorators: [{
346
438
  type: Component,
347
- args: [{ selector: "mindbill-bill-lifecycle", standalone: true, imports: [CommonModule, FormsModule, MindBillBillRejectionNoticeComponent], template: `
348
- <section class="mb" [ngStyle]="themeStyle">
349
- @if (!billId) {
350
- <div class="state error"><strong>A submitted bill ID is required.</strong></div>
351
- } @else if (store.loading() && !store.data()) {
352
- <div class="state">Loading billing…</div>
353
- } @else if (store.error() && !store.data()) {
354
- <div class="state error"><strong>Billing is temporarily unavailable</strong><span>{{ store.error()?.message }}</span><button type="button" (click)="store.refresh()">Try again</button></div>
355
- } @else if (store.data(); as data) {
356
- <header class="summary">
357
- <div><span class="eyebrow">Bill {{ data.bill.billNumber }}</span><h2>{{ stateLabel(data) }}</h2><p>{{ lifecycleDetail(data) }}</p></div>
358
- <div class="money"><span>{{ data.bill.balanceDue | currency }}</span><small>balance</small></div>
359
- </header>
360
-
361
- @if (data.lifecycle.state.toLowerCase() === 'rejected' && data.rejection) {
362
- <mindbill-bill-rejection-notice
363
- class="rejection"
364
- [rejection]="data.rejection"
365
- [submittedAt]="data.lifecycle.submittedAt ?? null"
366
- [appearance]="appearance"
367
- />
368
- } @else {
369
- <section class="card progress" aria-label="Bill lifecycle">
370
- <ol>@for (stage of lifecycleStages; track stage.id; let i=$index) { <li [class.complete]="i < currentStageIndex(data)" [class.current]="i === currentStageIndex(data)"><b>{{ i < currentStageIndex(data) ? '✓' : i + 1 }}</b><span>{{ stage.label }}</span></li> }</ol>
371
- </section>
372
- }
373
-
374
- <section class="card snapshot" aria-label="Bill snapshot">
375
- <div class="card-title"><div><h3>Bill snapshot</h3><p>The immutable values submitted to the payer.</p></div></div>
376
- <dl><div><dt>Patient</dt><dd>{{ data.patient.name }}</dd></div><div><dt>Claim</dt><dd>{{ data.injury.claimNumber || '—' }}</dd></div><div><dt>Payer</dt><dd>{{ data.delivery.payerName || data.injury.claimsAdminName || '—' }}</dd></div><div><dt>Date of service</dt><dd>{{ data.bill.dos || '—' }}</dd></div><div><dt>Charged</dt><dd>{{ data.bill.totalCharge | currency }}</dd></div><div><dt>Balance</dt><dd>{{ data.bill.balanceDue | currency }}</dd></div></dl>
377
- </section>
378
-
379
- <div class="columns">
380
- <section class="card"><div class="card-title"><div><h3>Remittance</h3><p>Amounts reported by the payer and posted to this bill.</p></div></div><dl><div><dt>Billed</dt><dd>{{ data.remittance.billedAmount | currency }}</dd></div><div><dt>Payer reported</dt><dd>{{ data.remittance.payerReportedPaid == null ? '—' : (data.remittance.payerReportedPaid | currency) }}</dd></div><div><dt>Principal posted</dt><dd>{{ data.remittance.postedPrincipal | currency }}</dd></div><div><dt>Penalty &amp; interest</dt><dd>{{ data.remittance.postedAdditional | currency }}</dd></div><div><dt>Total received</dt><dd>{{ data.remittance.totalPostedCash | currency }}</dd></div><div><dt>Balance</dt><dd>{{ data.remittance.balanceDue | currency }}</dd></div></dl></section>
381
- <section class="card"><div class="card-title"><div><h3>{{ data.delivery.payerName || 'Payer' }}</h3><p>Billing and follow-up contacts.</p></div></div><p class="muted">{{ payerContact(data) }}</p></section>
382
- </div>
383
-
384
- <section class="card"><div class="card-title"><div><h3>Documents</h3><p>Documents preserved with the submitted bill.</p></div><span>{{ data.bill.attachments.length }}</span></div>@if (data.bill.attachments.length) { <ul class="rows">@for (doc of data.bill.attachments; track doc.id) { <li><div><strong>{{ doc.filename }}</strong><small>{{ doc.description || doc.documentType }}</small></div><button type="button" (click)="openAttachment(doc.id, doc.filename)">View</button></li> }</ul> } @else { <p class="muted">No documents available.</p> }</section>
385
-
386
- <section class="card"><div class="card-title"><div><h3>Payments</h3><p>Payments posted to this bill.</p></div><span>{{ data.payments.length }}</span></div>@if (data.payments.length) { <ul class="rows">@for (payment of data.payments; track payment.id) { <li><div><strong>{{ payment.amount | currency }}</strong><small>{{ payment.method | uppercase }} · {{ payment.depositDate || payment.postedAt }}</small></div></li> }</ul> } @else { <p class="muted">No payments posted.</p> }</section>
387
-
388
- <section class="card"><div class="card-title"><div><h3>Bill history</h3><p>Submissions, payer responses, follow-up, and payments.</p></div><span>{{ data.history?.length ?? data.activity.length }}</span></div>
389
- @if (data.history?.length) {
390
- <!-- daisyBill-style Date/Action/User/Details table: submissions are pale-blue
391
- expandable rows (documents + compliance due dates), notes pale-yellow,
392
- 277 responses expand into decoded status-code sentences. Rows arrive
393
- pre-worded from the MindBill API (BillLifecycleData.history). -->
394
- <div class="history">
395
- <div class="history-head" aria-hidden="true"><span>Date</span><span>Action</span><span>User</span><span>Details</span></div>
396
- <ul class="history-rows">
397
- @for (entry of data.history; track entry.id) {
398
- <li [class.is-submission]="entry.tone === 'submission'" [class.is-note]="entry.tone === 'note'" [class.is-problem]="entry.tone === 'problem'">
399
- <button type="button" class="history-line" [disabled]="!entry.details" [attr.aria-expanded]="entry.details ? isHistoryOpen(entry.id) : null" (click)="toggleHistory(entry.id)">
400
- <span class="h-date">{{ historyDate(entry.date) }}</span>
401
- <span class="h-action">{{ entry.action }}</span>
402
- <span class="h-actor">{{ entry.actor || '—' }}</span>
403
- <span class="h-summary">@if (entry.details) { <i [class.open]="isHistoryOpen(entry.id)" aria-hidden="true"></i> }{{ entry.summary }}</span>
404
- </button>
405
- @if (entry.details; as details) {
406
- @if (isHistoryOpen(entry.id)) {
407
- <div class="history-details">
408
- @if (details.rows?.length) { <dl class="h-kv">@for (row of details.rows ?? []; track row.label) { <div><dt>{{ row.label }}</dt><dd>{{ row.value }}</dd></div> }</dl> }
409
- @if (details.documents?.length) { <dl class="h-kv"><div><dt>Documents</dt><dd>@for (doc of details.documents ?? []; track doc.id) { <span class="h-doc">{{ doc.filename }}</span> }</dd></div></dl> }
410
- @if (details.codes?.length) { <dl class="h-codes">@for (code of details.codes ?? []; track code.code) { <div><dt>{{ code.code }}</dt><dd>{{ code.text }}</dd></div> }</dl> }
411
- @if (details.complianceDueDates?.length) { <div class="h-due"><em>Compliance Due Dates</em><dl>@for (due of details.complianceDueDates ?? []; track due.text) { <div><dt>{{ historyDate(due.date) }}</dt><dd>{{ due.text }}</dd></div> }</dl></div> }
412
- @if (details.text) { <p class="h-text">{{ details.text }}</p> }
413
- </div>
414
- }
415
- }
416
- </li>
417
- }
418
- </ul>
419
- </div>
420
- } @else {
421
- <ul class="timeline">@for (event of data.activity; track event.id) { <li><i></i><div><strong>{{ activityLabel(event.type) }}</strong><p>{{ event.description }}</p><small>{{ event.createdAt | date:'medium' }}</small></div></li> }</ul>
422
- }
423
- </section>
424
-
425
- @if (notice) { <p class="notice">{{ notice }}</p> }
426
- <div class="actions">@for (action of data.lifecycle.actions; track action.id) { @if (action.id !== 'view_eor' || data.eors.length) { <button type="button" [class.primary]="action.primary" [disabled]="!action.enabled || store.mutating()" (click)="beginAction(action.id, data)">{{ action.label }}</button> } }</div>
427
-
428
- @if (panel === 'payment') { <form class="card action-form" (submit)="$event.preventDefault(); postPayment()"><h3>Post payment</h3><label>Amount<input required type="number" min="0.01" step="0.01" [(ngModel)]="payment.amount" name="amount"></label><label>Method<select [(ngModel)]="payment.method" name="method"><option value="check">Check</option><option value="eft">EFT</option></select></label><label>Deposit date<input required type="date" [(ngModel)]="payment.depositDate" name="depositDate"></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Post payment</button></div></form> }
429
- @if (panel === 'review') { <form class="card action-form" (submit)="$event.preventDefault(); submitReview()"><h3>Submit Second Bill Review</h3><label>Reason<textarea required [(ngModel)]="review.reason" name="reason"></textarea></label><label>Payer claim control number<input required [(ngModel)]="review.payerClaimControlNumber" name="control"></label><label>Disputed amount<input type="number" min="0" step="0.01" [(ngModel)]="review.disputedAmount" name="disputed"></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Submit review</button></div></form> }
430
- @if (panel === 'close') { <form class="card action-form" (submit)="$event.preventDefault(); closeBill()"><h3>Close bill</h3><label>Reason<textarea required [(ngModel)]="closeReason" name="closeReason"></textarea></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Close bill</button></div></form> }
431
- }
432
- </section>
433
- `, styles: [":host{display:block}.mb{font-family:var(--font);color:var(--t);background:var(--bg);padding:20px}.summary,.card,.columns,.rejection{display:block;max-width:1120px;margin:0 auto 16px}.summary{display:flex;justify-content:space-between;align-items:end}.summary h2{margin:4px 0}.summary p,.muted{color:var(--m)}.eyebrow,dt{font-size:12px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--m)}.money{text-align:right}.money span{display:block;font-size:28px;font-weight:800}.money small{color:var(--m)}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:20px}.card-title{display:flex;justify-content:space-between;gap:20px}.card-title h3{margin:0}.card-title p{margin:5px 0;color:var(--m)}.progress ol{display:flex;list-style:none;padding:0;margin:0}.progress li{flex:1;text-align:center;position:relative;color:var(--m)}.progress li:before{content:\"\";position:absolute;top:16px;left:0;right:0;border-top:2px solid var(--b)}.progress li:first-child:before{left:50%}.progress li:last-child:before{right:50%}.progress b{position:relative;z-index:1;display:grid;place-items:center;width:32px;height:32px;margin:auto;border:2px solid var(--b);border-radius:50%;background:var(--s)}.progress .complete b,.progress .current b{border-color:var(--a);background:var(--a);color:var(--ac)}.progress span{display:block;margin-top:8px;font-size:12px;font-weight:700}.snapshot dl,.columns dl{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}.snapshot dd,.columns dd{font-weight:700;margin:6px 0}.columns{display:grid;grid-template-columns:1fr 1fr;gap:16px}.columns .card{margin:0}.rows,.timeline{list-style:none;margin:12px 0 0;padding:0}.rows li{display:flex;justify-content:space-between;align-items:center;padding:12px 0;border-top:1px solid var(--b)}.rows small{display:block;color:var(--m);margin-top:3px}.timeline li{display:flex;gap:14px;padding:10px 0}.timeline i{width:10px;height:10px;margin-top:5px;border:3px solid var(--a);border-radius:50%}.timeline p{margin:4px 0}.timeline small{color:var(--m)}button,input,select,textarea{font:inherit;border:1px solid var(--b);border-radius:var(--cr);padding:9px 12px;background:var(--s);color:var(--t)}button{cursor:pointer;font-weight:700}.primary{background:var(--a);border-color:var(--a);color:var(--ac)}.actions{max-width:1120px;margin:0 auto;display:flex;justify-content:flex-end;gap:10px}.action-form{display:grid;gap:12px}.action-form label{display:grid;gap:5px;font-weight:700}.action-form div{display:flex;justify-content:flex-end;gap:10px}.state{max-width:720px;margin:auto;padding:32px;text-align:center}.state span{display:block;margin:8px}.error{color:#9b1c1c}.notice{max-width:1120px;margin:0 auto 12px;color:var(--m)}@media(max-width:760px){.mb{padding:12px}.summary,.columns{display:block}.money{text-align:left;margin-top:12px}.columns .card{margin-bottom:12px}.snapshot dl,.columns dl{grid-template-columns:1fr 1fr}.progress span{font-size:10px}.card{padding:15px}}.history{border:1px solid var(--b);border-radius:10px;overflow:hidden;margin-top:12px}.history-head{display:grid;grid-template-columns:96px 150px 130px minmax(0,1fr);gap:12px;padding:8px 14px;background:color-mix(in srgb,var(--b) 26%,var(--s));color:var(--m);font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}.history-rows{list-style:none;margin:0;padding:0}.history-rows>li{border-top:1px solid var(--b)}.history-rows>li.is-submission{background:color-mix(in srgb,var(--a) 9%,var(--s))}.history-rows>li.is-note{background:color-mix(in srgb,#f2c344 13%,var(--s))}.history-line{display:grid;grid-template-columns:96px 150px 130px minmax(0,1fr);gap:12px;width:100%;padding:10px 14px;border:0;border-radius:0;background:transparent;text-align:left;font-weight:400;cursor:default}.history-line:not(:disabled){cursor:pointer}.history-line:not(:disabled):hover .h-summary{text-decoration:underline}.h-date,.h-actor{color:var(--m);font-size:13px;overflow-wrap:anywhere}.h-action{font-weight:700;font-size:13.5px}.is-problem .h-action{color:#9b1c1c}.h-summary{display:flex;gap:7px;min-width:0;font-size:13.5px;line-height:1.45;overflow-wrap:anywhere}.h-summary i{flex:0 0 auto;margin-top:5px;width:0;height:0;border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:6px solid var(--m);border-radius:0;transition:transform .12s ease}.h-summary i.open{transform:rotate(90deg)}.history-details{padding:4px 14px 14px 122px;font-size:13.5px;line-height:1.5}.h-kv,.h-codes,.h-due dl{display:grid;gap:4px;margin:0}.h-kv>div{display:grid;grid-template-columns:180px minmax(0,1fr);gap:12px;border-top:1px solid color-mix(in srgb,var(--b) 60%,transparent);padding-top:4px}.h-kv dt,.h-codes dt,.h-due dt{font-size:13px;font-weight:700;letter-spacing:0;text-transform:none;color:var(--t)}.h-kv dd,.h-codes dd,.h-due dd{margin:0;overflow-wrap:anywhere}.h-doc{display:block}.h-codes>div{display:grid;grid-template-columns:56px minmax(0,1fr);gap:12px}.h-codes dt{font-weight:800}.h-due{margin-top:8px}.h-due em{display:block;color:var(--m);font-size:12.5px;margin-bottom:4px}.h-due dl>div{display:grid;grid-template-columns:96px minmax(0,1fr);gap:12px;border-top:1px solid color-mix(in srgb,var(--b) 60%,transparent);padding-top:4px}.h-text{margin:8px 0 0;white-space:pre-wrap}@media(max-width:760px){.history-head{display:none}.history-line{grid-template-columns:1fr;gap:2px;padding:10px 12px}.history-details{padding:4px 12px 12px}.h-kv>div,.h-due dl>div{grid-template-columns:1fr}}\n"] }]
434
- }], ctorParameters: () => [], propDecorators: { billId: [{
435
- type: Input
436
- }], sessionEndpoint: [{
437
- type: Input
438
- }], apiBaseUrl: [{
439
- type: Input
440
- }], getSession: [{
439
+ args: [{ selector: "mindbill-date-input", standalone: true, imports: [CommonModule], template: `
440
+ <input
441
+ type="text"
442
+ inputmode="numeric"
443
+ autocomplete="off"
444
+ placeholder="MM/DD/YYYY"
445
+ [attr.aria-label]="ariaLabel"
446
+ [attr.aria-invalid]="invalid || null"
447
+ [disabled]="disabled"
448
+ [value]="display"
449
+ (input)="onInput($event)"
450
+ (blur)="onBlur()"
451
+ />
452
+ @if (invalid) { <small>Use MM/DD/YYYY</small> }
453
+ `, styles: [":host{display:block}input{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}input[aria-invalid=true]{border-color:#c83c3c}small{display:block;margin-top:6px;color:#c83c3c;font-weight:450}\n"] }]
454
+ }], propDecorators: { value: [{
441
455
  type: Input
442
- }], refreshInterval: [{
456
+ }], disabled: [{
443
457
  type: Input
444
- }], appearance: [{
458
+ }], ariaLabel: [{
445
459
  type: Input
446
- }], billingError: [{
460
+ }], valueChange: [{
447
461
  type: Output
448
462
  }] } });
449
463
 
450
- const EMPTY_PROCEDURE_LINE = {
451
- code: "",
452
- modifiers: [],
453
- units: 1,
454
- charge: 0,
455
- };
456
- function isEmptyProcedureLine(line) {
457
- return !line.code.trim()
458
- && line.modifiers.length === 0
459
- && line.units === 1
460
- && !line.serviceDate
461
- && !line.serviceDateEnd
462
- && !(line.diagnosisPointers?.length)
463
- && !line.charge;
464
- }
465
- /** Keeps completed/partial lines plus one keyboard-ready empty row. */
466
- function ensureTrailingProcedureLine(lines) {
467
- const entered = lines
468
- .filter((line) => !isEmptyProcedureLine(line))
469
- .map((line) => ({ ...line, modifiers: [...line.modifiers] }));
470
- return entered.length < 50
471
- ? [...entered, { ...EMPTY_PROCEDURE_LINE, modifiers: [] }]
472
- : entered;
473
- }
474
-
464
+ const MAX_PDF_BYTES = 25 * 1024 * 1024;
465
+ const MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
466
+ const MAX_DOCUMENTS = 20;
467
+ const PROCEDURES = [
468
+ { code: "ML200", description: "Missed appointment for a medical-legal evaluation", allowedAmount: 503.75 },
469
+ { code: "ML201", description: "Comprehensive medical-legal evaluation", allowedAmount: 2015 },
470
+ { code: "ML202", description: "Follow-up medical-legal evaluation", allowedAmount: 1316.25 },
471
+ { code: "ML203", description: "Supplemental medical-legal evaluation", allowedAmount: 650 },
472
+ { code: "ML204", description: "Medical-legal testimony", allowedAmount: 113.75 },
473
+ { code: "ML205", description: "Review of sub rosa recordings", allowedAmount: 81.25 },
474
+ { code: "MLPRR", description: "Medical-legal record review", allowedAmount: 3 },
475
+ { code: "90791", description: "Psychiatric diagnostic evaluation" },
476
+ { code: "90792", description: "Psychiatric diagnostic evaluation with medical services" },
477
+ { code: "90832", description: "Psychotherapy, 30 minutes" },
478
+ { code: "90834", description: "Psychotherapy, 45 minutes" },
479
+ { code: "90837", description: "Psychotherapy, 60 minutes" },
480
+ { code: "99203", description: "New patient office visit, level 3" },
481
+ { code: "99204", description: "New patient office visit, level 4" },
482
+ { code: "99205", description: "New patient office visit, level 5", allowedAmount: 349.48 },
483
+ { code: "99213", description: "Established patient office visit, level 3" },
484
+ { code: "99214", description: "Established patient office visit, level 4" },
485
+ { code: "99215", description: "Established patient office visit, level 5" },
486
+ { code: "96130", description: "Psychological testing evaluation, first hour" },
487
+ { code: "96131", description: "Psychological testing evaluation, each additional hour", allowedAmount: 131.96 },
488
+ { code: "97161", description: "Physical therapy evaluation, low complexity" },
489
+ { code: "97162", description: "Physical therapy evaluation, moderate complexity" },
490
+ { code: "97163", description: "Physical therapy evaluation, high complexity" },
491
+ { code: "97110", description: "Therapeutic exercises" },
492
+ { code: "97140", description: "Manual therapy techniques" },
493
+ { code: "98940", description: "Chiropractic manipulative treatment, 1–2 regions" },
494
+ { code: "98941", description: "Chiropractic manipulative treatment, 3–4 regions" },
495
+ { code: "72141", description: "MRI cervical spine without contrast" },
496
+ { code: "72148", description: "MRI lumbar spine without contrast" },
497
+ { code: "73721", description: "MRI lower-extremity joint without contrast" },
498
+ { code: "20610", description: "Major joint injection or aspiration" },
499
+ { code: "99070", description: "Supplies and materials" },
500
+ ];
501
+ const MODIFIERS = [
502
+ { code: "92", description: "Primary Treating Physician evaluation" },
503
+ { code: "93", description: "Interpreter required" },
504
+ { code: "94", description: "Agreed Medical Evaluator" },
505
+ { code: "95", description: "Qualified Medical Evaluator" },
506
+ { code: "96", description: "Psychiatric/psychological evaluation" },
507
+ { code: "97", description: "Toxicology evaluation" },
508
+ { code: "98", description: "Oncology evaluation" },
509
+ { code: "59", description: "Distinct procedural service" },
510
+ { code: "XE", description: "Separate encounter" },
511
+ { code: "XS", description: "Separate structure" },
512
+ { code: "XP", description: "Separate practitioner" },
513
+ { code: "XU", description: "Unusual non-overlapping service" },
514
+ ];
515
+ const TAXONOMIES = [
516
+ { code: "103G00000X", description: "Clinical Neuropsychologist" },
517
+ { code: "103T00000X", description: "Psychologist" },
518
+ { code: "111N00000X", description: "Chiropractor" },
519
+ { code: "207Q00000X", description: "Family Medicine" },
520
+ { code: "207R00000X", description: "Internal Medicine" },
521
+ { code: "207X00000X", description: "Orthopaedic Surgery" },
522
+ { code: "207XS0117X", description: "Orthopaedic Surgery of the Spine" },
523
+ { code: "208100000X", description: "Physical Medicine & Rehabilitation" },
524
+ { code: "2083X0100X", description: "Occupational Medicine" },
525
+ { code: "2084N0400X", description: "Neurology" },
526
+ { code: "2084P0800X", description: "Psychiatry" },
527
+ { code: "208VP0000X", description: "Pain Medicine" },
528
+ { code: "213E00000X", description: "Podiatrist" },
529
+ { code: "225100000X", description: "Physical Therapist" },
530
+ { code: "225X00000X", description: "Occupational Therapist" },
531
+ { code: "363A00000X", description: "Physician Assistant" },
532
+ { code: "363L00000X", description: "Nurse Practitioner" },
533
+ ];
534
+ const QUICK_DIAGNOSES = [
535
+ { label: "Psych", code: "Z04.6", description: "General psychiatric examination requested by authority" },
536
+ { label: "Back", code: "M54.50", description: "Low back pain, unspecified" },
537
+ { label: "Neck", code: "M54.2", description: "Cervicalgia" },
538
+ { label: "Left hand", code: "M79.642", description: "Pain in left hand" },
539
+ { label: "Right hand", code: "M79.641", description: "Pain in right hand" },
540
+ { label: "Left knee", code: "M25.562", description: "Pain in left knee" },
541
+ { label: "Right knee", code: "M25.561", description: "Pain in right knee" },
542
+ ];
475
543
  const THEME$4 = {
476
544
  mindbill: { a: "#238dbd", ac: "#fff", bg: "#f3f8fa", s: "#fff", t: "#203743", m: "#657982", b: "#dbe6ea", r: "14px", cr: "8px", font: "Inter,system-ui,sans-serif" },
477
545
  "qme-companion": { a: "#53b5dc", ac: "#173542", bg: "#f2f8fb", s: "#fff", t: "#1d3440", m: "#617783", b: "#d7e5eb", r: "12px", cr: "8px", font: "Inter,system-ui,sans-serif" },
478
546
  "orange-bright": { a: "#f4510b", ac: "#fff", bg: "#fffaf6", s: "#fffefd", t: "#090f1f", m: "#626a73", b: "#e7e1da", r: "16px", cr: "10px", font: "Inter,system-ui,sans-serif" },
479
- "clinical-blue": { a: "#1677ff", ac: "#fff", bg: "#f5f7fa", s: "#fff", t: "#1f2d3d", m: "#66788a", b: "#d9e2ec", r: "8px", cr: "6px", font: "Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif" },
547
+ "clinical-blue": { a: "#1677ff", ac: "#fff", bg: "#fff", s: "#fff", t: "#1f2d3d", m: "#66788a", b: "#d9e2ec", r: "8px", cr: "6px", font: "Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif" },
480
548
  };
481
- function mindBillAgingDays(bill, now = new Date()) {
482
- if (bill.agingDays != null)
483
- return Math.max(0, Math.floor(bill.agingDays));
484
- const source = bill.submittedAt ?? bill.updatedAt;
485
- if (!source)
486
- return 0;
487
- const time = new Date(source).getTime();
488
- return Number.isFinite(time) ? Math.max(0, Math.floor((now.getTime() - time) / 86_400_000)) : 0;
489
- }
490
- function mindBillAgingBucket(days) {
491
- if (days <= 30)
492
- return "current";
493
- if (days <= 60)
494
- return "31-60";
495
- if (days <= 90)
496
- return "61-90";
497
- return "91+";
498
- }
499
- function summarizeMindBillDashboard(bills, now = new Date()) {
500
- const aging = [
501
- { id: "current", label: "0–30 days", count: 0, balance: 0 },
502
- { id: "31-60", label: "31–60 days", count: 0, balance: 0 },
503
- { id: "61-90", label: "61–90 days", count: 0, balance: 0 },
504
- { id: "91+", label: "91+ days", count: 0, balance: 0 },
505
- ];
506
- for (const bill of bills) {
507
- const bucket = aging.find((item) => item.id === mindBillAgingBucket(mindBillAgingDays(bill, now)));
508
- bucket.count += 1;
509
- bucket.balance += bill.balanceDue;
510
- }
511
- return {
512
- totalBilled: bills.reduce((sum, item) => sum + item.totalCharge, 0),
513
- totalPaid: bills.reduce((sum, item) => sum + item.totalPaid, 0),
514
- outstanding: bills.reduce((sum, item) => sum + item.balanceDue, 0),
515
- openCount: bills.filter((item) => !/closed|paid|written.off/i.test(item.state)).length,
516
- submittedThisMonth: bills.filter((item) => sameMonth(item.submittedAt, now)).length,
517
- closedThisMonth: bills.filter((item) => /closed|paid|written.off/i.test(item.state) && sameMonth(item.updatedAt ?? item.submittedAt, now)).length,
518
- bills,
519
- aging,
520
- };
521
- }
522
- function sameMonth(value, now) {
523
- if (!value)
524
- return false;
525
- const date = new Date(value);
526
- return Number.isFinite(date.getTime()) && date.getUTCFullYear() === now.getUTCFullYear() && date.getUTCMonth() === now.getUTCMonth();
549
+ function deepCopyBill(value) {
550
+ return JSON.parse(JSON.stringify(value));
527
551
  }
528
- function buildMindBillReportRows(bills, dimension) {
529
- const rows = new Map();
530
- for (const bill of bills) {
531
- const key = dimension === "payer" ? bill.payerName || "Unassigned payer" : dimension === "aging" ? mindBillAgingBucket(mindBillAgingDays(bill)) : bill.state;
532
- const label = key.replace(/_/g, " ").replace(/\b\w/g, (value) => value.toUpperCase());
533
- const row = rows.get(key) ?? { key, label, billCount: 0, totalBilled: 0, totalPaid: 0, balanceDue: 0 };
534
- row.billCount += 1;
535
- row.totalBilled += bill.totalCharge;
536
- row.totalPaid += bill.totalPaid;
537
- row.balanceDue += bill.balanceDue;
538
- rows.set(key, row);
539
- }
540
- return [...rows.values()].sort((a, b) => b.balanceDue - a.balanceDue || a.label.localeCompare(b.label));
552
+ function required(value) {
553
+ return typeof value === "string" ? value.trim().length > 0 : value != null;
541
554
  }
542
- function buildMindBillReportCsv(rows) {
543
- const quote = (value) => `"${String(value).replace(/"/g, '""')}"`;
544
- return ["Group,Bills,Total billed,Total paid,Balance due", ...rows.map((row) => [row.label, row.billCount, row.totalBilled, row.totalPaid, row.balanceDue].map(quote).join(","))].join("\n");
555
+ async function blobToBase64(blob) {
556
+ const buffer = new Uint8Array(await blob.arrayBuffer());
557
+ let binary = "";
558
+ for (const byte of buffer)
559
+ binary += String.fromCharCode(byte);
560
+ return btoa(binary);
545
561
  }
546
- class DashboardBase {
562
+ class MindBillBillSubmissionComponent {
563
+ initialBill;
564
+ attachments = [];
547
565
  appearance = { preset: "mindbill" };
566
+ sessionEndpoint = "/api/mindbill/session";
567
+ getSession;
568
+ apiBaseUrl;
569
+ heading = "Bill information";
570
+ description = "Review the bill details, add attachments, and submit.";
571
+ submitLabel = "Submit bill";
572
+ attentionFields = [];
573
+ procedureOptions = PROCEDURES;
574
+ modifierOptions = MODIFIERS;
575
+ taxonomyOptions = TAXONOMIES;
576
+ submitter;
577
+ submitted = new EventEmitter();
578
+ submissionError = new EventEmitter();
579
+ billChange = new EventEmitter();
580
+ bill = {};
581
+ workingAttachments = [];
582
+ payerQuery = "";
583
+ payerResults = [];
584
+ payerBusy = false;
585
+ diagnosisQuery = "";
586
+ diagnosisResults = [];
587
+ diagnosisOffset = 0;
588
+ diagnosisBusy = false;
589
+ submitting = false;
590
+ errorMessage = "";
591
+ postalStatus = "";
592
+ dragActive = false;
593
+ invalidFields = new Set();
594
+ attentionFieldSet = new Set();
595
+ quickDiagnoses = QUICK_DIAGNOSES;
596
+ // Zoneless-safe rendering: async work (debounced lookups, fetches, file reads)
597
+ // must schedule change detection explicitly for zone-less Angular hosts.
598
+ changeDetector = inject(ChangeDetectorRef);
599
+ customProcedureOption = mindBillCustomProcedureOption;
600
+ customTaxonomyOption = (query) => {
601
+ const code = query.trim().toUpperCase();
602
+ return /^[A-Z0-9]{10}$/.test(code) ? { id: code, label: code, detail: "Use this taxonomy code" } : null;
603
+ };
604
+ searchTimer;
605
+ postalTimer;
606
+ ngOnChanges(changes) {
607
+ if (changes["initialBill"] && this.initialBill) {
608
+ this.bill = deepCopyBill(this.initialBill);
609
+ if (!this.bill.serviceLines.length)
610
+ this.bill.serviceLines = [{ code: "", units: 1 }];
611
+ this.ensureTrailingLine();
612
+ this.payerQuery = this.bill.claim.claimsAdministrator.name ?? "";
613
+ }
614
+ if (changes["attachments"])
615
+ this.workingAttachments = this.attachments.map((item) => ({ ...item }));
616
+ if (changes["attentionFields"])
617
+ this.attentionFieldSet = new Set(this.attentionFields);
618
+ }
548
619
  get themeStyle() {
549
620
  const base = THEME$4[this.appearance.preset ?? "mindbill"] ?? THEME$4["mindbill"];
550
621
  return {
@@ -555,885 +626,1383 @@ class DashboardBase {
555
626
  "--cr": this.appearance.controlRadius ?? base["cr"], "--font": this.appearance.fontFamily ?? base["font"],
556
627
  };
557
628
  }
558
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DashboardBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
559
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.22", type: DashboardBase, isStandalone: true, inputs: { appearance: "appearance" }, ngImport: i0 });
560
- }
561
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DashboardBase, decorators: [{
562
- type: Directive
563
- }], propDecorators: { appearance: [{
564
- type: Input
565
- }] } });
566
- const sharedStyles = `
567
- :host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}
568
- .money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}
569
- `;
570
- class MindBillBillAgingSummaryComponent extends DashboardBase {
571
- buckets = [];
572
- selectedBucket = "";
573
- bucketSelected = new EventEmitter();
574
- width(balance) { const max = Math.max(1, ...this.buckets.map((item) => item.balance)); return Math.max(balance ? 8 : 0, balance / max * 100); }
575
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillAgingSummaryComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
576
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillAgingSummaryComponent, isStandalone: true, selector: "mindbill-bill-aging-summary", inputs: { buckets: "buckets", selectedBucket: "selectedBucket" }, outputs: { bucketSelected: "bucketSelected" }, usesInheritance: true, ngImport: i0, template: `<section class="mbd aging" [ngStyle]="themeStyle" aria-label="Accounts receivable aging">@for (bucket of buckets; track bucket.id) { <button type="button" class="card" [class.selected]="selectedBucket === bucket.id" (click)="bucketSelected.emit(bucket.id)"><div><strong>{{ bucket.label }}</strong><span>{{ bucket.count }} bill{{ bucket.count === 1 ? '' : 's' }}</span></div><b class="money">{{ bucket.balance | currency }}</b><i><em [style.width.%]="width(bucket.balance)"></em></i></button> }</section>`, isInline: true, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".aging{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.aging>button{border:1px solid var(--b);padding:16px;text-align:left;color:var(--t);cursor:pointer}.aging>button:hover,.aging>button.selected{border-color:var(--a);background:color-mix(in srgb,var(--a) 6%,var(--s))}.aging button>div{display:flex;justify-content:space-between;gap:8px}.aging span{color:var(--m);font-size:12px}.aging b{display:block;font-size:20px;margin:14px 0 12px}.aging i{display:block;height:5px;border-radius:99px;background:color-mix(in srgb,var(--b) 70%,transparent);overflow:hidden}.aging em{display:block;height:100%;background:var(--a)}@media(max-width:760px){.aging{grid-template-columns:1fr 1fr}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
577
- }
578
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillAgingSummaryComponent, decorators: [{
579
- type: Component,
580
- args: [{ selector: "mindbill-bill-aging-summary", standalone: true, imports: [CommonModule], template: `<section class="mbd aging" [ngStyle]="themeStyle" aria-label="Accounts receivable aging">@for (bucket of buckets; track bucket.id) { <button type="button" class="card" [class.selected]="selectedBucket === bucket.id" (click)="bucketSelected.emit(bucket.id)"><div><strong>{{ bucket.label }}</strong><span>{{ bucket.count }} bill{{ bucket.count === 1 ? '' : 's' }}</span></div><b class="money">{{ bucket.balance | currency }}</b><i><em [style.width.%]="width(bucket.balance)"></em></i></button> }</section>`, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".aging{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.aging>button{border:1px solid var(--b);padding:16px;text-align:left;color:var(--t);cursor:pointer}.aging>button:hover,.aging>button.selected{border-color:var(--a);background:color-mix(in srgb,var(--a) 6%,var(--s))}.aging button>div{display:flex;justify-content:space-between;gap:8px}.aging span{color:var(--m);font-size:12px}.aging b{display:block;font-size:20px;margin:14px 0 12px}.aging i{display:block;height:5px;border-radius:99px;background:color-mix(in srgb,var(--b) 70%,transparent);overflow:hidden}.aging em{display:block;height:100%;background:var(--a)}@media(max-width:760px){.aging{grid-template-columns:1fr 1fr}}\n"] }]
581
- }], propDecorators: { buckets: [{
582
- type: Input
583
- }], selectedBucket: [{
584
- type: Input
585
- }], bucketSelected: [{
586
- type: Output
587
- }] } });
588
- class MindBillBillListComponent extends DashboardBase {
589
- bills = [];
590
- billSelected = new EventEmitter();
591
- age(bill) { return mindBillAgingDays(bill); }
592
- label(value) { return value.replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); }
593
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillListComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
594
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillListComponent, isStandalone: true, selector: "mindbill-bill-list", inputs: { bills: "bills" }, outputs: { billSelected: "billSelected" }, usesInheritance: true, ngImport: i0, template: `<section class="mbd list card" [ngStyle]="themeStyle"><div class="head"><strong>Bill</strong><strong>Payer</strong><strong>Status</strong><strong>Age</strong><strong>Balance</strong></div>@for (bill of bills; track bill.id) { <button type="button" (click)="billSelected.emit(bill)"><span><b>{{ bill.patientName }}</b><small>{{ bill.claimNumber || bill.workItemLabel || bill.externalId || bill.id }}</small></span><span>{{ bill.payerName || '—' }}</span><span><i class="pill">{{ label(bill.state) }}</i></span><span>{{ age(bill) }} days</span><span class="money"><b>{{ bill.balanceDue | currency }}</b><small>{{ bill.totalPaid | currency }} paid</small></span></button> } @empty { <p class="empty">No bills match these filters.</p> }</section>`, isInline: true, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".list{overflow:hidden}.head,.list button{display:grid;grid-template-columns:1.6fr 1.2fr .85fr .55fr .8fr;gap:18px;align-items:center;text-align:left}.head{padding:11px 16px;background:color-mix(in srgb,var(--bg) 78%,var(--s));color:var(--m);font-size:11px;letter-spacing:.08em;text-transform:uppercase}.list button{width:100%;border:0;border-top:1px solid var(--b);background:var(--s);color:var(--t);padding:15px 16px;cursor:pointer}.list button:hover{background:color-mix(in srgb,var(--a) 5%,var(--s))}.list span,.list small{min-width:0}.list small{display:block;color:var(--m);margin-top:4px}.money{text-align:right}.empty{padding:24px;text-align:center;color:var(--m)}@media(max-width:760px){.head{display:none}.list button{grid-template-columns:1fr auto;gap:10px}.list button>span:nth-child(2){grid-column:1}.list button>span:nth-child(3){grid-row:1;grid-column:2}.list button>span:nth-child(4){grid-column:1}.money{grid-column:2;grid-row:2/4}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
595
- }
596
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillListComponent, decorators: [{
597
- type: Component,
598
- args: [{ selector: "mindbill-bill-list", standalone: true, imports: [CommonModule], template: `<section class="mbd list card" [ngStyle]="themeStyle"><div class="head"><strong>Bill</strong><strong>Payer</strong><strong>Status</strong><strong>Age</strong><strong>Balance</strong></div>@for (bill of bills; track bill.id) { <button type="button" (click)="billSelected.emit(bill)"><span><b>{{ bill.patientName }}</b><small>{{ bill.claimNumber || bill.workItemLabel || bill.externalId || bill.id }}</small></span><span>{{ bill.payerName || '—' }}</span><span><i class="pill">{{ label(bill.state) }}</i></span><span>{{ age(bill) }} days</span><span class="money"><b>{{ bill.balanceDue | currency }}</b><small>{{ bill.totalPaid | currency }} paid</small></span></button> } @empty { <p class="empty">No bills match these filters.</p> }</section>`, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".list{overflow:hidden}.head,.list button{display:grid;grid-template-columns:1.6fr 1.2fr .85fr .55fr .8fr;gap:18px;align-items:center;text-align:left}.head{padding:11px 16px;background:color-mix(in srgb,var(--bg) 78%,var(--s));color:var(--m);font-size:11px;letter-spacing:.08em;text-transform:uppercase}.list button{width:100%;border:0;border-top:1px solid var(--b);background:var(--s);color:var(--t);padding:15px 16px;cursor:pointer}.list button:hover{background:color-mix(in srgb,var(--a) 5%,var(--s))}.list span,.list small{min-width:0}.list small{display:block;color:var(--m);margin-top:4px}.money{text-align:right}.empty{padding:24px;text-align:center;color:var(--m)}@media(max-width:760px){.head{display:none}.list button{grid-template-columns:1fr auto;gap:10px}.list button>span:nth-child(2){grid-column:1}.list button>span:nth-child(3){grid-row:1;grid-column:2}.list button>span:nth-child(4){grid-column:1}.money{grid-column:2;grid-row:2/4}}\n"] }]
599
- }], propDecorators: { bills: [{
600
- type: Input
601
- }], billSelected: [{
602
- type: Output
603
- }] } });
604
- class MindBillBillingDashboardComponent extends DashboardBase {
605
- bills = [];
606
- heading = "Billing dashboard";
607
- description = "Track every submitted bill from payer acceptance through payment.";
608
- billSelected = new EventEmitter();
609
- createBill = new EventEmitter();
610
- query = "";
611
- state = "";
612
- agingBucket = "";
613
- get summary() { return summarizeMindBillDashboard(this.bills); }
614
- get states() { return [...new Set(this.bills.map((bill) => bill.state))].sort(); }
615
- get filteredBills() { const query = this.query.trim().toLowerCase(); return this.bills.filter((bill) => (!this.state || bill.state === this.state) && (!this.agingBucket || mindBillAgingBucket(mindBillAgingDays(bill)) === this.agingBucket) && (!query || [bill.patientName, bill.claimNumber, bill.payerName, bill.externalId, bill.billNumber].some((value) => String(value ?? "").toLowerCase().includes(query)))); }
616
- get agingLabel() { return this.summary.aging.find((item) => item.id === this.agingBucket)?.label ?? ""; }
617
- toggleAging(bucket) { this.agingBucket = this.agingBucket === bucket ? "" : bucket; }
618
- label(value) { return value.replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); }
619
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingDashboardComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
620
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillingDashboardComponent, isStandalone: true, selector: "mindbill-billing-dashboard", inputs: { bills: "bills", heading: "heading", description: "description" }, outputs: { billSelected: "billSelected", createBill: "createBill" }, usesInheritance: true, ngImport: i0, template: `<section class="mbd dashboard" [ngStyle]="themeStyle"><header><div><p>Billing operations</p><h2>{{ heading }}</h2><span class="muted">{{ description }}</span></div><button type="button" (click)="createBill.emit()">+ New bill</button></header><div class="metrics"><article class="card"><span>Total billed</span><b>{{ summary.totalBilled | currency }}</b></article><article class="card"><span>Outstanding</span><b>{{ summary.outstanding | currency }}</b></article><article class="card"><span>Submitted this month</span><b>{{ summary.submittedThisMonth }}</b></article><article class="card"><span>Closed this month</span><b>{{ summary.closedThisMonth }}</b></article></div><mindbill-bill-aging-summary [buckets]="summary.aging" [selectedBucket]="agingBucket" [appearance]="appearance" (bucketSelected)="toggleAging($event)"/><div class="filters"><input [(ngModel)]="query" placeholder="Search patient, claim, or payer" aria-label="Search bills"><select [(ngModel)]="state"><option value="">All statuses</option>@for (option of states; track option) { <option [value]="option">{{ label(option) }}</option> }</select>@if (agingBucket) { <button type="button" class="clear" (click)="agingBucket=''">{{ agingLabel }} ×</button> }</div><mindbill-bill-list [bills]="filteredBills" [appearance]="appearance" (billSelected)="billSelected.emit($event)"/></section>`, isInline: true, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".dashboard{display:grid;gap:18px}.dashboard header{display:flex;justify-content:space-between;align-items:end;gap:20px}.dashboard h2{font-size:28px;margin:2px 0 5px}.dashboard header p{margin:0;color:var(--a);font-size:12px;font-weight:800;letter-spacing:.12em;text-transform:uppercase}.dashboard header button{border:0;border-radius:var(--cr);background:var(--a);color:var(--ac);padding:11px 16px;font-weight:750;cursor:pointer}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.metrics article{padding:18px}.metrics span{display:block;color:var(--m);font-size:12px}.metrics b{display:block;margin-top:8px;font-size:24px}.filters{display:grid;grid-template-columns:1fr 190px auto;gap:10px}.filters input,.filters select,.filters .clear{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);color:var(--t);padding:11px 13px}.filters .clear{color:var(--a);font-weight:700;cursor:pointer}@media(max-width:760px){.dashboard header{align-items:start}.metrics{grid-template-columns:1fr 1fr}.filters{grid-template-columns:1fr}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { 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.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.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MindBillBillAgingSummaryComponent, selector: "mindbill-bill-aging-summary", inputs: ["buckets", "selectedBucket"], outputs: ["bucketSelected"] }, { kind: "component", type: MindBillBillListComponent, selector: "mindbill-bill-list", inputs: ["bills"], outputs: ["billSelected"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
621
- }
622
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingDashboardComponent, decorators: [{
623
- type: Component,
624
- args: [{ selector: "mindbill-billing-dashboard", standalone: true, imports: [CommonModule, FormsModule, MindBillBillAgingSummaryComponent, MindBillBillListComponent], template: `<section class="mbd dashboard" [ngStyle]="themeStyle"><header><div><p>Billing operations</p><h2>{{ heading }}</h2><span class="muted">{{ description }}</span></div><button type="button" (click)="createBill.emit()">+ New bill</button></header><div class="metrics"><article class="card"><span>Total billed</span><b>{{ summary.totalBilled | currency }}</b></article><article class="card"><span>Outstanding</span><b>{{ summary.outstanding | currency }}</b></article><article class="card"><span>Submitted this month</span><b>{{ summary.submittedThisMonth }}</b></article><article class="card"><span>Closed this month</span><b>{{ summary.closedThisMonth }}</b></article></div><mindbill-bill-aging-summary [buckets]="summary.aging" [selectedBucket]="agingBucket" [appearance]="appearance" (bucketSelected)="toggleAging($event)"/><div class="filters"><input [(ngModel)]="query" placeholder="Search patient, claim, or payer" aria-label="Search bills"><select [(ngModel)]="state"><option value="">All statuses</option>@for (option of states; track option) { <option [value]="option">{{ label(option) }}</option> }</select>@if (agingBucket) { <button type="button" class="clear" (click)="agingBucket=''">{{ agingLabel }} ×</button> }</div><mindbill-bill-list [bills]="filteredBills" [appearance]="appearance" (billSelected)="billSelected.emit($event)"/></section>`, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".dashboard{display:grid;gap:18px}.dashboard header{display:flex;justify-content:space-between;align-items:end;gap:20px}.dashboard h2{font-size:28px;margin:2px 0 5px}.dashboard header p{margin:0;color:var(--a);font-size:12px;font-weight:800;letter-spacing:.12em;text-transform:uppercase}.dashboard header button{border:0;border-radius:var(--cr);background:var(--a);color:var(--ac);padding:11px 16px;font-weight:750;cursor:pointer}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.metrics article{padding:18px}.metrics span{display:block;color:var(--m);font-size:12px}.metrics b{display:block;margin-top:8px;font-size:24px}.filters{display:grid;grid-template-columns:1fr 190px auto;gap:10px}.filters input,.filters select,.filters .clear{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);color:var(--t);padding:11px 13px}.filters .clear{color:var(--a);font-weight:700;cursor:pointer}@media(max-width:760px){.dashboard header{align-items:start}.metrics{grid-template-columns:1fr 1fr}.filters{grid-template-columns:1fr}}\n"] }]
625
- }], propDecorators: { bills: [{
626
- type: Input
627
- }], heading: [{
628
- type: Input
629
- }], description: [{
630
- type: Input
631
- }], billSelected: [{
632
- type: Output
633
- }], createBill: [{
634
- type: Output
635
- }] } });
636
- class MindBillBillingReportComponent extends DashboardBase {
637
- bills = [];
638
- heading = "Billing report";
639
- dimension = "status";
640
- get rows() { return buildMindBillReportRows(this.bills, this.dimension); }
641
- downloadCsv() { const blob = new Blob([buildMindBillReportCsv(this.rows)], { type: "text/csv;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `mindbill-${this.dimension}-report.csv`; link.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); }
642
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingReportComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
643
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillingReportComponent, isStandalone: true, selector: "mindbill-billing-report", inputs: { bills: "bills", heading: "heading" }, usesInheritance: true, ngImport: i0, template: `<section class="mbd report card" [ngStyle]="themeStyle"><header><div><h2>{{ heading }}</h2><p>Operational totals grouped for reconciliation and follow-up.</p></div><div><select [(ngModel)]="dimension"><option value="status">Status</option><option value="payer">Payer</option><option value="aging">Aging</option></select><button type="button" (click)="downloadCsv()">Export CSV</button></div></header><div class="thead"><b>Group</b><b>Bills</b><b>Total billed</b><b>Total paid</b><b>Balance due</b></div>@for (row of rows; track row.key) { <div class="row"><strong>{{ row.label }}</strong><span>{{ row.billCount }}</span><span>{{ row.totalBilled | currency }}</span><span>{{ row.totalPaid | currency }}</span><b>{{ row.balanceDue | currency }}</b></div> }</section>`, isInline: true, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".report{overflow:hidden}.report header{display:flex;justify-content:space-between;align-items:center;padding:18px}.report h2{margin:0;font-size:22px}.report p{margin:4px 0 0;color:var(--m)}.report header div:last-child{display:flex;gap:8px}.report select,.report button{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);color:var(--t);padding:9px 12px}.report button{background:var(--a);border-color:var(--a);color:var(--ac);font-weight:750;cursor:pointer}.thead,.row{display:grid;grid-template-columns:1.5fr .5fr 1fr 1fr 1fr;gap:14px;padding:12px 18px;border-top:1px solid var(--b);align-items:center}.thead{background:color-mix(in srgb,var(--bg) 78%,var(--s));color:var(--m);font-size:11px;text-transform:uppercase}.row span,.row b:not(:first-child){text-align:right;font-variant-numeric:tabular-nums}@media(max-width:700px){.report header{align-items:flex-start}.thead{display:none}.row{grid-template-columns:1fr 1fr}.row>*:nth-child(n+2){text-align:right}.row strong{grid-column:1}.row span:nth-child(2){grid-column:2}.row span:nth-child(3):before{content:\"Billed \";color:var(--m)}.row span:nth-child(4):before{content:\"Paid \";color:var(--m)}.row b:before{content:\"Due \";color:var(--m)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { 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.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
644
- }
645
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingReportComponent, decorators: [{
646
- type: Component,
647
- args: [{ selector: "mindbill-billing-report", standalone: true, imports: [CommonModule, FormsModule], template: `<section class="mbd report card" [ngStyle]="themeStyle"><header><div><h2>{{ heading }}</h2><p>Operational totals grouped for reconciliation and follow-up.</p></div><div><select [(ngModel)]="dimension"><option value="status">Status</option><option value="payer">Payer</option><option value="aging">Aging</option></select><button type="button" (click)="downloadCsv()">Export CSV</button></div></header><div class="thead"><b>Group</b><b>Bills</b><b>Total billed</b><b>Total paid</b><b>Balance due</b></div>@for (row of rows; track row.key) { <div class="row"><strong>{{ row.label }}</strong><span>{{ row.billCount }}</span><span>{{ row.totalBilled | currency }}</span><span>{{ row.totalPaid | currency }}</span><b>{{ row.balanceDue | currency }}</b></div> }</section>`, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".report{overflow:hidden}.report header{display:flex;justify-content:space-between;align-items:center;padding:18px}.report h2{margin:0;font-size:22px}.report p{margin:4px 0 0;color:var(--m)}.report header div:last-child{display:flex;gap:8px}.report select,.report button{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);color:var(--t);padding:9px 12px}.report button{background:var(--a);border-color:var(--a);color:var(--ac);font-weight:750;cursor:pointer}.thead,.row{display:grid;grid-template-columns:1.5fr .5fr 1fr 1fr 1fr;gap:14px;padding:12px 18px;border-top:1px solid var(--b);align-items:center}.thead{background:color-mix(in srgb,var(--bg) 78%,var(--s));color:var(--m);font-size:11px;text-transform:uppercase}.row span,.row b:not(:first-child){text-align:right;font-variant-numeric:tabular-nums}@media(max-width:700px){.report header{align-items:flex-start}.thead{display:none}.row{grid-template-columns:1fr 1fr}.row>*:nth-child(n+2){text-align:right}.row strong{grid-column:1}.row span:nth-child(2){grid-column:2}.row span:nth-child(3):before{content:\"Billed \";color:var(--m)}.row span:nth-child(4):before{content:\"Paid \";color:var(--m)}.row b:before{content:\"Due \";color:var(--m)}}\n"] }]
648
- }], propDecorators: { bills: [{
649
- type: Input
650
- }], heading: [{
651
- type: Input
652
- }] } });
653
-
654
- const MINDBILL_STATUS_AGING_BUCKETS = [
655
- { id: "current", label: "0–30 days" },
656
- { id: "31-60", label: "31–60 days" },
657
- { id: "61-90", label: "61–90 days" },
658
- { id: "91+", label: "91+ days" },
659
- ];
660
- const DEFAULT_STATE_ORDER = [
661
- "draft", "incomplete", "created", "sent", "submitted", "accepted", "processed",
662
- "paid", "underpaid", "denied", "rejected", "appealing", "second_review", "ibr", "lien", "closed",
663
- ];
664
- const agingDays = (bill, now) => {
665
- if (bill.agingDays != null)
666
- return Math.max(0, Math.floor(bill.agingDays));
667
- const source = bill.submittedAt ?? bill.updatedAt;
668
- if (!source)
669
- return 0;
670
- const time = new Date(source).getTime();
671
- return Number.isFinite(time) ? Math.max(0, Math.floor((now.getTime() - time) / 86_400_000)) : 0;
672
- };
673
- const agingBucketIndex = (days) => (days <= 30 ? 0 : days <= 60 ? 1 : days <= 90 ? 2 : 3);
674
- const stateLabel = (state) => state.replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
675
- const emptyCell = (state, bucket) => ({ state, bucket, count: 0, balance: 0, bills: [] });
676
- function buildMindBillStatusAgingMatrix(bills, stateOrder = DEFAULT_STATE_ORDER, now = new Date()) {
677
- const orderIndex = new Map(stateOrder.map((state, index) => [state.toLowerCase(), index]));
678
- const states = [...new Set(bills.map((bill) => bill.state))].sort((a, b) => {
679
- const left = orderIndex.get(a.toLowerCase()) ?? stateOrder.length;
680
- const right = orderIndex.get(b.toLowerCase()) ?? stateOrder.length;
681
- return left - right || a.localeCompare(b);
682
- });
683
- const columnTotals = MINDBILL_STATUS_AGING_BUCKETS.map((bucket) => emptyCell("all", bucket.id));
684
- const grandTotal = emptyCell("all", "total");
685
- const rows = states.map((state) => ({
686
- state,
687
- label: stateLabel(state),
688
- cells: MINDBILL_STATUS_AGING_BUCKETS.map((bucket) => emptyCell(state, bucket.id)),
689
- total: emptyCell(state, "total"),
690
- }));
691
- const rowByState = new Map(rows.map((row) => [row.state, row]));
692
- for (const bill of bills) {
693
- const row = rowByState.get(bill.state);
694
- const bucketIndex = agingBucketIndex(agingDays(bill, now));
695
- for (const cell of [row.cells[bucketIndex], row.total, columnTotals[bucketIndex], grandTotal]) {
696
- cell.count += 1;
697
- cell.balance += bill.balanceDue;
698
- cell.bills.push(bill);
629
+ get procedureComboOptions() {
630
+ return this.procedureOptions.map((item) => ({ id: item.code, label: item.code, detail: item.description }));
631
+ }
632
+ get taxonomyComboOptions() {
633
+ return this.taxonomyOptions.map((item) => ({ id: item.code, label: item.description, detail: item.code }));
634
+ }
635
+ modifierComboOptions(line) {
636
+ return this.modifierOptions
637
+ .filter((item) => !(line.modifiers ?? []).includes(item.code))
638
+ .map((item) => ({ id: item.code, label: `−${item.code}`, detail: item.description }));
639
+ }
640
+ referenceClient() {
641
+ return createBillReferenceClient({ sessionEndpoint: this.sessionEndpoint, ...(this.getSession ? { getSession: this.getSession } : {}), ...(this.apiBaseUrl ? { apiBaseUrl: this.apiBaseUrl } : {}) });
642
+ }
643
+ setDate(path, value) {
644
+ if (path === "patient.dateOfBirth")
645
+ this.bill.patient.dateOfBirth = value;
646
+ else if (path === "service.date")
647
+ this.bill.service.date = value;
648
+ else
649
+ this.bill.claim.dateOfInjury = value;
650
+ if (value)
651
+ this.invalidFields.delete(path);
652
+ this.billChange.emit(this.bill);
653
+ }
654
+ postalChanged(value) {
655
+ clearTimeout(this.postalTimer);
656
+ this.postalStatus = "";
657
+ if (!/^\d{5}$/.test(value.trim()))
658
+ return;
659
+ this.postalStatus = "Looking up ZIP…";
660
+ this.postalTimer = setTimeout(async () => {
661
+ try {
662
+ const place = await this.referenceClient().lookupPostalCode(value);
663
+ if (place) {
664
+ this.bill.patient.address.city = place.city;
665
+ this.bill.patient.address.state = place.state.toUpperCase();
666
+ this.postalStatus = `${place.city}, ${place.state.toUpperCase()} filled from ZIP`;
667
+ this.invalidFields.delete("patient.address.city");
668
+ this.invalidFields.delete("patient.address.state");
669
+ this.billChange.emit(this.bill);
670
+ }
671
+ else {
672
+ this.postalStatus = "ZIP not found";
673
+ }
674
+ }
675
+ catch {
676
+ this.postalStatus = "ZIP lookup unavailable";
677
+ }
678
+ this.changeDetector.markForCheck();
679
+ }, 180);
680
+ }
681
+ searchPayers() {
682
+ clearTimeout(this.searchTimer);
683
+ this.searchTimer = setTimeout(async () => {
684
+ this.payerBusy = true;
685
+ try {
686
+ this.payerResults = await this.referenceClient().searchClaimsAdministrators(this.payerQuery, this.bill.claim.claimNumber);
687
+ }
688
+ catch {
689
+ this.payerResults = [];
690
+ }
691
+ finally {
692
+ this.payerBusy = false;
693
+ this.changeDetector.markForCheck();
694
+ }
695
+ }, 180);
696
+ }
697
+ selectPayer(payer) {
698
+ this.bill.claim.claimsAdministrator = { id: payer.id, name: payer.name };
699
+ this.payerQuery = payer.name;
700
+ this.payerResults = [];
701
+ this.invalidFields.delete("claim.claimsAdministrator");
702
+ this.billChange.emit(this.bill);
703
+ }
704
+ searchDiagnoses(reset) {
705
+ if (this.diagnosisBusy)
706
+ return;
707
+ if (reset) {
708
+ this.diagnosisOffset = 0;
709
+ this.diagnosisResults = [];
710
+ }
711
+ clearTimeout(this.searchTimer);
712
+ this.searchTimer = setTimeout(async () => {
713
+ this.diagnosisBusy = true;
714
+ try {
715
+ const results = await this.referenceClient().searchDiagnosisCodes(this.diagnosisQuery, 60, this.diagnosisOffset);
716
+ this.diagnosisResults = reset ? results : [...this.diagnosisResults, ...results];
717
+ this.diagnosisOffset = this.diagnosisResults.length;
718
+ }
719
+ catch {
720
+ if (reset)
721
+ this.diagnosisResults = [];
722
+ }
723
+ finally {
724
+ this.diagnosisBusy = false;
725
+ this.changeDetector.markForCheck();
726
+ }
727
+ }, reset ? 160 : 0);
728
+ }
729
+ diagnosisScrolled(event) {
730
+ const element = event.currentTarget;
731
+ if (element.scrollTop + element.clientHeight >= element.scrollHeight - 80)
732
+ this.searchDiagnoses(false);
733
+ }
734
+ hasDiagnosis(code) { return this.bill.diagnoses.includes(code); }
735
+ toggleDiagnosis(item) {
736
+ if (this.hasDiagnosis(item.code))
737
+ this.removeDiagnosis(item.code);
738
+ else
739
+ this.addDiagnosis(item);
740
+ }
741
+ addDiagnosis(item) { if (!this.hasDiagnosis(item.code))
742
+ this.bill.diagnoses.push(item.code); this.diagnosisQuery = ""; this.diagnosisResults = []; this.invalidFields.delete("diagnoses"); this.billChange.emit(this.bill); }
743
+ removeDiagnosis(code) { this.bill.diagnoses = this.bill.diagnoses.filter((item) => item !== code); this.billChange.emit(this.bill); }
744
+ ensureTrailingLine() {
745
+ const last = this.bill.serviceLines.at(-1);
746
+ if (!last || last.code.trim())
747
+ this.bill.serviceLines.push({ code: "", units: 1 });
748
+ this.billChange.emit(this.bill);
749
+ }
750
+ removeLine(index) { this.bill.serviceLines.splice(index, 1); if (!this.bill.serviceLines.length)
751
+ this.bill.serviceLines.push({ code: "", units: 1 }); this.ensureTrailingLine(); }
752
+ selectProcedure(index, option) {
753
+ this.bill.serviceLines[index].code = option.id;
754
+ this.invalidFields.delete("serviceLines");
755
+ this.ensureTrailingLine();
756
+ }
757
+ addModifier(index, option) {
758
+ const line = this.bill.serviceLines[index];
759
+ line.modifiers = [...new Set([...(line.modifiers ?? []), option.id])];
760
+ this.billChange.emit(this.bill);
761
+ }
762
+ removeModifier(index, modifier) {
763
+ const line = this.bill.serviceLines[index];
764
+ line.modifiers = (line.modifiers ?? []).filter((item) => item !== modifier);
765
+ this.billChange.emit(this.bill);
766
+ }
767
+ dxLetter(pointer) { return String.fromCharCode(64 + pointer); }
768
+ hasDiagnosisPointer(line, pointer) {
769
+ return (line.diagnosisPointers ?? []).includes(pointer);
770
+ }
771
+ toggleDiagnosisPointer(index, pointer) {
772
+ const line = this.bill.serviceLines[index];
773
+ line.diagnosisPointers = this.hasDiagnosisPointer(line, pointer)
774
+ ? (line.diagnosisPointers ?? []).filter((item) => item !== pointer)
775
+ : [...(line.diagnosisPointers ?? []), pointer].sort((left, right) => left - right);
776
+ this.billChange.emit(this.bill);
777
+ }
778
+ setAuthorizationNumber(value) {
779
+ this.bill.service.authorizationNumber = value.trim() ? value : null;
780
+ this.billChange.emit(this.bill);
781
+ }
782
+ selectTaxonomy(option) {
783
+ this.bill.renderingProvider.taxonomy = option.id;
784
+ this.invalidFields.delete("renderingProvider.taxonomy");
785
+ this.billChange.emit(this.bill);
786
+ }
787
+ procedureDescription(code) { return this.procedureOptions.find((item) => item.code.toLowerCase() === code.trim().toLowerCase())?.description ?? ""; }
788
+ allowed(line) { const base = this.procedureOptions.find((item) => item.code.toLowerCase() === line.code.trim().toLowerCase())?.allowedAmount; return base == null ? (line.charge ?? null) : Math.round(base * Math.max(1, line.units ?? 1) * 100) / 100; }
789
+ get totalAllowed() { return this.bill.serviceLines.reduce((sum, line) => sum + (this.allowed(line) ?? 0), 0); }
790
+ onWindowDragOver(event) {
791
+ if (event.dataTransfer?.types.includes("Files")) {
792
+ event.preventDefault();
793
+ this.dragActive = true;
794
+ this.changeDetector.markForCheck();
699
795
  }
700
796
  }
701
- return { rows, columnTotals, grandTotal };
702
- }
703
- function buildMindBillStatusAgingCsv(bills, stateOrder) {
704
- const matrix = buildMindBillStatusAgingMatrix(bills, stateOrder);
705
- const quote = (value) => `"${String(value).replace(/"/g, '""')}"`;
706
- const header = ["Status", ...MINDBILL_STATUS_AGING_BUCKETS.map((bucket) => bucket.label), "Total"];
707
- const lines = matrix.rows.map((row) => [row.label, ...row.cells.map((cell) => cell.count), row.total.count]);
708
- lines.push(["Total", ...matrix.columnTotals.map((cell) => cell.count), matrix.grandTotal.count]);
709
- return [header, ...lines].map((line) => line.map(quote).join(",")).join("\n");
710
- }
711
-
712
- const THEME$3 = {
713
- mindbill: { a: "#238dbd", ac: "#fff", bg: "#f3f8fa", s: "#fff", t: "#203743", m: "#657982", b: "#dbe6ea", r: "14px", cr: "8px", font: "Inter,system-ui,sans-serif" },
714
- "qme-companion": { a: "#53b5dc", ac: "#173542", bg: "#f2f8fb", s: "#fff", t: "#1d3440", m: "#617783", b: "#d7e5eb", r: "12px", cr: "8px", font: "Inter,system-ui,sans-serif" },
715
- "orange-bright": { a: "#f4510b", ac: "#fff", bg: "#fffaf6", s: "#fffefd", t: "#090f1f", m: "#626a73", b: "#e7e1da", r: "16px", cr: "10px", font: "Inter,system-ui,sans-serif" },
716
- "clinical-blue": { a: "#1677ff", ac: "#fff", bg: "#f5f7fa", s: "#fff", t: "#1f2d3d", m: "#66788a", b: "#d9e2ec", r: "8px", cr: "6px", font: "Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif" },
717
- };
718
- class MindBillStatusAgingMatrixComponent {
719
- bills = [];
720
- heading = "Bills by status and age";
721
- description = "Every bill grouped by lifecycle status, then by days outstanding.";
722
- stateOrder;
723
- showBalances = true;
724
- appearance = { preset: "mindbill" };
725
- cellSelected = new EventEmitter();
726
- buckets = MINDBILL_STATUS_AGING_BUCKETS;
727
- get matrix() {
728
- return buildMindBillStatusAgingMatrix(this.bills, this.stateOrder);
797
+ onWindowDragLeave(event) {
798
+ if (!event.relatedTarget) {
799
+ this.dragActive = false;
800
+ this.changeDetector.markForCheck();
801
+ }
729
802
  }
730
- get themeStyle() {
731
- const base = THEME$3[this.appearance.preset ?? "mindbill"] ?? THEME$3["mindbill"];
732
- return {
733
- "--a": this.appearance.accentColor ?? base["a"], "--ac": this.appearance.accentTextColor ?? base["ac"],
734
- "--bg": this.appearance.backgroundColor ?? base["bg"], "--s": this.appearance.surfaceColor ?? base["s"],
735
- "--t": this.appearance.textColor ?? base["t"], "--m": this.appearance.mutedColor ?? base["m"],
736
- "--b": this.appearance.borderColor ?? base["b"], "--r": this.appearance.borderRadius ?? base["r"],
737
- "--cr": this.appearance.controlRadius ?? base["cr"], "--font": this.appearance.fontFamily ?? base["font"],
738
- };
803
+ onWindowDrop(event) {
804
+ if (event.dataTransfer?.files.length) {
805
+ event.preventDefault();
806
+ this.dragActive = false;
807
+ void this.addFiles(event.dataTransfer.files);
808
+ this.changeDetector.markForCheck();
809
+ }
739
810
  }
740
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillStatusAgingMatrixComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
741
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillStatusAgingMatrixComponent, isStandalone: true, selector: "mindbill-status-aging-matrix", inputs: { bills: "bills", heading: "heading", description: "description", stateOrder: "stateOrder", showBalances: "showBalances", appearance: "appearance" }, outputs: { cellSelected: "cellSelected" }, ngImport: i0, template: `
742
- <section class="mbmx" [ngStyle]="themeStyle" aria-label="Bills by status and age">
743
- <header><h2>{{ heading }}</h2><p>{{ description }}</p></header>
744
- <div class="card">
745
- <table>
746
- <thead>
747
- <tr><th class="status">Status</th>@for (bucket of buckets; track bucket.id) { <th>{{ bucket.label }}</th> }<th>Total</th></tr>
748
- </thead>
749
- <tbody>
750
- @for (row of matrix.rows; track row.state) {
751
- <tr>
752
- <td class="status"><i class="pill">{{ row.label }}</i></td>
753
- @for (cell of row.cells; track cell.bucket) {
754
- <td><button type="button" [disabled]="cell.count === 0" (click)="cellSelected.emit(cell)"><b [class.zero]="cell.count === 0">{{ cell.count === 0 ? '—' : cell.count }}</b>@if (showBalances && cell.count > 0) { <small>{{ cell.balance | currency }}</small> }</button></td>
755
- }
756
- <td class="rowtotal"><button type="button" [disabled]="row.total.count === 0" (click)="cellSelected.emit(row.total)"><b>{{ row.total.count }}</b>@if (showBalances) { <small>{{ row.total.balance | currency }}</small> }</button></td>
757
- </tr>
758
- }
759
- <tr class="totals">
760
- <td class="status">Total</td>
761
- @for (cell of matrix.columnTotals; track cell.bucket) {
762
- <td><button type="button" [disabled]="cell.count === 0" (click)="cellSelected.emit(cell)"><b [class.zero]="cell.count === 0">{{ cell.count === 0 ? '—' : cell.count }}</b>@if (showBalances && cell.count > 0) { <small>{{ cell.balance | currency }}</small> }</button></td>
763
- }
764
- <td class="rowtotal"><button type="button" [disabled]="matrix.grandTotal.count === 0" (click)="cellSelected.emit(matrix.grandTotal)"><b>{{ matrix.grandTotal.count }}</b>@if (showBalances) { <small>{{ matrix.grandTotal.balance | currency }}</small> }</button></td>
765
- </tr>
766
- </tbody>
767
- </table>
768
- </div>
769
- </section>
770
- `, isInline: true, styles: [":host{display:block}.mbmx{font-family:var(--font);color:var(--t)}.mbmx header{margin-bottom:16px}.mbmx h2{margin:0;font-size:24px}.mbmx header p{margin:5px 0 0;color:var(--m)}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);overflow:auto}table{width:100%;border-collapse:collapse;min-width:640px}th{padding:13px 16px;border-bottom:1px solid var(--b);color:var(--m);font-size:11px;letter-spacing:.06em;text-transform:uppercase;text-align:right;white-space:nowrap}th.status{text-align:left}td{padding:0;border-bottom:1px solid var(--b);border-left:1px solid color-mix(in srgb,var(--b) 55%,transparent);text-align:right}td.status{border-left:0;padding:14px 16px;text-align:left;white-space:nowrap}tr:last-child td{border-bottom:0}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-style:normal;font-weight:750;padding:5px 9px}td button{display:block;width:100%;min-height:56px;padding:9px 16px;border:0;background:none;color:var(--t);font:inherit;text-align:right;cursor:pointer}td button:disabled{cursor:default}td button:not(:disabled):hover{background:color-mix(in srgb,var(--a) 8%,var(--s))}td button b{display:block;font-size:17px;font-weight:760;font-variant-numeric:tabular-nums}td button b.zero{color:var(--m);font-weight:500}td button small{display:block;margin-top:2px;color:var(--m);font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap}td.rowtotal,tr.totals td{background:color-mix(in srgb,var(--bg) 78%,var(--s));font-weight:750}tr.totals td.status{color:var(--t);font-weight:750}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
771
- }
772
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillStatusAgingMatrixComponent, decorators: [{
773
- type: Component,
774
- args: [{ selector: "mindbill-status-aging-matrix", standalone: true, imports: [CommonModule], template: `
775
- <section class="mbmx" [ngStyle]="themeStyle" aria-label="Bills by status and age">
776
- <header><h2>{{ heading }}</h2><p>{{ description }}</p></header>
777
- <div class="card">
778
- <table>
779
- <thead>
780
- <tr><th class="status">Status</th>@for (bucket of buckets; track bucket.id) { <th>{{ bucket.label }}</th> }<th>Total</th></tr>
781
- </thead>
782
- <tbody>
783
- @for (row of matrix.rows; track row.state) {
784
- <tr>
785
- <td class="status"><i class="pill">{{ row.label }}</i></td>
786
- @for (cell of row.cells; track cell.bucket) {
787
- <td><button type="button" [disabled]="cell.count === 0" (click)="cellSelected.emit(cell)"><b [class.zero]="cell.count === 0">{{ cell.count === 0 ? '—' : cell.count }}</b>@if (showBalances && cell.count > 0) { <small>{{ cell.balance | currency }}</small> }</button></td>
788
- }
789
- <td class="rowtotal"><button type="button" [disabled]="row.total.count === 0" (click)="cellSelected.emit(row.total)"><b>{{ row.total.count }}</b>@if (showBalances) { <small>{{ row.total.balance | currency }}</small> }</button></td>
790
- </tr>
791
- }
792
- <tr class="totals">
793
- <td class="status">Total</td>
794
- @for (cell of matrix.columnTotals; track cell.bucket) {
795
- <td><button type="button" [disabled]="cell.count === 0" (click)="cellSelected.emit(cell)"><b [class.zero]="cell.count === 0">{{ cell.count === 0 ? '—' : cell.count }}</b>@if (showBalances && cell.count > 0) { <small>{{ cell.balance | currency }}</small> }</button></td>
796
- }
797
- <td class="rowtotal"><button type="button" [disabled]="matrix.grandTotal.count === 0" (click)="cellSelected.emit(matrix.grandTotal)"><b>{{ matrix.grandTotal.count }}</b>@if (showBalances) { <small>{{ matrix.grandTotal.balance | currency }}</small> }</button></td>
798
- </tr>
799
- </tbody>
800
- </table>
801
- </div>
802
- </section>
803
- `, styles: [":host{display:block}.mbmx{font-family:var(--font);color:var(--t)}.mbmx header{margin-bottom:16px}.mbmx h2{margin:0;font-size:24px}.mbmx header p{margin:5px 0 0;color:var(--m)}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);overflow:auto}table{width:100%;border-collapse:collapse;min-width:640px}th{padding:13px 16px;border-bottom:1px solid var(--b);color:var(--m);font-size:11px;letter-spacing:.06em;text-transform:uppercase;text-align:right;white-space:nowrap}th.status{text-align:left}td{padding:0;border-bottom:1px solid var(--b);border-left:1px solid color-mix(in srgb,var(--b) 55%,transparent);text-align:right}td.status{border-left:0;padding:14px 16px;text-align:left;white-space:nowrap}tr:last-child td{border-bottom:0}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-style:normal;font-weight:750;padding:5px 9px}td button{display:block;width:100%;min-height:56px;padding:9px 16px;border:0;background:none;color:var(--t);font:inherit;text-align:right;cursor:pointer}td button:disabled{cursor:default}td button:not(:disabled):hover{background:color-mix(in srgb,var(--a) 8%,var(--s))}td button b{display:block;font-size:17px;font-weight:760;font-variant-numeric:tabular-nums}td button b.zero{color:var(--m);font-weight:500}td button small{display:block;margin-top:2px;color:var(--m);font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap}td.rowtotal,tr.totals td{background:color-mix(in srgb,var(--bg) 78%,var(--s));font-weight:750}tr.totals td.status{color:var(--t);font-weight:750}\n"] }]
804
- }], propDecorators: { bills: [{
805
- type: Input
806
- }], heading: [{
807
- type: Input
808
- }], description: [{
809
- type: Input
810
- }], stateOrder: [{
811
- type: Input
812
- }], showBalances: [{
813
- type: Input
814
- }], appearance: [{
815
- type: Input
816
- }], cellSelected: [{
817
- type: Output
818
- }] } });
819
-
820
- const MINDBILL_BILL_TASKS_TONES = {
821
- violet: "#7c53c3",
822
- red: "#cf4437",
823
- blue: "#2f7fd1",
824
- green: "#2c9a5b",
825
- amber: "#d9931f",
826
- neutral: "#8195a1",
827
- };
828
- const MINDBILL_BILL_TASKS_PILL_BASES = ["#2c9a5b", "#1f9aa8", "#dcbb28", "#e08a2e", "#d75d8a"];
829
- function mindBillBillTasksTone(tone) {
830
- return MINDBILL_BILL_TASKS_TONES[tone] ?? MINDBILL_BILL_TASKS_TONES.neutral;
831
- }
832
- function mindBillBillTasksPillBase(index) {
833
- return MINDBILL_BILL_TASKS_PILL_BASES[index % MINDBILL_BILL_TASKS_PILL_BASES.length];
834
- }
835
- /** Click-through payload for a bucket cell (bucketIndex) or a row total (null). */
836
- function mindBillBillTasksCell(section, row, bucketIndex, buckets) {
837
- if (bucketIndex === null) {
838
- return {
839
- sectionId: section.id,
840
- rowId: row.id,
841
- bucketId: null,
842
- refs: row.refs.flat(),
843
- count: row.total,
844
- };
811
+ async addFiles(fileList) {
812
+ const files = Array.from(fileList);
813
+ if (!files.length)
814
+ return;
815
+ this.errorMessage = "";
816
+ const invalid = files.find((file) => file.type !== "application/pdf" && !file.name.toLowerCase().endsWith(".pdf"));
817
+ if (invalid) {
818
+ this.errorMessage = `${invalid.name} is not a PDF.`;
819
+ return;
820
+ }
821
+ const oversized = files.find((file) => file.size > MAX_PDF_BYTES);
822
+ if (oversized) {
823
+ this.errorMessage = `${oversized.name} is larger than 25 MB.`;
824
+ return;
825
+ }
826
+ if (this.workingAttachments.length + files.length > MAX_DOCUMENTS) {
827
+ this.errorMessage = `A bill can include at most ${MAX_DOCUMENTS} attachments.`;
828
+ return;
829
+ }
830
+ const existingBytes = this.workingAttachments.reduce((sum, item) => sum + (item.contentBase64 ? item.contentBase64.length * 0.75 : 0), 0);
831
+ if (existingBytes + files.reduce((sum, file) => sum + file.size, 0) > MAX_UPLOAD_BYTES) {
832
+ this.errorMessage = "Attachments exceed the 100 MB upload limit.";
833
+ return;
834
+ }
835
+ for (const file of files)
836
+ this.workingAttachments.push({ filename: file.name, description: "Supporting document", documentType: "other", contentBase64: await blobToBase64(file), contentUrl: URL.createObjectURL(file) });
837
+ this.changeDetector.markForCheck();
845
838
  }
846
- return {
847
- sectionId: section.id,
848
- rowId: row.id,
849
- bucketId: buckets[bucketIndex]?.id ?? null,
850
- refs: row.refs[bucketIndex] ?? [],
851
- count: row.counts[bucketIndex] ?? 0,
852
- };
853
- }
854
-
855
- // daisyBill-style "Bill Tasks" dashboard: one rounded, tone-colored card per
856
- // task section, rows bucketed by age in days with colored bucket header pills,
857
- // per-cell click-through counts, and a closing grand-total card. The pure
858
- // aggregation lives in @mindbill/browser (buildBillTasksDashboard) so React
859
- // and Angular render the exact same data shape.
860
- class MindBillBillTasksDashboardComponent {
861
- data = null;
862
- buckets = BILL_TASKS_AGING_BUCKETS;
863
- heading = "";
864
- grandTotalLabel = "Bill Tasks Total";
865
- emptyLabel = "No Tasks";
866
- appearance = { preset: "mindbill" };
867
- cellSelected = new EventEmitter();
868
- get themeStyle() {
869
- return {
870
- ...mindBillAngularAppearanceStyle(this.appearance),
871
- "--cols": `minmax(150px,1.6fr) repeat(${this.buckets.length},minmax(84px,1fr)) minmax(84px,1fr)`,
872
- };
839
+ async filesSelected(event) {
840
+ const input = event.target;
841
+ await this.addFiles(input.files ?? []);
842
+ input.value = "";
873
843
  }
874
- tone(section) {
875
- return mindBillBillTasksTone(section.tone);
844
+ removeAttachment(index) { const item = this.workingAttachments[index]; if (item?.contentUrl?.startsWith("blob:"))
845
+ URL.revokeObjectURL(item.contentUrl); this.workingAttachments.splice(index, 1); }
846
+ async previewAttachment(attachment) {
847
+ if (attachment.loadBlob) {
848
+ const url = URL.createObjectURL(await attachment.loadBlob());
849
+ window.open(url, "_blank", "noopener,noreferrer");
850
+ setTimeout(() => URL.revokeObjectURL(url), 60_000);
851
+ }
852
+ else if (attachment.contentUrl)
853
+ window.open(attachment.contentUrl, "_blank", "noopener,noreferrer");
854
+ else if (attachment.contentBase64)
855
+ window.open(`data:application/pdf;base64,${attachment.contentBase64}`, "_blank", "noopener,noreferrer");
876
856
  }
877
- pillBackground(index) {
878
- return `color-mix(in srgb,${mindBillBillTasksPillBase(index)} 20%,var(--s))`;
857
+ bad(path) { return this.invalidFields.has(path) || this.attentionFieldSet.has(path); }
858
+ validate() {
859
+ const candidates = [
860
+ ["patient.firstName", this.bill.patient.firstName], ["patient.lastName", this.bill.patient.lastName], ["patient.dateOfBirth", parseMindBillSubmissionDate(this.bill.patient.dateOfBirth ?? "")],
861
+ ["patient.address.line1", this.bill.patient.address.line1], ["patient.address.city", this.bill.patient.address.city], ["patient.address.state", this.bill.patient.address.state], ["patient.address.postalCode", this.bill.patient.address.postalCode],
862
+ ["service.date", parseMindBillSubmissionDate(this.bill.service.date ?? "")], ["claim.dateOfInjury", parseMindBillSubmissionDate(this.bill.claim.dateOfInjury ?? "")], ["claim.claimNumber", this.bill.claim.claimNumber], ["claim.employer", this.bill.claim.employer],
863
+ ["claim.claimsAdministrator", this.bill.claim.claimsAdministrator.id], ["diagnoses", this.bill.diagnoses.length],
864
+ ["billingProvider.name", this.bill.billingProvider.name], ["billingProvider.taxId", this.bill.billingProvider.taxId], ["billingProvider.npi", this.bill.billingProvider.npi], ["billingProvider.phone", this.bill.billingProvider.phone],
865
+ ["billingProvider.address.line1", this.bill.billingProvider.address.line1], ["billingProvider.address.city", this.bill.billingProvider.address.city], ["billingProvider.address.state", this.bill.billingProvider.address.state], ["billingProvider.address.postalCode", this.bill.billingProvider.address.postalCode],
866
+ ["renderingProvider.name", this.bill.renderingProvider.name], ["renderingProvider.npi", this.bill.renderingProvider.npi], ["renderingProvider.taxonomy", this.bill.renderingProvider.taxonomy],
867
+ ["serviceLocation.placeOfServiceCode", this.bill.serviceLocation.placeOfServiceCode], ["serviceLocation.address.line1", this.bill.serviceLocation.address.line1], ["serviceLocation.address.city", this.bill.serviceLocation.address.city], ["serviceLocation.address.state", this.bill.serviceLocation.address.state], ["serviceLocation.address.postalCode", this.bill.serviceLocation.address.postalCode],
868
+ ["serviceLines", this.bill.serviceLines.some((line) => required(line.code) && (line.units ?? 1) > 0)],
869
+ ];
870
+ this.invalidFields = new Set(candidates.filter(([, value]) => !value || !required(value)).map(([path]) => path));
871
+ if (this.invalidFields.size) {
872
+ this.errorMessage = `Complete ${this.invalidFields.size} highlighted required field${this.invalidFields.size === 1 ? "" : "s"} before submitting.`;
873
+ setTimeout(() => document.querySelector(".mbs .invalid,.mbs .invalid-set")?.scrollIntoView({ behavior: "smooth", block: "center" }));
874
+ return false;
875
+ }
876
+ return true;
879
877
  }
880
- pillColor(index) {
881
- return `color-mix(in srgb,${mindBillBillTasksPillBase(index)} 62%,var(--t))`;
878
+ async submissionDocuments() {
879
+ const documents = [];
880
+ for (const attachment of this.workingAttachments) {
881
+ let contentBase64 = attachment.contentBase64;
882
+ if (!contentBase64 && attachment.loadBlob)
883
+ contentBase64 = await blobToBase64(await attachment.loadBlob());
884
+ if (!contentBase64 && attachment.contentUrl) {
885
+ const response = await fetch(attachment.contentUrl);
886
+ if (!response.ok)
887
+ throw new Error(`Could not load ${attachment.filename}.`);
888
+ contentBase64 = await blobToBase64(await response.blob());
889
+ }
890
+ if (!contentBase64)
891
+ continue;
892
+ documents.push({
893
+ filename: attachment.filename,
894
+ documentType: attachment.documentType,
895
+ contentBase64,
896
+ ...(attachment.externalId ? { externalId: attachment.externalId } : {}),
897
+ ...(attachment.description ? { description: attachment.description } : {}),
898
+ ...(attachment.reportTypeCode ? { reportTypeCode: attachment.reportTypeCode } : {}),
899
+ });
900
+ }
901
+ return documents;
882
902
  }
883
- select(section, row, bucketIndex) {
884
- this.cellSelected.emit(mindBillBillTasksCell(section, row, bucketIndex, this.buckets));
903
+ async submit(event) {
904
+ event.preventDefault();
905
+ this.errorMessage = "";
906
+ if (!this.validate())
907
+ return;
908
+ this.submitting = true;
909
+ try {
910
+ const bill = deepCopyBill(this.bill);
911
+ bill.patient.dateOfBirth = parseMindBillSubmissionDate(bill.patient.dateOfBirth ?? "") ?? bill.patient.dateOfBirth;
912
+ bill.service.date = parseMindBillSubmissionDate(bill.service.date ?? "") ?? bill.service.date;
913
+ bill.claim.dateOfInjury = parseMindBillSubmissionDate(bill.claim.dateOfInjury ?? "") ?? bill.claim.dateOfInjury;
914
+ bill.serviceLines = bill.serviceLines
915
+ .filter((line) => line.code.trim())
916
+ .map((line) => {
917
+ const charge = line.charge ?? this.allowed(line);
918
+ return charge == null ? line : { ...line, charge };
919
+ });
920
+ const input = { bill, documents: await this.submissionDocuments() };
921
+ const result = this.submitter
922
+ ? await this.submitter(input)
923
+ : await createBillSubmissionClient({ sessionEndpoint: this.sessionEndpoint, ...(this.getSession ? { getSession: this.getSession } : {}), ...(this.apiBaseUrl ? { apiBaseUrl: this.apiBaseUrl } : {}) }).submitBill(input);
924
+ this.submitted.emit(result);
925
+ }
926
+ catch (value) {
927
+ const error = value instanceof Error ? value : new Error("The bill could not be submitted.");
928
+ this.errorMessage = error.message;
929
+ this.submissionError.emit(error);
930
+ }
931
+ finally {
932
+ this.submitting = false;
933
+ this.changeDetector.markForCheck();
934
+ }
885
935
  }
886
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillTasksDashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
887
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillTasksDashboardComponent, isStandalone: true, selector: "mindbill-bill-tasks-dashboard", inputs: { data: "data", buckets: "buckets", heading: "heading", grandTotalLabel: "grandTotalLabel", emptyLabel: "emptyLabel", appearance: "appearance" }, outputs: { cellSelected: "cellSelected" }, ngImport: i0, template: `
888
- <div class="mbtk" [ngStyle]="themeStyle">
889
- @if (heading) { <header class="heading"><h2>{{ heading }}</h2></header> }
890
- @for (section of data?.sections ?? []; track section.id) {
891
- <section class="card" [style.borderColor]="tone(section)" [attr.aria-label]="section.label">
892
- <div class="grid head" [style.borderBottomColor]="tone(section)">
893
- <span class="title">
894
- <i class="dot" [style.background]="tone(section)"></i>
895
- <span class="title-text"><strong>{{ section.label }}</strong><span class="basis">by {{ section.agingBasisLabel }}</span></span>
896
- </span>
897
- @for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
898
- <span class="pill" [style.background]="pillBackground(bucketIndex)" [style.color]="pillColor(bucketIndex)">{{ bucket.label }}</span>
899
- }
900
- <span class="colhead">Task Total</span>
936
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillSubmissionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
937
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillSubmissionComponent, isStandalone: true, selector: "mindbill-bill-submission", inputs: { initialBill: "initialBill", attachments: "attachments", appearance: "appearance", sessionEndpoint: "sessionEndpoint", getSession: "getSession", apiBaseUrl: "apiBaseUrl", heading: "heading", description: "description", submitLabel: "submitLabel", attentionFields: "attentionFields", procedureOptions: "procedureOptions", modifierOptions: "modifierOptions", taxonomyOptions: "taxonomyOptions", submitter: "submitter" }, outputs: { submitted: "submitted", submissionError: "submissionError", billChange: "billChange" }, host: { listeners: { "window:dragover": "onWindowDragOver($event)", "window:dragleave": "onWindowDragLeave($event)", "window:drop": "onWindowDrop($event)" } }, usesOnChanges: true, ngImport: i0, template: `
938
+ <form class="mbs" [ngStyle]="themeStyle" (submit)="submit($event)" novalidate>
939
+ <div class="intro"><div><h2>{{ heading }}</h2><p>{{ description }}</p></div><span><b>*</b> Required</span></div>
940
+
941
+ <fieldset id="mbs-patient"><legend>Patient</legend><div class="grid">
942
+ <label [class.invalid]="bad('patient.firstName')">First name <b>*</b><input name="firstName" [(ngModel)]="bill.patient.firstName"></label>
943
+ <label [class.invalid]="bad('patient.lastName')">Last name <b>*</b><input name="lastName" [(ngModel)]="bill.patient.lastName"></label>
944
+ <label [class.invalid]="bad('patient.dateOfBirth')">Date of birth <b>*</b><mindbill-date-input ariaLabel="Date of birth" [value]="bill.patient.dateOfBirth" (valueChange)="setDate('patient.dateOfBirth', $event)"/></label>
945
+ <label>Phone (optional)<input name="phone" type="tel" [(ngModel)]="bill.patient.phone"></label>
946
+ <label class="wide" [class.invalid]="bad('patient.address.line1')">Address <b>*</b><input name="address" [(ngModel)]="bill.patient.address.line1"></label>
947
+ <label [class.invalid]="bad('patient.address.postalCode')">ZIP <b>*</b><input name="zip" [(ngModel)]="bill.patient.address.postalCode" (ngModelChange)="postalChanged($event)" inputmode="numeric">@if (postalStatus) { <small>{{ postalStatus }}</small> }</label>
948
+ <label [class.invalid]="bad('patient.address.city')">City <b>*</b><input name="city" [(ngModel)]="bill.patient.address.city"></label>
949
+ <label [class.invalid]="bad('patient.address.state')">State <b>*</b><input name="state" [(ngModel)]="bill.patient.address.state" maxlength="2"></label>
950
+ </div></fieldset>
951
+
952
+ <fieldset id="mbs-claim"><legend>Injury &amp; claim</legend><div class="grid">
953
+ <label [class.invalid]="bad('service.date')">Date of service <b>*</b><mindbill-date-input ariaLabel="Date of service" [value]="bill.service.date" (valueChange)="setDate('service.date', $event)"/></label>
954
+ <label [class.invalid]="bad('claim.dateOfInjury')">Date of injury <b>*</b><mindbill-date-input ariaLabel="Date of injury" [value]="bill.claim.dateOfInjury" (valueChange)="setDate('claim.dateOfInjury', $event)"/></label>
955
+ <label class="wide">Treatment authorization # (optional)<input name="authNumber" [ngModel]="bill.service.authorizationNumber || ''" (ngModelChange)="setAuthorizationNumber($event)" placeholder="Utilization-review authorization number"><small>Rides on the bill as CMS-1500 Box 23 / 837 REF*G1.</small></label>
956
+ <label [class.invalid]="bad('claim.claimNumber')">Claim number <b>*</b><input name="claimNumber" [(ngModel)]="bill.claim.claimNumber"></label>
957
+ <label [class.invalid]="bad('claim.employer')">Employer <b>*</b><input name="employer" [(ngModel)]="bill.claim.employer"></label>
958
+ <div class="wide lookup" [class.invalid]="bad('claim.claimsAdministrator')"><label>Claims administrator <b>*</b></label>
959
+ <input name="payerQuery" [(ngModel)]="payerQuery" (focus)="searchPayers()" (input)="searchPayers()" placeholder="Search the MindBill payer directory…" autocomplete="off">
960
+ @if (payerBusy) { <small>Searching directory…</small> }
961
+ @if (payerResults.length) { <div class="options">@for (payer of payerResults; track payer.id) { <button type="button" (click)="selectPayer(payer)"><strong>{{ payer.name }}</strong><span>{{ payer.hasElectronic ? 'Electronic routing' : 'Paper / fax' }}@if (payer.confidence) { · {{ payer.confidence }} match }</span></button> }</div> }
962
+ @if (bill.claim.claimsAdministrator.id) { <small class="selected">✓ Routed to {{ bill.claim.claimsAdministrator.name }}</small> }
963
+ </div>
964
+ <label class="wide">Injury description (optional)<input name="description" [(ngModel)]="bill.claim.description"></label>
965
+ <div class="wide diagnosis" [class.invalid]="bad('diagnoses')"><label>Diagnosis codes (ICD-10) <b>*</b></label>
966
+ <div class="quick">@for (item of quickDiagnoses; track item.code) { <button type="button" [class.on]="hasDiagnosis(item.code)" (click)="toggleDiagnosis(item)">{{ hasDiagnosis(item.code) ? '✓' : '+' }} {{ item.label }}</button> }</div>
967
+ @if (bill.diagnoses.length) { <div class="chips">@for (code of bill.diagnoses; track code) { <button type="button" (click)="removeDiagnosis(code)"><strong>{{ code }}</strong> ×</button> }</div> }
968
+ <input name="diagnosisQuery" [(ngModel)]="diagnosisQuery" (focus)="searchDiagnoses(true)" (input)="searchDiagnoses(true)" placeholder="Search ICD-10 codes…" autocomplete="off">
969
+ @if (diagnosisBusy && !diagnosisResults.length) { <small>Searching ICD-10 directory…</small> }
970
+ @if (diagnosisResults.length) { <div class="options diagnoses" (scroll)="diagnosisScrolled($event)">@for (item of diagnosisResults; track item.code) { <button type="button" (click)="addDiagnosis(item)"><strong>{{ item.code }}</strong><span>{{ item.description }}</span></button> }</div> }
971
+ </div>
972
+ </div></fieldset>
973
+
974
+ <fieldset id="mbs-provider"><legend>Providers &amp; place of service</legend><div class="grid">
975
+ <h3 class="wide">Billing provider</h3>
976
+ <label [class.invalid]="bad('billingProvider.name')">Billing provider name <b>*</b><input name="billingProvider" [(ngModel)]="bill.billingProvider.name"></label>
977
+ <label [class.invalid]="bad('billingProvider.taxId')">Tax ID <b>*</b><input name="taxId" [(ngModel)]="bill.billingProvider.taxId"></label>
978
+ <label [class.invalid]="bad('billingProvider.npi')">Billing NPI <b>*</b><input name="billingNpi" [(ngModel)]="bill.billingProvider.npi" inputmode="numeric" maxlength="10"></label>
979
+ <label [class.invalid]="bad('billingProvider.phone')">Billing phone <b>*</b><input name="billingPhone" type="tel" [(ngModel)]="bill.billingProvider.phone"></label>
980
+ <label class="wide" [class.invalid]="bad('billingProvider.address.line1')">Billing address <b>*</b><input name="billingAddress" [(ngModel)]="bill.billingProvider.address.line1"></label>
981
+ <label [class.invalid]="bad('billingProvider.address.postalCode')">Billing ZIP <b>*</b><input name="billingZip" [(ngModel)]="bill.billingProvider.address.postalCode" inputmode="numeric"></label>
982
+ <label [class.invalid]="bad('billingProvider.address.city')">Billing city <b>*</b><input name="billingCity" [(ngModel)]="bill.billingProvider.address.city"></label>
983
+ <label [class.invalid]="bad('billingProvider.address.state')">Billing state <b>*</b><input name="billingState" [(ngModel)]="bill.billingProvider.address.state" maxlength="2"></label>
984
+ <h3 class="wide">Rendering provider</h3>
985
+ <label [class.invalid]="bad('renderingProvider.name')">Rendering provider name <b>*</b><input name="renderingProvider" [(ngModel)]="bill.renderingProvider.name"></label>
986
+ <label [class.invalid]="bad('renderingProvider.npi')">Rendering provider NPI <b>*</b><input name="renderingNpi" [(ngModel)]="bill.renderingProvider.npi" inputmode="numeric" maxlength="10"></label>
987
+ <div class="wide" [class.invalid]="bad('renderingProvider.taxonomy')"><label>Rendering taxonomy <b>*</b></label>
988
+ <mindbill-combo-box ariaLabel="Rendering taxonomy" [value]="bill.renderingProvider.taxonomy || ''" [invalid]="bad('renderingProvider.taxonomy')" [preserveValueOnOpen]="true" placeholder="Search specialty name or taxonomy code…" [options]="taxonomyComboOptions" [createOption]="customTaxonomyOption" (selected)="selectTaxonomy($event)"/>
989
+ <small>Search by specialty name or 10-character taxonomy code.</small>
990
+ </div>
991
+ <h3 class="wide">Place of service</h3>
992
+ <label [class.invalid]="bad('serviceLocation.placeOfServiceCode')">Place of service code <b>*</b><input name="pos" [(ngModel)]="bill.serviceLocation.placeOfServiceCode" placeholder="11" inputmode="numeric" maxlength="2"></label>
993
+ <label class="wide" [class.invalid]="bad('serviceLocation.address.line1')">Service address <b>*</b><input name="serviceAddress" [(ngModel)]="bill.serviceLocation.address.line1"></label>
994
+ <label [class.invalid]="bad('serviceLocation.address.postalCode')">Service ZIP <b>*</b><input name="serviceZip" [(ngModel)]="bill.serviceLocation.address.postalCode" inputmode="numeric"></label>
995
+ <label [class.invalid]="bad('serviceLocation.address.city')">Service city <b>*</b><input name="serviceCity" [(ngModel)]="bill.serviceLocation.address.city"></label>
996
+ <label [class.invalid]="bad('serviceLocation.address.state')">Service state <b>*</b><input name="serviceState" [(ngModel)]="bill.serviceLocation.address.state" maxlength="2"></label>
997
+ </div></fieldset>
998
+
999
+ <fieldset id="mbs-lines" [class.invalid-set]="bad('serviceLines')"><legend>Service lines</legend>
1000
+ <div class="line-head"><span>Procedure code *</span><span>Modifiers</span><span>Dx</span><span>Units *</span><span>Allowed</span><span></span></div>
1001
+ @for (line of bill.serviceLines; track $index; let index = $index) { <div class="service-line">
1002
+ <div class="cell"><span class="mobile-label">Procedure</span>
1003
+ <mindbill-combo-box [ariaLabel]="'Procedure code ' + (index + 1)" [value]="line.code" placeholder="Search or enter code…" [options]="procedureComboOptions" [createOption]="customProcedureOption" (selected)="selectProcedure(index, $event)"/>
1004
+ @if (line.code) { <small>{{ procedureDescription(line.code) || 'Custom CPT, HCPCS, or medical-legal code' }}</small> }
901
1005
  </div>
902
- @if (section.empty) {
903
- <p class="empty">{{ emptyLabel }}</p>
904
- } @else {
905
- @for (row of section.rows; track row.id) {
906
- <div class="grid row">
907
- <span class="rowlabel">{{ row.label }}</span>
908
- @for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
909
- <span class="count">
910
- @if ((row.counts[bucketIndex] ?? 0) > 0) {
911
- <button type="button" class="link" (click)="select(section, row, bucketIndex)">{{ row.counts[bucketIndex] }}</button>
912
- } @else { <span class="zero">0</span> }
913
- </span>
1006
+ <div class="cell"><span class="mobile-label">Modifiers</span>
1007
+ @if (line.modifiers?.length) { <div class="chips">@for (modifier of line.modifiers; track modifier) { <button type="button" (click)="removeModifier(index, modifier)" [attr.aria-label]="'Remove modifier ' + modifier">−{{ modifier }} ×</button> }</div> }
1008
+ <mindbill-combo-box [ariaLabel]="'Modifiers ' + (index + 1)" value="" [placeholder]="line.modifiers?.length ? line.modifiers!.length + ' modifier' + (line.modifiers!.length === 1 ? '' : 's') : 'Add modifiers…'" [options]="modifierComboOptions(line)" (selected)="addModifier(index, $event)"/>
1009
+ </div>
1010
+ <div class="cell"><span class="mobile-label">Dx pointers</span>
1011
+ <div class="dx">
1012
+ @if (bill.diagnoses.length) {
1013
+ @for (code of bill.diagnoses; track code; let dxIndex = $index) {
1014
+ <button type="button" [class.on]="hasDiagnosisPointer(line, dxIndex + 1)" [title]="dxLetter(dxIndex + 1) + ' — ' + code" [attr.aria-pressed]="hasDiagnosisPointer(line, dxIndex + 1)" [attr.aria-label]="'Point line ' + (index + 1) + ' at diagnosis ' + code" (click)="toggleDiagnosisPointer(index, dxIndex + 1)">{{ dxLetter(dxIndex + 1) }}</button>
914
1015
  }
915
- <span class="total">
916
- @if (row.total > 0) {
917
- <button type="button" class="link" (click)="select(section, row, null)">{{ row.total }}</button>
918
- } @else { <span class="zero">0</span> }
919
- </span>
920
- </div>
921
- }
922
- <div class="grid row totals">
923
- <span class="rowlabel">Total</span>
924
- @for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
925
- <span class="count">{{ section.totals[bucketIndex] ?? 0 }}</span>
926
- }
927
- <span class="total">{{ section.total }}</span>
1016
+ } @else { <small>Add diagnoses above</small> }
928
1017
  </div>
929
- }
930
- </section>
931
- }
932
- <section class="card grand" [attr.aria-label]="grandTotalLabel">
933
- <div class="grid row totals">
934
- <span class="rowlabel grand-label">{{ grandTotalLabel }}</span>
935
- @for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
936
- <span class="count">{{ data?.grandTotals?.[bucketIndex] ?? 0 }}</span>
937
- }
938
- <span class="total">{{ data?.grandTotal ?? 0 }}</span>
939
- </div>
940
- </section>
941
- </div>
942
- `, isInline: true, styles: [":host{display:block}.mbtk{color:var(--t);font-family:var(--font);font-size:14px}.heading{margin-bottom:14px}.heading h2{margin:0;font-size:24px}.card{margin-bottom:16px;border:1.5px solid var(--b);border-radius:var(--r);background:var(--s);overflow:hidden}.grid{display:grid;grid-template-columns:var(--cols);gap:0 10px;align-items:center;padding:0 16px}.head{padding:12px 16px;border-bottom:1px solid var(--b)}.title{display:flex;align-items:center;gap:9px;min-width:0}.dot{flex:0 0 auto;width:11px;height:11px;border-radius:50%}.title-text{min-width:0}.title-text strong{display:block;font-size:15.5px;line-height:1.3}.basis{display:block;color:var(--m);font-size:12px;line-height:1.35}.pill{justify-self:stretch;padding:5px 6px;border-radius:999px;font-size:11px;font-weight:750;line-height:1.25;text-align:center;white-space:nowrap}.colhead{justify-self:stretch;color:var(--m);font-size:11px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;text-align:center;white-space:nowrap}.row{border-top:1px solid color-mix(in srgb,var(--b) 60%,transparent);min-height:44px}.row:first-of-type{border-top:0}.rowlabel{color:var(--m);font-size:13px;line-height:1.35;padding:9px 0;overflow-wrap:anywhere}.count,.total{text-align:center;font-variant-numeric:tabular-nums}.zero{color:var(--m)}.link{border:0;background:none;padding:6px 8px;color:var(--a);font:inherit;font-weight:750;font-variant-numeric:tabular-nums;cursor:pointer;border-radius:6px}.link:hover{text-decoration:underline;background:color-mix(in srgb,var(--a) 8%,transparent)}.totals{background:color-mix(in srgb,var(--bg) 78%,var(--s));border-top:1px solid var(--b);font-weight:760}.totals .rowlabel{color:var(--t);font-weight:760}.empty{margin:0;padding:22px 16px;color:var(--m);text-align:center}.grand{border-color:var(--b)}.grand .row{min-height:52px}.grand-label{font-size:14px;font-weight:800}@media(max-width:760px){.pill,.colhead,.count{display:none}.grid{grid-template-columns:minmax(0,1fr) auto}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] });
1018
+ </div>
1019
+ <label><span class="mobile-label">Units</span><input [name]="'units'+index" type="number" min="1" [(ngModel)]="line.units"></label>
1020
+ <strong>{{ allowed(line) == null ? '—' : (allowed(line) | currency) }}</strong>
1021
+ <button class="remove" type="button" (click)="removeLine(index)" aria-label="Remove service line">×</button>
1022
+ </div> }
1023
+ <div class="total"><span>Total</span><strong>{{ totalAllowed | currency }}</strong></div>
1024
+ </fieldset>
1025
+
1026
+ <fieldset><legend>Attachments</legend>
1027
+ @for (attachment of workingAttachments; track attachment.filename; let index = $index) { <div class="attachment" [class.locked]="attachment.locked"><div><strong>{{ attachment.filename }}</strong><span>{{ attachment.description || attachment.badge || 'Supporting document' }}</span></div><div class="attachment-actions"><button type="button" (click)="previewAttachment(attachment)">Preview</button>@if (!attachment.locked) { <button type="button" (click)="removeAttachment(index)" aria-label="Remove attachment">×</button> }</div></div> }
1028
+ <label class="dropzone" [class.drag-active]="dragActive"><input type="file" accept="application/pdf,.pdf" multiple (change)="filesSelected($event)"><strong>Drop additional PDF files here, or click to choose</strong><span>Add supporting documents by dropping them anywhere on this screen.</span></label>
1029
+ </fieldset>
1030
+
1031
+ @if (errorMessage) { <div class="error" role="alert">{{ errorMessage }}</div> }
1032
+ <div class="submit-row"><span>{{ invalidFields.size ? 'Complete the highlighted required fields.' : 'All required billing data is ready.' }}</span><button type="submit" [disabled]="submitting">{{ submitting ? 'Submitting…' : submitLabel }}</button></div>
1033
+ @if (dragActive) { <div class="drop-overlay" aria-hidden="true"><div><strong>Drop PDFs to attach</strong><span>Supporting documents are added to this submission.</span></div></div> }
1034
+ </form>
1035
+ `, isInline: true, styles: [":host{display:block}.mbs{--danger:#c83c3c;font-family:var(--font);color:var(--t)}*{box-sizing:border-box}.intro{display:flex;justify-content:space-between;align-items:start;margin:0 0 24px}.intro h2{margin:0;font-size:24px;font-weight:600}.intro p{margin:6px 0 0;color:var(--m)}.intro>span{color:var(--m);font-size:13px}.mbs b{color:inherit}.intro b,label>b,.lookup>label>b,.diagnosis>label>b,.cell>label>b{color:var(--danger)}fieldset{border:1px solid var(--b);border-radius:var(--r);margin:0 0 22px;padding:24px 26px 26px;background:var(--s)}legend{padding:0 10px;font-size:18px;font-weight:700}.grid{display:grid;grid-template-columns:1fr 1fr;gap:20px 28px}.wide{grid-column:1/-1}h3{margin:4px 0 -4px;padding-top:12px;border-top:1px solid var(--b);font-size:16px}h3:first-child{border:0;padding-top:0}label,.lookup,.diagnosis{display:grid;gap:8px;font-size:14px;font-weight:700}.grid>label{display:flex;flex-wrap:wrap;align-content:start;align-items:center;column-gap:4px;row-gap:8px}.grid>label>input,.grid>label>select,.grid>label>small,.grid>label>mindbill-date-input{flex:0 0 100%}.grid>div.wide{display:grid;gap:8px;font-size:14px;font-weight:700}input,select{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus,select:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}.invalid input,.invalid select,.invalid-set{border-color:var(--danger)!important;background:color-mix(in srgb,var(--danger) 4%,var(--s))}.invalid>label{color:var(--danger)}small{color:var(--m);font-weight:450}.selected{color:#238655}.options{position:relative;z-index:4;max-height:300px;overflow:auto;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);box-shadow:0 14px 35px #172b3730}.options button{display:grid;width:100%;gap:3px;border:0;border-bottom:1px solid var(--b);background:var(--s);padding:12px 14px;text-align:left;color:var(--t);cursor:pointer}.options button:hover{background:color-mix(in srgb,var(--a) 7%,var(--s))}.options span{color:var(--m);font-size:12px}.quick,.chips{display:flex;flex-wrap:wrap;gap:8px}.quick button,.chips button{border:1px solid var(--b);border-radius:999px;background:var(--s);padding:7px 11px;color:var(--t);font:inherit;font-size:13px;cursor:pointer}.quick .on{border-color:var(--a);color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.chips button{border-color:color-mix(in srgb,var(--a) 40%,var(--b));background:color-mix(in srgb,var(--a) 7%,var(--s));font-weight:700}.dx{display:flex;flex-wrap:wrap;gap:5px;padding-top:6px}.dx button{width:30px;height:30px;border:1px solid var(--b);border-radius:8px;background:var(--s);color:var(--m);font:inherit;font-size:13px;font-weight:750;cursor:pointer;padding:0}.dx button.on{border-color:var(--a);background:color-mix(in srgb,var(--a) 10%,var(--s));color:var(--a)}.line-head,.service-line{display:grid;grid-template-columns:1.5fr 1.35fr .8fr .5fr .7fr 36px;gap:14px;align-items:start}.line-head{padding:0 0 9px;color:var(--m);font-size:12px;font-weight:700}.service-line{border-top:1px solid var(--b);padding:12px 0}.service-line label,.service-line .cell{display:grid;gap:6px;font-size:14px;font-weight:700}.service-line strong{text-align:right;padding-top:12px}.remove{border:0;background:transparent;color:var(--m);font-size:25px;cursor:pointer}.mobile-label{display:none}.total{display:flex;justify-content:flex-end;gap:36px;border-top:1px solid var(--b);padding-top:14px;font-size:17px}.attachment{display:flex;align-items:center;justify-content:space-between;gap:16px;border:1px solid var(--b);border-radius:var(--cr);margin-bottom:10px;padding:14px 16px}.attachment.locked{border-color:#9fd6b4;background:#f3fcf6}.attachment>div:first-child{display:grid;gap:4px}.attachment span{color:var(--m);font-size:13px}.attachment-actions{display:flex;gap:8px}.attachment button{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:8px 12px;color:var(--t);font-weight:700;cursor:pointer}.dropzone{position:relative;display:grid;min-height:170px;place-content:center;gap:8px;border:2px dashed color-mix(in srgb,var(--m) 60%,transparent);border-radius:var(--cr);text-align:center;cursor:pointer;transition:border-color .15s,background .15s}.dropzone.drag-active{border-color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.dropzone input{position:absolute;inset:0;opacity:0;cursor:pointer}.dropzone span{color:var(--m);font-weight:450}.drop-overlay{position:fixed;inset:0;z-index:60;display:grid;place-content:center;background:color-mix(in srgb,var(--a) 14%,#ffffffd9);pointer-events:none}.drop-overlay div{display:grid;gap:6px;border:2px dashed var(--a);border-radius:var(--r);background:var(--s);padding:34px 44px;text-align:center;box-shadow:0 24px 60px #172b3740}.drop-overlay strong{font-size:19px}.drop-overlay span{color:var(--m)}.error{margin:0 0 14px;border-left:4px solid var(--danger);border-radius:var(--cr);background:color-mix(in srgb,var(--danger) 10%,var(--s));padding:14px;color:var(--danger)}.submit-row{position:sticky;bottom:0;z-index:3;display:flex;align-items:center;justify-content:space-between;border:1px solid var(--b);border-radius:var(--r);background:color-mix(in srgb,var(--s) 96%,transparent);box-shadow:0 -8px 24px #172b3718;padding:14px 18px}.submit-row>span{color:#43835f}.submit-row button{min-width:190px;border:0;border-radius:var(--cr);background:var(--a);color:var(--ac);padding:12px 24px;font:inherit;font-weight:800;cursor:pointer}.submit-row button:disabled{opacity:.6;cursor:wait}@media(max-width:760px){fieldset{padding:18px 16px}.grid{grid-template-columns:1fr}.wide{grid-column:auto}.intro{display:grid;gap:10px}.line-head{display:none}.service-line{grid-template-columns:1fr 1fr}.service-line>*{grid-column:1/-1}.service-line label:nth-child(3){grid-column:1}.service-line>strong{grid-column:2;grid-row:3;text-align:left}.service-line>.remove{grid-column:1/-1}.mobile-label{display:block;color:var(--m);font-size:12px}.attachment{align-items:flex-start}.attachment-actions{align-items:center}.submit-row>span{display:none}.submit-row button{width:100%}}\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.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { 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: "component", type: MindBillComboBoxComponent, selector: "mindbill-combo-box", inputs: ["value", "placeholder", "options", "disabled", "loading", "loadingMore", "invalid", "preserveValueOnOpen", "ariaLabel", "createOption"], outputs: ["opened", "queryChange", "endReached", "selected"] }, { kind: "component", type: MindBillDateInputComponent, selector: "mindbill-date-input", inputs: ["value", "disabled", "ariaLabel"], outputs: ["valueChange"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
943
1036
  }
944
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillTasksDashboardComponent, decorators: [{
1037
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillSubmissionComponent, decorators: [{
945
1038
  type: Component,
946
- args: [{ selector: "mindbill-bill-tasks-dashboard", standalone: true, imports: [CommonModule], template: `
947
- <div class="mbtk" [ngStyle]="themeStyle">
948
- @if (heading) { <header class="heading"><h2>{{ heading }}</h2></header> }
949
- @for (section of data?.sections ?? []; track section.id) {
950
- <section class="card" [style.borderColor]="tone(section)" [attr.aria-label]="section.label">
951
- <div class="grid head" [style.borderBottomColor]="tone(section)">
952
- <span class="title">
953
- <i class="dot" [style.background]="tone(section)"></i>
954
- <span class="title-text"><strong>{{ section.label }}</strong><span class="basis">by {{ section.agingBasisLabel }}</span></span>
955
- </span>
956
- @for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
957
- <span class="pill" [style.background]="pillBackground(bucketIndex)" [style.color]="pillColor(bucketIndex)">{{ bucket.label }}</span>
958
- }
959
- <span class="colhead">Task Total</span>
1039
+ args: [{ selector: "mindbill-bill-submission", standalone: true, imports: [CommonModule, FormsModule, MindBillComboBoxComponent, MindBillDateInputComponent], template: `
1040
+ <form class="mbs" [ngStyle]="themeStyle" (submit)="submit($event)" novalidate>
1041
+ <div class="intro"><div><h2>{{ heading }}</h2><p>{{ description }}</p></div><span><b>*</b> Required</span></div>
1042
+
1043
+ <fieldset id="mbs-patient"><legend>Patient</legend><div class="grid">
1044
+ <label [class.invalid]="bad('patient.firstName')">First name <b>*</b><input name="firstName" [(ngModel)]="bill.patient.firstName"></label>
1045
+ <label [class.invalid]="bad('patient.lastName')">Last name <b>*</b><input name="lastName" [(ngModel)]="bill.patient.lastName"></label>
1046
+ <label [class.invalid]="bad('patient.dateOfBirth')">Date of birth <b>*</b><mindbill-date-input ariaLabel="Date of birth" [value]="bill.patient.dateOfBirth" (valueChange)="setDate('patient.dateOfBirth', $event)"/></label>
1047
+ <label>Phone (optional)<input name="phone" type="tel" [(ngModel)]="bill.patient.phone"></label>
1048
+ <label class="wide" [class.invalid]="bad('patient.address.line1')">Address <b>*</b><input name="address" [(ngModel)]="bill.patient.address.line1"></label>
1049
+ <label [class.invalid]="bad('patient.address.postalCode')">ZIP <b>*</b><input name="zip" [(ngModel)]="bill.patient.address.postalCode" (ngModelChange)="postalChanged($event)" inputmode="numeric">@if (postalStatus) { <small>{{ postalStatus }}</small> }</label>
1050
+ <label [class.invalid]="bad('patient.address.city')">City <b>*</b><input name="city" [(ngModel)]="bill.patient.address.city"></label>
1051
+ <label [class.invalid]="bad('patient.address.state')">State <b>*</b><input name="state" [(ngModel)]="bill.patient.address.state" maxlength="2"></label>
1052
+ </div></fieldset>
1053
+
1054
+ <fieldset id="mbs-claim"><legend>Injury &amp; claim</legend><div class="grid">
1055
+ <label [class.invalid]="bad('service.date')">Date of service <b>*</b><mindbill-date-input ariaLabel="Date of service" [value]="bill.service.date" (valueChange)="setDate('service.date', $event)"/></label>
1056
+ <label [class.invalid]="bad('claim.dateOfInjury')">Date of injury <b>*</b><mindbill-date-input ariaLabel="Date of injury" [value]="bill.claim.dateOfInjury" (valueChange)="setDate('claim.dateOfInjury', $event)"/></label>
1057
+ <label class="wide">Treatment authorization # (optional)<input name="authNumber" [ngModel]="bill.service.authorizationNumber || ''" (ngModelChange)="setAuthorizationNumber($event)" placeholder="Utilization-review authorization number"><small>Rides on the bill as CMS-1500 Box 23 / 837 REF*G1.</small></label>
1058
+ <label [class.invalid]="bad('claim.claimNumber')">Claim number <b>*</b><input name="claimNumber" [(ngModel)]="bill.claim.claimNumber"></label>
1059
+ <label [class.invalid]="bad('claim.employer')">Employer <b>*</b><input name="employer" [(ngModel)]="bill.claim.employer"></label>
1060
+ <div class="wide lookup" [class.invalid]="bad('claim.claimsAdministrator')"><label>Claims administrator <b>*</b></label>
1061
+ <input name="payerQuery" [(ngModel)]="payerQuery" (focus)="searchPayers()" (input)="searchPayers()" placeholder="Search the MindBill payer directory…" autocomplete="off">
1062
+ @if (payerBusy) { <small>Searching directory…</small> }
1063
+ @if (payerResults.length) { <div class="options">@for (payer of payerResults; track payer.id) { <button type="button" (click)="selectPayer(payer)"><strong>{{ payer.name }}</strong><span>{{ payer.hasElectronic ? 'Electronic routing' : 'Paper / fax' }}@if (payer.confidence) { · {{ payer.confidence }} match }</span></button> }</div> }
1064
+ @if (bill.claim.claimsAdministrator.id) { <small class="selected">✓ Routed to {{ bill.claim.claimsAdministrator.name }}</small> }
1065
+ </div>
1066
+ <label class="wide">Injury description (optional)<input name="description" [(ngModel)]="bill.claim.description"></label>
1067
+ <div class="wide diagnosis" [class.invalid]="bad('diagnoses')"><label>Diagnosis codes (ICD-10) <b>*</b></label>
1068
+ <div class="quick">@for (item of quickDiagnoses; track item.code) { <button type="button" [class.on]="hasDiagnosis(item.code)" (click)="toggleDiagnosis(item)">{{ hasDiagnosis(item.code) ? '✓' : '+' }} {{ item.label }}</button> }</div>
1069
+ @if (bill.diagnoses.length) { <div class="chips">@for (code of bill.diagnoses; track code) { <button type="button" (click)="removeDiagnosis(code)"><strong>{{ code }}</strong> ×</button> }</div> }
1070
+ <input name="diagnosisQuery" [(ngModel)]="diagnosisQuery" (focus)="searchDiagnoses(true)" (input)="searchDiagnoses(true)" placeholder="Search ICD-10 codes…" autocomplete="off">
1071
+ @if (diagnosisBusy && !diagnosisResults.length) { <small>Searching ICD-10 directory…</small> }
1072
+ @if (diagnosisResults.length) { <div class="options diagnoses" (scroll)="diagnosisScrolled($event)">@for (item of diagnosisResults; track item.code) { <button type="button" (click)="addDiagnosis(item)"><strong>{{ item.code }}</strong><span>{{ item.description }}</span></button> }</div> }
1073
+ </div>
1074
+ </div></fieldset>
1075
+
1076
+ <fieldset id="mbs-provider"><legend>Providers &amp; place of service</legend><div class="grid">
1077
+ <h3 class="wide">Billing provider</h3>
1078
+ <label [class.invalid]="bad('billingProvider.name')">Billing provider name <b>*</b><input name="billingProvider" [(ngModel)]="bill.billingProvider.name"></label>
1079
+ <label [class.invalid]="bad('billingProvider.taxId')">Tax ID <b>*</b><input name="taxId" [(ngModel)]="bill.billingProvider.taxId"></label>
1080
+ <label [class.invalid]="bad('billingProvider.npi')">Billing NPI <b>*</b><input name="billingNpi" [(ngModel)]="bill.billingProvider.npi" inputmode="numeric" maxlength="10"></label>
1081
+ <label [class.invalid]="bad('billingProvider.phone')">Billing phone <b>*</b><input name="billingPhone" type="tel" [(ngModel)]="bill.billingProvider.phone"></label>
1082
+ <label class="wide" [class.invalid]="bad('billingProvider.address.line1')">Billing address <b>*</b><input name="billingAddress" [(ngModel)]="bill.billingProvider.address.line1"></label>
1083
+ <label [class.invalid]="bad('billingProvider.address.postalCode')">Billing ZIP <b>*</b><input name="billingZip" [(ngModel)]="bill.billingProvider.address.postalCode" inputmode="numeric"></label>
1084
+ <label [class.invalid]="bad('billingProvider.address.city')">Billing city <b>*</b><input name="billingCity" [(ngModel)]="bill.billingProvider.address.city"></label>
1085
+ <label [class.invalid]="bad('billingProvider.address.state')">Billing state <b>*</b><input name="billingState" [(ngModel)]="bill.billingProvider.address.state" maxlength="2"></label>
1086
+ <h3 class="wide">Rendering provider</h3>
1087
+ <label [class.invalid]="bad('renderingProvider.name')">Rendering provider name <b>*</b><input name="renderingProvider" [(ngModel)]="bill.renderingProvider.name"></label>
1088
+ <label [class.invalid]="bad('renderingProvider.npi')">Rendering provider NPI <b>*</b><input name="renderingNpi" [(ngModel)]="bill.renderingProvider.npi" inputmode="numeric" maxlength="10"></label>
1089
+ <div class="wide" [class.invalid]="bad('renderingProvider.taxonomy')"><label>Rendering taxonomy <b>*</b></label>
1090
+ <mindbill-combo-box ariaLabel="Rendering taxonomy" [value]="bill.renderingProvider.taxonomy || ''" [invalid]="bad('renderingProvider.taxonomy')" [preserveValueOnOpen]="true" placeholder="Search specialty name or taxonomy code…" [options]="taxonomyComboOptions" [createOption]="customTaxonomyOption" (selected)="selectTaxonomy($event)"/>
1091
+ <small>Search by specialty name or 10-character taxonomy code.</small>
1092
+ </div>
1093
+ <h3 class="wide">Place of service</h3>
1094
+ <label [class.invalid]="bad('serviceLocation.placeOfServiceCode')">Place of service code <b>*</b><input name="pos" [(ngModel)]="bill.serviceLocation.placeOfServiceCode" placeholder="11" inputmode="numeric" maxlength="2"></label>
1095
+ <label class="wide" [class.invalid]="bad('serviceLocation.address.line1')">Service address <b>*</b><input name="serviceAddress" [(ngModel)]="bill.serviceLocation.address.line1"></label>
1096
+ <label [class.invalid]="bad('serviceLocation.address.postalCode')">Service ZIP <b>*</b><input name="serviceZip" [(ngModel)]="bill.serviceLocation.address.postalCode" inputmode="numeric"></label>
1097
+ <label [class.invalid]="bad('serviceLocation.address.city')">Service city <b>*</b><input name="serviceCity" [(ngModel)]="bill.serviceLocation.address.city"></label>
1098
+ <label [class.invalid]="bad('serviceLocation.address.state')">Service state <b>*</b><input name="serviceState" [(ngModel)]="bill.serviceLocation.address.state" maxlength="2"></label>
1099
+ </div></fieldset>
1100
+
1101
+ <fieldset id="mbs-lines" [class.invalid-set]="bad('serviceLines')"><legend>Service lines</legend>
1102
+ <div class="line-head"><span>Procedure code *</span><span>Modifiers</span><span>Dx</span><span>Units *</span><span>Allowed</span><span></span></div>
1103
+ @for (line of bill.serviceLines; track $index; let index = $index) { <div class="service-line">
1104
+ <div class="cell"><span class="mobile-label">Procedure</span>
1105
+ <mindbill-combo-box [ariaLabel]="'Procedure code ' + (index + 1)" [value]="line.code" placeholder="Search or enter code…" [options]="procedureComboOptions" [createOption]="customProcedureOption" (selected)="selectProcedure(index, $event)"/>
1106
+ @if (line.code) { <small>{{ procedureDescription(line.code) || 'Custom CPT, HCPCS, or medical-legal code' }}</small> }
960
1107
  </div>
961
- @if (section.empty) {
962
- <p class="empty">{{ emptyLabel }}</p>
963
- } @else {
964
- @for (row of section.rows; track row.id) {
965
- <div class="grid row">
966
- <span class="rowlabel">{{ row.label }}</span>
967
- @for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
968
- <span class="count">
969
- @if ((row.counts[bucketIndex] ?? 0) > 0) {
970
- <button type="button" class="link" (click)="select(section, row, bucketIndex)">{{ row.counts[bucketIndex] }}</button>
971
- } @else { <span class="zero">0</span> }
972
- </span>
1108
+ <div class="cell"><span class="mobile-label">Modifiers</span>
1109
+ @if (line.modifiers?.length) { <div class="chips">@for (modifier of line.modifiers; track modifier) { <button type="button" (click)="removeModifier(index, modifier)" [attr.aria-label]="'Remove modifier ' + modifier">−{{ modifier }} ×</button> }</div> }
1110
+ <mindbill-combo-box [ariaLabel]="'Modifiers ' + (index + 1)" value="" [placeholder]="line.modifiers?.length ? line.modifiers!.length + ' modifier' + (line.modifiers!.length === 1 ? '' : 's') : 'Add modifiers…'" [options]="modifierComboOptions(line)" (selected)="addModifier(index, $event)"/>
1111
+ </div>
1112
+ <div class="cell"><span class="mobile-label">Dx pointers</span>
1113
+ <div class="dx">
1114
+ @if (bill.diagnoses.length) {
1115
+ @for (code of bill.diagnoses; track code; let dxIndex = $index) {
1116
+ <button type="button" [class.on]="hasDiagnosisPointer(line, dxIndex + 1)" [title]="dxLetter(dxIndex + 1) + ' — ' + code" [attr.aria-pressed]="hasDiagnosisPointer(line, dxIndex + 1)" [attr.aria-label]="'Point line ' + (index + 1) + ' at diagnosis ' + code" (click)="toggleDiagnosisPointer(index, dxIndex + 1)">{{ dxLetter(dxIndex + 1) }}</button>
973
1117
  }
974
- <span class="total">
975
- @if (row.total > 0) {
976
- <button type="button" class="link" (click)="select(section, row, null)">{{ row.total }}</button>
977
- } @else { <span class="zero">0</span> }
978
- </span>
979
- </div>
980
- }
981
- <div class="grid row totals">
982
- <span class="rowlabel">Total</span>
983
- @for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
984
- <span class="count">{{ section.totals[bucketIndex] ?? 0 }}</span>
985
- }
986
- <span class="total">{{ section.total }}</span>
1118
+ } @else { <small>Add diagnoses above</small> }
987
1119
  </div>
988
- }
989
- </section>
990
- }
991
- <section class="card grand" [attr.aria-label]="grandTotalLabel">
992
- <div class="grid row totals">
993
- <span class="rowlabel grand-label">{{ grandTotalLabel }}</span>
994
- @for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
995
- <span class="count">{{ data?.grandTotals?.[bucketIndex] ?? 0 }}</span>
996
- }
997
- <span class="total">{{ data?.grandTotal ?? 0 }}</span>
998
- </div>
999
- </section>
1000
- </div>
1001
- `, styles: [":host{display:block}.mbtk{color:var(--t);font-family:var(--font);font-size:14px}.heading{margin-bottom:14px}.heading h2{margin:0;font-size:24px}.card{margin-bottom:16px;border:1.5px solid var(--b);border-radius:var(--r);background:var(--s);overflow:hidden}.grid{display:grid;grid-template-columns:var(--cols);gap:0 10px;align-items:center;padding:0 16px}.head{padding:12px 16px;border-bottom:1px solid var(--b)}.title{display:flex;align-items:center;gap:9px;min-width:0}.dot{flex:0 0 auto;width:11px;height:11px;border-radius:50%}.title-text{min-width:0}.title-text strong{display:block;font-size:15.5px;line-height:1.3}.basis{display:block;color:var(--m);font-size:12px;line-height:1.35}.pill{justify-self:stretch;padding:5px 6px;border-radius:999px;font-size:11px;font-weight:750;line-height:1.25;text-align:center;white-space:nowrap}.colhead{justify-self:stretch;color:var(--m);font-size:11px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;text-align:center;white-space:nowrap}.row{border-top:1px solid color-mix(in srgb,var(--b) 60%,transparent);min-height:44px}.row:first-of-type{border-top:0}.rowlabel{color:var(--m);font-size:13px;line-height:1.35;padding:9px 0;overflow-wrap:anywhere}.count,.total{text-align:center;font-variant-numeric:tabular-nums}.zero{color:var(--m)}.link{border:0;background:none;padding:6px 8px;color:var(--a);font:inherit;font-weight:750;font-variant-numeric:tabular-nums;cursor:pointer;border-radius:6px}.link:hover{text-decoration:underline;background:color-mix(in srgb,var(--a) 8%,transparent)}.totals{background:color-mix(in srgb,var(--bg) 78%,var(--s));border-top:1px solid var(--b);font-weight:760}.totals .rowlabel{color:var(--t);font-weight:760}.empty{margin:0;padding:22px 16px;color:var(--m);text-align:center}.grand{border-color:var(--b)}.grand .row{min-height:52px}.grand-label{font-size:14px;font-weight:800}@media(max-width:760px){.pill,.colhead,.count{display:none}.grid{grid-template-columns:minmax(0,1fr) auto}}\n"] }]
1002
- }], propDecorators: { data: [{
1120
+ </div>
1121
+ <label><span class="mobile-label">Units</span><input [name]="'units'+index" type="number" min="1" [(ngModel)]="line.units"></label>
1122
+ <strong>{{ allowed(line) == null ? '—' : (allowed(line) | currency) }}</strong>
1123
+ <button class="remove" type="button" (click)="removeLine(index)" aria-label="Remove service line">×</button>
1124
+ </div> }
1125
+ <div class="total"><span>Total</span><strong>{{ totalAllowed | currency }}</strong></div>
1126
+ </fieldset>
1127
+
1128
+ <fieldset><legend>Attachments</legend>
1129
+ @for (attachment of workingAttachments; track attachment.filename; let index = $index) { <div class="attachment" [class.locked]="attachment.locked"><div><strong>{{ attachment.filename }}</strong><span>{{ attachment.description || attachment.badge || 'Supporting document' }}</span></div><div class="attachment-actions"><button type="button" (click)="previewAttachment(attachment)">Preview</button>@if (!attachment.locked) { <button type="button" (click)="removeAttachment(index)" aria-label="Remove attachment">×</button> }</div></div> }
1130
+ <label class="dropzone" [class.drag-active]="dragActive"><input type="file" accept="application/pdf,.pdf" multiple (change)="filesSelected($event)"><strong>Drop additional PDF files here, or click to choose</strong><span>Add supporting documents by dropping them anywhere on this screen.</span></label>
1131
+ </fieldset>
1132
+
1133
+ @if (errorMessage) { <div class="error" role="alert">{{ errorMessage }}</div> }
1134
+ <div class="submit-row"><span>{{ invalidFields.size ? 'Complete the highlighted required fields.' : 'All required billing data is ready.' }}</span><button type="submit" [disabled]="submitting">{{ submitting ? 'Submitting…' : submitLabel }}</button></div>
1135
+ @if (dragActive) { <div class="drop-overlay" aria-hidden="true"><div><strong>Drop PDFs to attach</strong><span>Supporting documents are added to this submission.</span></div></div> }
1136
+ </form>
1137
+ `, styles: [":host{display:block}.mbs{--danger:#c83c3c;font-family:var(--font);color:var(--t)}*{box-sizing:border-box}.intro{display:flex;justify-content:space-between;align-items:start;margin:0 0 24px}.intro h2{margin:0;font-size:24px;font-weight:600}.intro p{margin:6px 0 0;color:var(--m)}.intro>span{color:var(--m);font-size:13px}.mbs b{color:inherit}.intro b,label>b,.lookup>label>b,.diagnosis>label>b,.cell>label>b{color:var(--danger)}fieldset{border:1px solid var(--b);border-radius:var(--r);margin:0 0 22px;padding:24px 26px 26px;background:var(--s)}legend{padding:0 10px;font-size:18px;font-weight:700}.grid{display:grid;grid-template-columns:1fr 1fr;gap:20px 28px}.wide{grid-column:1/-1}h3{margin:4px 0 -4px;padding-top:12px;border-top:1px solid var(--b);font-size:16px}h3:first-child{border:0;padding-top:0}label,.lookup,.diagnosis{display:grid;gap:8px;font-size:14px;font-weight:700}.grid>label{display:flex;flex-wrap:wrap;align-content:start;align-items:center;column-gap:4px;row-gap:8px}.grid>label>input,.grid>label>select,.grid>label>small,.grid>label>mindbill-date-input{flex:0 0 100%}.grid>div.wide{display:grid;gap:8px;font-size:14px;font-weight:700}input,select{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus,select:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}.invalid input,.invalid select,.invalid-set{border-color:var(--danger)!important;background:color-mix(in srgb,var(--danger) 4%,var(--s))}.invalid>label{color:var(--danger)}small{color:var(--m);font-weight:450}.selected{color:#238655}.options{position:relative;z-index:4;max-height:300px;overflow:auto;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);box-shadow:0 14px 35px #172b3730}.options button{display:grid;width:100%;gap:3px;border:0;border-bottom:1px solid var(--b);background:var(--s);padding:12px 14px;text-align:left;color:var(--t);cursor:pointer}.options button:hover{background:color-mix(in srgb,var(--a) 7%,var(--s))}.options span{color:var(--m);font-size:12px}.quick,.chips{display:flex;flex-wrap:wrap;gap:8px}.quick button,.chips button{border:1px solid var(--b);border-radius:999px;background:var(--s);padding:7px 11px;color:var(--t);font:inherit;font-size:13px;cursor:pointer}.quick .on{border-color:var(--a);color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.chips button{border-color:color-mix(in srgb,var(--a) 40%,var(--b));background:color-mix(in srgb,var(--a) 7%,var(--s));font-weight:700}.dx{display:flex;flex-wrap:wrap;gap:5px;padding-top:6px}.dx button{width:30px;height:30px;border:1px solid var(--b);border-radius:8px;background:var(--s);color:var(--m);font:inherit;font-size:13px;font-weight:750;cursor:pointer;padding:0}.dx button.on{border-color:var(--a);background:color-mix(in srgb,var(--a) 10%,var(--s));color:var(--a)}.line-head,.service-line{display:grid;grid-template-columns:1.5fr 1.35fr .8fr .5fr .7fr 36px;gap:14px;align-items:start}.line-head{padding:0 0 9px;color:var(--m);font-size:12px;font-weight:700}.service-line{border-top:1px solid var(--b);padding:12px 0}.service-line label,.service-line .cell{display:grid;gap:6px;font-size:14px;font-weight:700}.service-line strong{text-align:right;padding-top:12px}.remove{border:0;background:transparent;color:var(--m);font-size:25px;cursor:pointer}.mobile-label{display:none}.total{display:flex;justify-content:flex-end;gap:36px;border-top:1px solid var(--b);padding-top:14px;font-size:17px}.attachment{display:flex;align-items:center;justify-content:space-between;gap:16px;border:1px solid var(--b);border-radius:var(--cr);margin-bottom:10px;padding:14px 16px}.attachment.locked{border-color:#9fd6b4;background:#f3fcf6}.attachment>div:first-child{display:grid;gap:4px}.attachment span{color:var(--m);font-size:13px}.attachment-actions{display:flex;gap:8px}.attachment button{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:8px 12px;color:var(--t);font-weight:700;cursor:pointer}.dropzone{position:relative;display:grid;min-height:170px;place-content:center;gap:8px;border:2px dashed color-mix(in srgb,var(--m) 60%,transparent);border-radius:var(--cr);text-align:center;cursor:pointer;transition:border-color .15s,background .15s}.dropzone.drag-active{border-color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.dropzone input{position:absolute;inset:0;opacity:0;cursor:pointer}.dropzone span{color:var(--m);font-weight:450}.drop-overlay{position:fixed;inset:0;z-index:60;display:grid;place-content:center;background:color-mix(in srgb,var(--a) 14%,#ffffffd9);pointer-events:none}.drop-overlay div{display:grid;gap:6px;border:2px dashed var(--a);border-radius:var(--r);background:var(--s);padding:34px 44px;text-align:center;box-shadow:0 24px 60px #172b3740}.drop-overlay strong{font-size:19px}.drop-overlay span{color:var(--m)}.error{margin:0 0 14px;border-left:4px solid var(--danger);border-radius:var(--cr);background:color-mix(in srgb,var(--danger) 10%,var(--s));padding:14px;color:var(--danger)}.submit-row{position:sticky;bottom:0;z-index:3;display:flex;align-items:center;justify-content:space-between;border:1px solid var(--b);border-radius:var(--r);background:color-mix(in srgb,var(--s) 96%,transparent);box-shadow:0 -8px 24px #172b3718;padding:14px 18px}.submit-row>span{color:#43835f}.submit-row button{min-width:190px;border:0;border-radius:var(--cr);background:var(--a);color:var(--ac);padding:12px 24px;font:inherit;font-weight:800;cursor:pointer}.submit-row button:disabled{opacity:.6;cursor:wait}@media(max-width:760px){fieldset{padding:18px 16px}.grid{grid-template-columns:1fr}.wide{grid-column:auto}.intro{display:grid;gap:10px}.line-head{display:none}.service-line{grid-template-columns:1fr 1fr}.service-line>*{grid-column:1/-1}.service-line label:nth-child(3){grid-column:1}.service-line>strong{grid-column:2;grid-row:3;text-align:left}.service-line>.remove{grid-column:1/-1}.mobile-label{display:block;color:var(--m);font-size:12px}.attachment{align-items:flex-start}.attachment-actions{align-items:center}.submit-row>span{display:none}.submit-row button{width:100%}}\n"] }]
1138
+ }], propDecorators: { initialBill: [{
1003
1139
  type: Input
1004
- }], buckets: [{
1140
+ }], attachments: [{
1141
+ type: Input
1142
+ }], appearance: [{
1143
+ type: Input
1144
+ }], sessionEndpoint: [{
1145
+ type: Input
1146
+ }], getSession: [{
1147
+ type: Input
1148
+ }], apiBaseUrl: [{
1005
1149
  type: Input
1006
1150
  }], heading: [{
1007
1151
  type: Input
1008
- }], grandTotalLabel: [{
1152
+ }], description: [{
1009
1153
  type: Input
1010
- }], emptyLabel: [{
1154
+ }], submitLabel: [{
1011
1155
  type: Input
1012
- }], appearance: [{
1156
+ }], attentionFields: [{
1013
1157
  type: Input
1014
- }], cellSelected: [{
1158
+ }], procedureOptions: [{
1159
+ type: Input
1160
+ }], modifierOptions: [{
1161
+ type: Input
1162
+ }], taxonomyOptions: [{
1163
+ type: Input
1164
+ }], submitter: [{
1165
+ type: Input
1166
+ }], submitted: [{
1167
+ type: Output
1168
+ }], submissionError: [{
1015
1169
  type: Output
1170
+ }], billChange: [{
1171
+ type: Output
1172
+ }], onWindowDragOver: [{
1173
+ type: HostListener,
1174
+ args: ["window:dragover", ["$event"]]
1175
+ }], onWindowDragLeave: [{
1176
+ type: HostListener,
1177
+ args: ["window:dragleave", ["$event"]]
1178
+ }], onWindowDrop: [{
1179
+ type: HostListener,
1180
+ args: ["window:drop", ["$event"]]
1016
1181
  }] } });
1017
1182
 
1018
- /** Date helpers shared by the Angular submission surfaces. Mirrors the React
1019
- * package's parseBillSubmissionDate/formatBillSubmissionDate exactly so both
1020
- * frameworks accept and render the same values. */
1021
- function parseMindBillSubmissionDate(value) {
1022
- const trimmed = value.trim();
1023
- const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
1024
- const us = /^(\d{1,2})[/-](\d{1,2})[/-](\d{4})$/.exec(trimmed);
1025
- const digits = /^(\d{2})(\d{2})(\d{4})$/.exec(trimmed.replace(/\D/g, ""));
1026
- const match = iso ? [iso[2], iso[3], iso[1]] : us ? [us[1], us[2], us[3]] : digits ? [digits[1], digits[2], digits[3]] : null;
1027
- if (!match)
1028
- return undefined;
1029
- const month = Number(match[0]);
1030
- const day = Number(match[1]);
1031
- const year = Number(match[2]);
1032
- const date = new Date(Date.UTC(year, month - 1, day));
1033
- if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day)
1034
- return undefined;
1035
- return `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
1036
- }
1037
- function formatMindBillSubmissionDate(value) {
1038
- const parsed = value ? parseMindBillSubmissionDate(value) : undefined;
1039
- if (!parsed)
1040
- return "";
1041
- const [year, month, day] = parsed.split("-");
1042
- return `${month}/${day}/${year}`;
1043
- }
1044
- /** Accepts complete CPT (5 digits), HCPCS (letter + 4 digits), or California
1045
- * medical-legal codes typed directly into the procedure picker. */
1046
- function mindBillCustomProcedureOption(query) {
1047
- const code = query.trim().toUpperCase().replace(/\s+/g, "");
1048
- if (!/^(?:\d{5}|[A-Z]\d{4}|ML(?:20[0-5]|PRR))$/.test(code))
1049
- return null;
1050
- return { id: code, label: code, detail: "Use this CPT, HCPCS, or medical-legal code" };
1051
- }
1052
-
1053
- /** Styled searchable dropdown shared by the submission form's procedure,
1054
- * modifier, taxonomy, payer, and diagnosis pickers. Mirrors the React
1055
- * package's ComboBox: query filtering, optional custom-code creation,
1056
- * loading states, and infinite scroll via (endReached). */
1057
- class MindBillComboBoxComponent {
1058
- value = "";
1059
- placeholder = "";
1060
- options = [];
1061
- disabled = false;
1062
- loading = false;
1063
- loadingMore = false;
1064
- invalid = false;
1065
- preserveValueOnOpen = false;
1066
- ariaLabel = "";
1067
- createOption;
1068
- opened = new EventEmitter();
1069
- queryChange = new EventEmitter();
1070
- endReached = new EventEmitter();
1071
- selected = new EventEmitter();
1072
- open = false;
1073
- query = "";
1074
- closeTimer;
1075
- get visible() {
1076
- const q = this.query.trim().toLowerCase();
1077
- const matches = q
1078
- ? this.options.filter((option) => `${option.id} ${option.label} ${option.detail ?? ""}`.toLowerCase().includes(q))
1079
- : this.options;
1080
- const custom = this.createOption?.(this.query) ?? null;
1081
- return custom && !matches.some((option) => option.id.toUpperCase() === custom.id.toUpperCase())
1082
- ? [...matches, custom]
1083
- : matches;
1183
+ class MindBillBillLifecycleComponent {
1184
+ billId = "";
1185
+ sessionEndpoint = "/api/mindbill/session";
1186
+ apiBaseUrl = "https://app.mindbill.org";
1187
+ getSession;
1188
+ refreshInterval = 60_000;
1189
+ appearance = { preset: "mindbill" };
1190
+ billingError = new EventEmitter();
1191
+ store = new MindBillLifecycleStore();
1192
+ lifecycleStages = [
1193
+ { id: "submitted", label: "Submitted" }, { id: "accepted", label: "Accepted" },
1194
+ { id: "response", label: "Payer response" }, { id: "follow_up", label: "Follow-up" },
1195
+ { id: "closed", label: "Closed" },
1196
+ ];
1197
+ panel = "";
1198
+ notice = "";
1199
+ closeReason = "";
1200
+ payment = { amount: 0, method: "check", depositDate: new Date().toISOString().slice(0, 10) };
1201
+ review = { reason: "", payerClaimControlNumber: "", disputedAmount: 0 };
1202
+ reportStatusOptions = REPORT_BILL_STATUS_OPTIONS;
1203
+ duplicateDelivery = null;
1204
+ duplicateDeliveryError = "";
1205
+ duplicate = { route: "ebill", faxNumber: "", email: "", mailingAddress: "" };
1206
+ report = { status: null, company: "", representativeName: "", representativeRole: "", phone: "", callReference: "", note: "" };
1207
+ reportContactSummary = "";
1208
+ correctionInitialBill = null;
1209
+ correctionAttachments = [];
1210
+ correctionAttentionFields = [];
1211
+ correctionReason = "";
1212
+ constructor() { effect(() => { const error = this.store.error(); if (error)
1213
+ this.billingError.emit(error); }); }
1214
+ ngOnChanges(changes) {
1215
+ if (changes["billId"] || changes["sessionEndpoint"] || changes["apiBaseUrl"] || changes["getSession"]) {
1216
+ if (!this.billId.trim()) {
1217
+ this.store.disconnect();
1218
+ return;
1219
+ }
1220
+ this.store.connect({ billId: this.billId, sessionEndpoint: this.sessionEndpoint, apiBaseUrl: this.apiBaseUrl, ...(this.getSession ? { getSession: this.getSession } : {}) }, this.refreshInterval);
1221
+ }
1084
1222
  }
1085
- get showMenu() {
1086
- return this.open && (this.loading || this.visible.length > 0 || this.query.trim().length >= 2);
1223
+ ngOnDestroy() { this.store.disconnect(); }
1224
+ get themeStyle() {
1225
+ return mindBillAngularAppearanceStyle(this.appearance);
1087
1226
  }
1088
- onFocus() {
1089
- clearTimeout(this.closeTimer);
1090
- const next = this.preserveValueOnOpen ? this.value : "";
1091
- this.open = true;
1092
- this.query = next;
1093
- this.opened.emit();
1094
- if (next)
1095
- this.queryChange.emit(next);
1227
+ stateLabel(data) { return data.lifecycle.state.replace(/_/g, " ").replace(/\b\w/g, (value) => value.toUpperCase()); }
1228
+ lifecycleDetail(data) { return data.lifecycle.submittedAt ? `Submitted ${new Date(data.lifecycle.submittedAt).toLocaleDateString()}${data.lifecycle.agingDays != null ? ` · ${data.lifecycle.agingDays} days old` : ""}` : "Submitted bill"; }
1229
+ currentStageIndex(data) { const value = data.lifecycle.state.toLowerCase(); if (value.includes("closed") || value.includes("written_off"))
1230
+ return 4; if (value.includes("second_review") || value.includes("ibr") || value.includes("lien") || value.includes("appeal"))
1231
+ return 3; if (value.includes("processed") || value.includes("paid") || value.includes("denied") || value.includes("rejected") || value.includes("response"))
1232
+ return 2; if (value.includes("accepted"))
1233
+ return 1; return 0; }
1234
+ activityLabel(type) { return type.replace(/[._-]+/g, " ").replace(/\b\w/g, (value) => value.toUpperCase()); }
1235
+ historyExpanded = new Set();
1236
+ toggleHistory(id) { if (this.historyExpanded.has(id))
1237
+ this.historyExpanded.delete(id);
1238
+ else
1239
+ this.historyExpanded.add(id); }
1240
+ isHistoryOpen(id) { return this.historyExpanded.has(id); }
1241
+ historyDate(iso) { const parsed = new Date(iso); return Number.isNaN(parsed.getTime()) ? iso : parsed.toLocaleDateString("en-US", { month: "2-digit", day: "2-digit", year: "numeric" }); }
1242
+ payerContact(data) { const contacts = data.delivery.contacts; return contacts.adjusterName || contacts.adjusterPhone || contacts.adjusterEmail || contacts.claimsEmail || contacts.faxNumber || contacts.mailingAddress || "No payer contact details are available."; }
1243
+ beginAction(action, data) {
1244
+ if (action === "resubmit")
1245
+ this.prepareCorrection(data);
1246
+ else if (action === "post_payment")
1247
+ this.panel = "payment";
1248
+ else if (action === "second_review" || action === "independent_bill_review")
1249
+ this.panel = "review";
1250
+ else if (action === "close")
1251
+ this.panel = "close";
1252
+ else if (action === "send_duplicate")
1253
+ this.beginSendDuplicate();
1254
+ else if (action === "report_bill_status")
1255
+ this.beginReportStatus(data);
1256
+ else if (action === "view_eor" && data.eors[0])
1257
+ void this.openEor(data.eors[0].id, data.eors[0].filename);
1258
+ }
1259
+ beginSendDuplicate() { this.panel = "duplicate"; this.loadDuplicateDelivery(); }
1260
+ loadDuplicateDelivery() {
1261
+ this.duplicateDeliveryError = "";
1262
+ if (this.duplicateDelivery)
1263
+ return;
1264
+ this.store.getDeliveryOptions().then((delivery) => {
1265
+ this.duplicateDelivery = delivery;
1266
+ this.duplicate.route = delivery.recommended.route;
1267
+ this.duplicate.faxNumber = delivery.contacts.faxNumber || "";
1268
+ this.duplicate.email = delivery.contacts.claimsEmail || "";
1269
+ this.duplicate.mailingAddress = delivery.contacts.mailingAddress || "";
1270
+ }).catch((cause) => {
1271
+ this.duplicateDeliveryError = cause instanceof Error ? cause.message : "Delivery options could not be loaded.";
1272
+ });
1096
1273
  }
1097
- onInput(event) {
1098
- this.open = true;
1099
- this.query = event.target.value;
1100
- this.queryChange.emit(this.query);
1274
+ duplicateRoutes() {
1275
+ const seen = new Set();
1276
+ return (this.duplicateDelivery?.options ?? []).filter((option) => {
1277
+ if (seen.has(option.route))
1278
+ return false;
1279
+ seen.add(option.route);
1280
+ return true;
1281
+ });
1101
1282
  }
1102
- onBlur() {
1103
- this.closeTimer = setTimeout(() => { this.open = false; }, 120);
1283
+ duplicateReady() {
1284
+ if (this.duplicate.route === "fax")
1285
+ return this.duplicate.faxNumber.replace(/\D/g, "").length >= 10;
1286
+ if (this.duplicate.route === "email")
1287
+ return this.duplicate.email.includes("@");
1288
+ if (this.duplicate.route === "mail")
1289
+ return this.duplicate.mailingAddress.trim().length > 2;
1290
+ return true;
1104
1291
  }
1105
- onScroll(event) {
1106
- const menu = event.currentTarget;
1107
- if (menu.scrollHeight - menu.scrollTop - menu.clientHeight < 120)
1108
- this.endReached.emit();
1292
+ async sendDuplicate() {
1293
+ if (!this.duplicateDelivery || !this.duplicateReady())
1294
+ return;
1295
+ const route = this.duplicate.route;
1296
+ const destination = route === "fax" ? { faxNumber: this.duplicate.faxNumber.trim() }
1297
+ : route === "email" ? { email: this.duplicate.email.trim() }
1298
+ : route === "mail" ? { mailingAddress: this.duplicate.mailingAddress.trim() }
1299
+ : null;
1300
+ await this.store.sendDuplicateBill({ route, ...(destination ? { destination } : {}) });
1301
+ this.panel = "";
1302
+ this.notice = "Duplicate bill sent.";
1303
+ }
1304
+ beginReportStatus(data) {
1305
+ const contacts = reportBillStatusContacts(data.delivery);
1306
+ const parts = [];
1307
+ if (contacts.claimsAdmin) {
1308
+ const phones = contacts.claimsAdmin.phones.map((phone) => `${phone.label} ${phone.value}`).join(", ");
1309
+ parts.push(`${contacts.claimsAdmin.name}${phones ? ` (${phones})` : ""}${contacts.claimsAdmin.hoursOfOperation ? ` · Hours: ${contacts.claimsAdmin.hoursOfOperation}` : ""}`);
1310
+ }
1311
+ if (contacts.billReview)
1312
+ parts.push(`Bill Review: ${contacts.billReview.name}${contacts.billReview.phone ? ` ${contacts.billReview.phone}` : ""}`);
1313
+ this.reportContactSummary = parts.length
1314
+ ? `Call ${parts.join(" · ")} to request the payment status of this bill, then record the status reported and who provided it.`
1315
+ : "Call the Claims Administrator or Bill Review vendor for this bill, then record the status reported and who provided it.";
1316
+ this.report = { status: null, company: "", representativeName: "", representativeRole: "", phone: "", callReference: "", note: "" };
1317
+ this.panel = "report_status";
1318
+ }
1319
+ async reportStatus() {
1320
+ const status = this.report.status;
1321
+ if (!status)
1322
+ return;
1323
+ await this.store.reportBillStatus({
1324
+ status,
1325
+ ...(this.report.company.trim() ? { company: this.report.company.trim() } : {}),
1326
+ ...(this.report.representativeName.trim() ? { representativeName: this.report.representativeName.trim() } : {}),
1327
+ ...(this.report.representativeRole.trim() ? { representativeRole: this.report.representativeRole.trim() } : {}),
1328
+ ...(this.report.phone.trim() ? { phone: this.report.phone.trim() } : {}),
1329
+ ...(this.report.callReference.trim() ? { callReference: this.report.callReference.trim() } : {}),
1330
+ ...(this.report.note.trim() ? { note: this.report.note.trim() } : {}),
1331
+ });
1332
+ this.panel = "";
1333
+ this.notice = "Bill status reported.";
1109
1334
  }
1110
- choose(option) {
1111
- this.selected.emit(option);
1112
- this.open = false;
1113
- this.query = "";
1335
+ async openAttachment(id, filename) { this.openBlob(await this.store.getAttachment(id), filename); }
1336
+ async openEor(id, filename) { this.openBlob(await this.store.getEor(id), filename); }
1337
+ async postPayment() { await this.store.postPayment(this.payment); this.panel = ""; this.notice = "Payment posted."; }
1338
+ async submitReview() { await this.store.submitSecondReview({ ...this.review, disputedAmount: this.review.disputedAmount > 0 ? this.review.disputedAmount : undefined, attachmentIds: this.store.data()?.bill.attachments.map((document) => document.id) ?? [], route: "ebill" }); this.panel = ""; this.notice = "Second Review submitted."; }
1339
+ async closeBill() { if (!this.closeReason.trim())
1340
+ return; await this.store.closeBill({ reason: this.closeReason }); this.panel = ""; this.notice = "Bill closed."; }
1341
+ resubmitFromForm = async (input) => {
1342
+ const data = await this.store.resubmitBill({
1343
+ bill: input.bill,
1344
+ ...(input.documents?.length ? { documents: input.documents } : {}),
1345
+ ...(this.correctionReason.trim() ? { reason: this.correctionReason.trim() } : {}),
1346
+ });
1347
+ return { billId: data.bill.id, bill: { id: data.bill.id, state: data.lifecycle.state } };
1348
+ };
1349
+ finishCorrection() { this.panel = ""; this.correctionReason = ""; this.notice = "Corrected submission sent."; }
1350
+ closeCorrectionBackdrop(event) { if (event.currentTarget === event.target)
1351
+ this.panel = ""; }
1352
+ prepareCorrection(data) {
1353
+ this.correctionInitialBill = correctionBill(data);
1354
+ this.correctionAttachments = data.bill.attachments.map((attachment) => ({
1355
+ filename: attachment.filename,
1356
+ documentType: correctionDocumentType(attachment.documentType),
1357
+ ...(attachment.description ? { description: attachment.description } : {}),
1358
+ ...(attachment.reportType ? { reportTypeCode: attachment.reportType } : {}),
1359
+ loadBlob: () => this.store.getAttachment(attachment.id),
1360
+ ...(attachment.documentType === "w9" ? { locked: true, badge: "Auto-attached" } : {}),
1361
+ }));
1362
+ this.correctionAttentionFields = [...new Set(data.rejection?.issues?.flatMap((issue) => issue.fieldPaths || []) ?? [])];
1363
+ this.correctionReason = "";
1364
+ this.panel = "resubmit";
1114
1365
  }
1115
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillComboBoxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1116
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillComboBoxComponent, isStandalone: true, selector: "mindbill-combo-box", inputs: { value: "value", placeholder: "placeholder", options: "options", disabled: "disabled", loading: "loading", loadingMore: "loadingMore", invalid: "invalid", preserveValueOnOpen: "preserveValueOnOpen", ariaLabel: "ariaLabel", createOption: "createOption" }, outputs: { opened: "opened", queryChange: "queryChange", endReached: "endReached", selected: "selected" }, ngImport: i0, template: `
1117
- <div class="combo">
1118
- <input
1119
- type="text"
1120
- role="combobox"
1121
- autocomplete="off"
1122
- [attr.aria-label]="ariaLabel"
1123
- [attr.aria-invalid]="invalid || null"
1124
- [attr.aria-expanded]="showMenu"
1125
- [disabled]="disabled"
1126
- [placeholder]="placeholder"
1127
- [value]="open ? query : value"
1128
- (focus)="onFocus()"
1129
- (click)="onFocus()"
1130
- (input)="onInput($event)"
1131
- (blur)="onBlur()"
1132
- />
1133
- @if (showMenu) {
1134
- <div class="menu" role="listbox" (scroll)="onScroll($event)">
1135
- @if (loading && !visible.length) { <div class="status">Loading…</div> }
1136
- @else if (visible.length) {
1137
- @for (option of visible; track option.id) {
1138
- <button type="button" role="option" (mousedown)="$event.preventDefault()" (click)="choose(option)">
1139
- <strong>{{ option.label }}</strong>
1140
- @if (option.detail) { <small>{{ option.detail }}</small> }
1141
- </button>
1142
- }
1143
- } @else { <div class="status">No matches</div> }
1144
- @if (loadingMore) { <div class="status">Loading more…</div> }
1366
+ 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); }
1367
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillLifecycleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1368
+ 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: { billingError: "billingError" }, usesOnChanges: true, ngImport: i0, template: `
1369
+ <section class="mb" [ngStyle]="themeStyle">
1370
+ @if (!billId) {
1371
+ <div class="state error"><strong>A submitted bill ID is required.</strong></div>
1372
+ } @else if (store.loading() && !store.data()) {
1373
+ <div class="state">Loading billing…</div>
1374
+ } @else if (store.error() && !store.data()) {
1375
+ <div class="state error"><strong>Billing is temporarily unavailable</strong><span>{{ store.error()?.message }}</span><button type="button" (click)="store.refresh()">Try again</button></div>
1376
+ } @else if (store.data(); as data) {
1377
+ <header class="summary">
1378
+ <div><span class="eyebrow">Bill {{ data.bill.billNumber }}</span><h2>{{ stateLabel(data) }}</h2><p>{{ lifecycleDetail(data) }}</p></div>
1379
+ <div class="money"><span>{{ data.bill.balanceDue | currency }}</span><small>balance</small></div>
1380
+ </header>
1381
+
1382
+ @if (data.lifecycle.state.toLowerCase() === 'rejected' && data.rejection) {
1383
+ <mindbill-bill-rejection-notice
1384
+ class="rejection"
1385
+ [rejection]="data.rejection"
1386
+ [submittedAt]="data.lifecycle.submittedAt ?? null"
1387
+ [appearance]="appearance"
1388
+ />
1389
+ } @else {
1390
+ <section class="card progress" aria-label="Bill lifecycle">
1391
+ <ol>@for (stage of lifecycleStages; track stage.id; let i=$index) { <li [class.complete]="i < currentStageIndex(data)" [class.current]="i === currentStageIndex(data)"><b>{{ i < currentStageIndex(data) ? '✓' : i + 1 }}</b><span>{{ stage.label }}</span></li> }</ol>
1392
+ </section>
1393
+ }
1394
+
1395
+ <section class="card snapshot" aria-label="Bill snapshot">
1396
+ <div class="card-title"><div><h3>Bill snapshot</h3><p>The immutable values submitted to the payer.</p></div></div>
1397
+ <dl><div><dt>Patient</dt><dd>{{ data.patient.name }}</dd></div><div><dt>Claim</dt><dd>{{ data.injury.claimNumber || '—' }}</dd></div><div><dt>Payer</dt><dd>{{ data.delivery.payerName || data.injury.claimsAdminName || '—' }}</dd></div><div><dt>Date of service</dt><dd>{{ data.bill.dos || '—' }}</dd></div><div><dt>Charged</dt><dd>{{ data.bill.totalCharge | currency }}</dd></div><div><dt>Balance</dt><dd>{{ data.bill.balanceDue | currency }}</dd></div></dl>
1398
+ </section>
1399
+
1400
+ <div class="columns">
1401
+ <section class="card"><div class="card-title"><div><h3>Remittance</h3><p>Amounts reported by the payer and posted to this bill.</p></div></div><dl><div><dt>Billed</dt><dd>{{ data.remittance.billedAmount | currency }}</dd></div><div><dt>Payer reported</dt><dd>{{ data.remittance.payerReportedPaid == null ? '—' : (data.remittance.payerReportedPaid | currency) }}</dd></div><div><dt>Principal posted</dt><dd>{{ data.remittance.postedPrincipal | currency }}</dd></div><div><dt>Penalty &amp; interest</dt><dd>{{ data.remittance.postedAdditional | currency }}</dd></div><div><dt>Total received</dt><dd>{{ data.remittance.totalPostedCash | currency }}</dd></div><div><dt>Balance</dt><dd>{{ data.remittance.balanceDue | currency }}</dd></div></dl></section>
1402
+ <section class="card"><div class="card-title"><div><h3>{{ data.delivery.payerName || 'Payer' }}</h3><p>Billing and follow-up contacts.</p></div></div><p class="muted">{{ payerContact(data) }}</p></section>
1145
1403
  </div>
1404
+
1405
+ <section class="card"><div class="card-title"><div><h3>Documents</h3><p>Documents preserved with the submitted bill.</p></div><span>{{ data.bill.attachments.length }}</span></div>@if (data.bill.attachments.length) { <ul class="rows">@for (doc of data.bill.attachments; track doc.id) { <li><div><strong>{{ doc.filename }}</strong><small>{{ doc.description || doc.documentType }}</small></div><button type="button" (click)="openAttachment(doc.id, doc.filename)">View</button></li> }</ul> } @else { <p class="muted">No documents available.</p> }</section>
1406
+
1407
+ <section class="card"><div class="card-title"><div><h3>Payments</h3><p>Payments posted to this bill.</p></div><span>{{ data.payments.length }}</span></div>@if (data.payments.length) { <ul class="rows">@for (payment of data.payments; track payment.id) { <li><div><strong>{{ payment.amount | currency }}</strong><small>{{ payment.method | uppercase }} · {{ payment.depositDate || payment.postedAt }}</small></div></li> }</ul> } @else { <p class="muted">No payments posted.</p> }</section>
1408
+
1409
+ <section class="card"><div class="card-title"><div><h3>Bill history</h3><p>Submissions, payer responses, follow-up, and payments.</p></div><span>{{ data.history?.length ?? data.activity.length }}</span></div>
1410
+ @if (data.history?.length) {
1411
+ <!-- daisyBill-style Date/Action/User/Details table: submissions are pale-blue
1412
+ expandable rows (documents + compliance due dates), notes pale-yellow,
1413
+ 277 responses expand into decoded status-code sentences. Rows arrive
1414
+ pre-worded from the MindBill API (BillLifecycleData.history). -->
1415
+ <div class="history">
1416
+ <div class="history-head" aria-hidden="true"><span>Date</span><span>Action</span><span>User</span><span>Details</span></div>
1417
+ <ul class="history-rows">
1418
+ @for (entry of data.history; track entry.id) {
1419
+ <li [class.is-submission]="entry.tone === 'submission'" [class.is-note]="entry.tone === 'note'" [class.is-problem]="entry.tone === 'problem'">
1420
+ <button type="button" class="history-line" [disabled]="!entry.details" [attr.aria-expanded]="entry.details ? isHistoryOpen(entry.id) : null" (click)="toggleHistory(entry.id)">
1421
+ <span class="h-date">{{ historyDate(entry.date) }}</span>
1422
+ <span class="h-action">{{ entry.action }}</span>
1423
+ <span class="h-actor">{{ entry.actor || '—' }}</span>
1424
+ <span class="h-summary">@if (entry.details) { <i [class.open]="isHistoryOpen(entry.id)" aria-hidden="true"></i> }{{ entry.summary }}</span>
1425
+ </button>
1426
+ @if (entry.details; as details) {
1427
+ @if (isHistoryOpen(entry.id)) {
1428
+ <div class="history-details">
1429
+ @if (details.rows?.length) { <dl class="h-kv">@for (row of details.rows ?? []; track row.label) { <div><dt>{{ row.label }}</dt><dd>{{ row.value }}</dd></div> }</dl> }
1430
+ @if (details.documents?.length) { <dl class="h-kv"><div><dt>Documents</dt><dd>@for (doc of details.documents ?? []; track doc.id) { <span class="h-doc">{{ doc.filename }}</span> }</dd></div></dl> }
1431
+ @if (details.codes?.length) { <dl class="h-codes">@for (code of details.codes ?? []; track code.code) { <div><dt>{{ code.code }}</dt><dd>{{ code.text }}</dd></div> }</dl> }
1432
+ @if (details.complianceDueDates?.length) { <div class="h-due"><em>Compliance Due Dates</em><dl>@for (due of details.complianceDueDates ?? []; track due.text) { <div><dt>{{ historyDate(due.date) }}</dt><dd>{{ due.text }}</dd></div> }</dl></div> }
1433
+ @if (details.text) { <p class="h-text">{{ details.text }}</p> }
1434
+ </div>
1435
+ }
1436
+ }
1437
+ </li>
1438
+ }
1439
+ </ul>
1440
+ </div>
1441
+ } @else {
1442
+ <ul class="timeline">@for (event of data.activity; track event.id) { <li><i></i><div><strong>{{ activityLabel(event.type) }}</strong><p>{{ event.description }}</p><small>{{ event.createdAt | date:'medium' }}</small></div></li> }</ul>
1443
+ }
1444
+ </section>
1445
+
1446
+ @if (notice) { <p class="notice">{{ notice }}</p> }
1447
+ <div class="actions">@for (action of data.lifecycle.actions; track action.id) { @if (action.id !== 'view_eor' || data.eors.length) { <button type="button" [class.primary]="action.primary" [disabled]="!action.enabled || store.mutating()" (click)="beginAction(action.id, data)">{{ action.label }}</button> } }</div>
1448
+
1449
+ @if (panel === 'payment') { <form class="card action-form" (submit)="$event.preventDefault(); postPayment()"><h3>Post payment</h3><label>Amount<input required type="number" min="0.01" step="0.01" [(ngModel)]="payment.amount" name="amount"></label><label>Method<select [(ngModel)]="payment.method" name="method"><option value="check">Check</option><option value="eft">EFT</option></select></label><label>Deposit date<input required type="date" [(ngModel)]="payment.depositDate" name="depositDate"></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Post payment</button></div></form> }
1450
+ @if (panel === 'review') { <form class="card action-form" (submit)="$event.preventDefault(); submitReview()"><h3>Submit Second Bill Review</h3><label>Reason<textarea required [(ngModel)]="review.reason" name="reason"></textarea></label><label>Payer claim control number<input required [(ngModel)]="review.payerClaimControlNumber" name="control"></label><label>Disputed amount<input type="number" min="0" step="0.01" [(ngModel)]="review.disputedAmount" name="disputed"></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Submit review</button></div></form> }
1451
+ @if (panel === 'close') { <form class="card action-form" (submit)="$event.preventDefault(); closeBill()"><h3>Close bill</h3><label>Reason<textarea required [(ngModel)]="closeReason" name="closeReason"></textarea></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Close bill</button></div></form> }
1452
+ @if (panel === 'duplicate') {
1453
+ <form class="card action-form" (submit)="$event.preventDefault(); sendDuplicate()">
1454
+ <h3>Send duplicate bill</h3>
1455
+ @if (!duplicateDelivery) {
1456
+ <p class="muted">{{ duplicateDeliveryError || 'Loading delivery options…' }}</p>
1457
+ <div><button type="button" (click)="panel=''">Cancel</button>@if (duplicateDeliveryError) { <button class="primary" type="button" (click)="loadDuplicateDelivery()">Try again</button> }</div>
1458
+ } @else {
1459
+ <p class="muted">Resend the unchanged submission packet to {{ duplicateDelivery.payerName }}.</p>
1460
+ <label>Delivery method<select [(ngModel)]="duplicate.route" name="duplicateRoute">@for (option of duplicateRoutes(); track option.route) { <option [ngValue]="option.route">{{ option.label }}</option> }</select></label>
1461
+ @if (duplicate.route === 'fax') { <label>Fax number<input required [(ngModel)]="duplicate.faxNumber" name="duplicateFax" placeholder="(000) 000-0000"></label> }
1462
+ @if (duplicate.route === 'email') { <label>Email<input required type="email" [(ngModel)]="duplicate.email" name="duplicateEmail" placeholder="claims@payer.example"></label> }
1463
+ @if (duplicate.route === 'mail') { <label>Mailing address<textarea required [(ngModel)]="duplicate.mailingAddress" name="duplicateMail"></textarea></label> }
1464
+ <div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit" [disabled]="store.mutating() || !duplicateReady()">Send duplicate</button></div>
1465
+ }
1466
+ </form>
1467
+ }
1468
+ @if (panel === 'report_status') {
1469
+ <form class="card action-form" (submit)="$event.preventDefault(); reportStatus()">
1470
+ <h3>Report Bill Status</h3>
1471
+ <p class="muted">{{ reportContactSummary }}</p>
1472
+ <div class="report-options" role="radiogroup" aria-label="Reported bill payment status">
1473
+ @for (option of reportStatusOptions; track option.id) {
1474
+ <label class="report-option"><input type="radio" [value]="option.id" [(ngModel)]="report.status" name="reportStatus"><span><strong>{{ option.label }}</strong><small>{{ option.description }}</small></span></label>
1475
+ }
1476
+ </div>
1477
+ <label>Company<input [(ngModel)]="report.company" name="reportCompany"></label>
1478
+ <label>Name<input [(ngModel)]="report.representativeName" name="reportRepresentativeName"></label>
1479
+ <label>Role<input [(ngModel)]="report.representativeRole" name="reportRepresentativeRole"></label>
1480
+ <label>Phone Number<input [(ngModel)]="report.phone" name="reportPhone"></label>
1481
+ <label>Call Reference Number<input [(ngModel)]="report.callReference" name="reportCallReference"></label>
1482
+ <label>Message Note<textarea [(ngModel)]="report.note" name="reportNote"></textarea></label>
1483
+ <div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit" [disabled]="!report.status || store.mutating()">Save</button></div>
1484
+ </form>
1485
+ }
1486
+ @if (panel === 'resubmit' && correctionInitialBill) {
1487
+ <div class="correction-backdrop" (mousedown)="closeCorrectionBackdrop($event)">
1488
+ <section class="correction-dialog" role="dialog" aria-modal="true" aria-label="Correct and resubmit">
1489
+ <button type="button" class="dialog-close" aria-label="Close" (click)="panel=''">×</button>
1490
+ <header><h2>Correct and resubmit</h2><p>The original submission remains immutable. Review the rejected values and create the next submission attempt on this bill.</p></header>
1491
+ @if (data.rejection; as rejection) {
1492
+ <div class="correction-reason"><strong>{{ rejection.reason || 'Why it was rejected' }}</strong>@for (issue of rejection.issues || []; track issue.code) { <span><b>{{ issue.code }}</b> {{ issue.description }}</span> }</div>
1493
+ }
1494
+ @if (payerContact(data) !== 'No payer contact details are available.') { <p class="contact-hint"><strong>Questions?</strong> {{ payerContact(data) }}</p> }
1495
+ <label class="correction-note">Correction note (optional)<textarea [(ngModel)]="correctionReason" name="correctionReason" placeholder="What changed before resubmission?"></textarea></label>
1496
+ <mindbill-bill-submission
1497
+ [initialBill]="correctionInitialBill"
1498
+ [attachments]="correctionAttachments"
1499
+ [attentionFields]="correctionAttentionFields"
1500
+ [appearance]="appearance"
1501
+ [sessionEndpoint]="sessionEndpoint"
1502
+ [getSession]="getSession"
1503
+ [apiBaseUrl]="apiBaseUrl"
1504
+ [submitter]="resubmitFromForm"
1505
+ heading="Corrected bill information"
1506
+ description="Correct the highlighted values, confirm every required field, then resubmit."
1507
+ submitLabel="Resubmit bill"
1508
+ (submitted)="finishCorrection()"
1509
+ />
1510
+ </section>
1511
+ </div>
1512
+ }
1146
1513
  }
1147
- </div>
1148
- `, isInline: true, styles: [":host{display:block}.combo{position:relative}input{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}input[aria-invalid=true]{border-color:#c83c3c}.menu{position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:30;max-height:320px;overflow:auto;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);box-shadow:0 14px 35px #172b3730}.menu button{display:grid;width:100%;gap:3px;border:0;border-bottom:1px solid color-mix(in srgb,var(--b) 60%,transparent);background:var(--s);padding:11px 14px;text-align:left;color:var(--t);font:inherit;cursor:pointer}.menu button:last-child{border-bottom:0}.menu button:hover{background:color-mix(in srgb,var(--a) 7%,var(--s))}.menu strong{font-weight:720}.menu small{color:var(--m);font-size:12px}.status{padding:11px 14px;color:var(--m);font-size:13px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
1514
+ </section>
1515
+ `, isInline: true, styles: [":host{display:block}.mb{font-family:var(--font);color:var(--t);background:var(--bg);padding:20px}.summary,.card,.columns,.rejection{display:block;max-width:1120px;margin:0 auto 16px}.summary{display:flex;justify-content:space-between;align-items:end}.summary h2{margin:4px 0}.summary p,.muted{color:var(--m)}.eyebrow,dt{font-size:12px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--m)}.money{text-align:right}.money span{display:block;font-size:28px;font-weight:800}.money small{color:var(--m)}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:20px}.card-title{display:flex;justify-content:space-between;gap:20px}.card-title h3{margin:0}.card-title p{margin:5px 0;color:var(--m)}.progress ol{display:flex;list-style:none;padding:0;margin:0}.progress li{flex:1;text-align:center;position:relative;color:var(--m)}.progress li:before{content:\"\";position:absolute;top:16px;left:0;right:0;border-top:2px solid var(--b)}.progress li:first-child:before{left:50%}.progress li:last-child:before{right:50%}.progress b{position:relative;z-index:1;display:grid;place-items:center;width:32px;height:32px;margin:auto;border:2px solid var(--b);border-radius:50%;background:var(--s)}.progress .complete b,.progress .current b{border-color:var(--a);background:var(--a);color:var(--ac)}.progress span{display:block;margin-top:8px;font-size:12px;font-weight:700}.snapshot dl,.columns dl{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}.snapshot dd,.columns dd{font-weight:700;margin:6px 0}.columns{display:grid;grid-template-columns:1fr 1fr;gap:16px}.columns .card{margin:0}.rows,.timeline{list-style:none;margin:12px 0 0;padding:0}.rows li{display:flex;justify-content:space-between;align-items:center;padding:12px 0;border-top:1px solid var(--b)}.rows small{display:block;color:var(--m);margin-top:3px}.timeline li{display:flex;gap:14px;padding:10px 0}.timeline i{width:10px;height:10px;margin-top:5px;border:3px solid var(--a);border-radius:50%}.timeline p{margin:4px 0}.timeline small{color:var(--m)}button,input,select,textarea{font:inherit;border:1px solid var(--b);border-radius:var(--cr);padding:9px 12px;background:var(--s);color:var(--t)}button{cursor:pointer;font-weight:700}.primary{background:var(--a);border-color:var(--a);color:var(--ac)}.actions{max-width:1120px;margin:0 auto;display:flex;justify-content:flex-end;gap:10px}.action-form{display:grid;gap:12px}.action-form label{display:grid;gap:5px;font-weight:700}.action-form div{display:flex;justify-content:flex-end;gap:10px}.state{max-width:720px;margin:auto;padding:32px;text-align:center}.state span{display:block;margin:8px}.error{color:#9b1c1c}.notice{max-width:1120px;margin:0 auto 12px;color:var(--m)}@media(max-width:760px){.mb{padding:12px}.summary,.columns{display:block}.money{text-align:left;margin-top:12px}.columns .card{margin-bottom:12px}.snapshot dl,.columns dl{grid-template-columns:1fr 1fr}.progress span{font-size:10px}.card{padding:15px}}.history{border:1px solid var(--b);border-radius:10px;overflow:hidden;margin-top:12px}.history-head{display:grid;grid-template-columns:96px 150px 130px minmax(0,1fr);gap:12px;padding:8px 14px;background:color-mix(in srgb,var(--b) 26%,var(--s));color:var(--m);font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}.history-rows{list-style:none;margin:0;padding:0}.history-rows>li{border-top:1px solid var(--b)}.history-rows>li.is-submission{background:color-mix(in srgb,var(--a) 9%,var(--s))}.history-rows>li.is-note{background:color-mix(in srgb,#f2c344 13%,var(--s))}.history-line{display:grid;grid-template-columns:96px 150px 130px minmax(0,1fr);gap:12px;width:100%;padding:10px 14px;border:0;border-radius:0;background:transparent;text-align:left;font-weight:400;cursor:default}.history-line:not(:disabled){cursor:pointer}.history-line:not(:disabled):hover .h-summary{text-decoration:underline}.h-date,.h-actor{color:var(--m);font-size:13px;overflow-wrap:anywhere}.h-action{font-weight:700;font-size:13.5px}.is-problem .h-action{color:#9b1c1c}.h-summary{display:flex;gap:7px;min-width:0;font-size:13.5px;line-height:1.45;overflow-wrap:anywhere}.h-summary i{flex:0 0 auto;margin-top:5px;width:0;height:0;border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:6px solid var(--m);border-radius:0;transition:transform .12s ease}.h-summary i.open{transform:rotate(90deg)}.history-details{padding:4px 14px 14px 122px;font-size:13.5px;line-height:1.5}.h-kv,.h-codes,.h-due dl{display:grid;gap:4px;margin:0}.h-kv>div{display:grid;grid-template-columns:180px minmax(0,1fr);gap:12px;border-top:1px solid color-mix(in srgb,var(--b) 60%,transparent);padding-top:4px}.h-kv dt,.h-codes dt,.h-due dt{font-size:13px;font-weight:700;letter-spacing:0;text-transform:none;color:var(--t)}.h-kv dd,.h-codes dd,.h-due dd{margin:0;overflow-wrap:anywhere}.h-doc{display:block}.h-codes>div{display:grid;grid-template-columns:56px minmax(0,1fr);gap:12px}.h-codes dt{font-weight:800}.h-due{margin-top:8px}.h-due em{display:block;color:var(--m);font-size:12.5px;margin-bottom:4px}.h-due dl>div{display:grid;grid-template-columns:96px minmax(0,1fr);gap:12px;border-top:1px solid color-mix(in srgb,var(--b) 60%,transparent);padding-top:4px}.h-text{margin:8px 0 0;white-space:pre-wrap}@media(max-width:760px){.history-head{display:none}.history-line{grid-template-columns:1fr;gap:2px;padding:10px 12px}.history-details{padding:4px 12px 12px}.h-kv>div,.h-due dl>div{grid-template-columns:1fr}}.report-options{display:grid;gap:8px}.action-form label.report-option{display:flex;gap:10px;align-items:flex-start;border:1px solid var(--b);border-radius:var(--cr);padding:10px 12px;font-weight:400;cursor:pointer}.report-option input{width:16px;min-height:16px;margin-top:3px;padding:0;border:0}.report-option strong{display:block;font-size:13.5px}.report-option small{display:block;color:var(--m);margin-top:2px;font-size:12.5px}.correction-backdrop{position:fixed;inset:0;z-index:100;overflow:auto;background:#172b37b8;padding:28px}.correction-dialog{position:relative;max-width:1180px;margin:0 auto;background:var(--bg);border-radius:var(--r);box-shadow:0 24px 80px #0006;padding:26px}.correction-dialog>header{padding-right:48px;margin-bottom:18px}.correction-dialog h2{margin:0 0 6px}.correction-dialog p{margin:0;color:var(--m)}.dialog-close{position:absolute;right:20px;top:20px;width:40px;height:40px;padding:0;font-size:24px}.correction-reason{display:grid;gap:8px;margin-bottom:14px;border-left:4px solid #c43d3d;border-radius:8px;background:#fff5f5;padding:14px;color:#8f2525}.correction-reason span{display:block}.correction-reason b{margin-right:6px}.contact-hint{margin:0 0 14px!important;border:1px solid var(--b);border-radius:8px;background:var(--s);padding:12px!important;color:var(--t)!important}.correction-note{display:grid;gap:6px;margin-bottom:18px;font-weight:700}.correction-note textarea{min-height:76px}.correction-dialog mindbill-bill-submission{display:block}.correction-dialog .mb{padding:0}@media(max-width:760px){.correction-backdrop{padding:0}.correction-dialog{min-height:100%;border-radius:0;padding:18px 12px}.dialog-close{right:12px;top:12px}}\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.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: "component", type: MindBillBillRejectionNoticeComponent, selector: "mindbill-bill-rejection-notice", inputs: ["rejection", "submittedAt", "title", "appearance"] }, { kind: "component", type: MindBillBillSubmissionComponent, selector: "mindbill-bill-submission", inputs: ["initialBill", "attachments", "appearance", "sessionEndpoint", "getSession", "apiBaseUrl", "heading", "description", "submitLabel", "attentionFields", "procedureOptions", "modifierOptions", "taxonomyOptions", "submitter"], outputs: ["submitted", "submissionError", "billChange"] }, { kind: "pipe", type: i1.UpperCasePipe, name: "uppercase" }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }, { kind: "pipe", type: i1.DatePipe, name: "date" }] });
1149
1516
  }
1150
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillComboBoxComponent, decorators: [{
1517
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillLifecycleComponent, decorators: [{
1151
1518
  type: Component,
1152
- args: [{ selector: "mindbill-combo-box", standalone: true, imports: [CommonModule], template: `
1153
- <div class="combo">
1154
- <input
1155
- type="text"
1156
- role="combobox"
1157
- autocomplete="off"
1158
- [attr.aria-label]="ariaLabel"
1159
- [attr.aria-invalid]="invalid || null"
1160
- [attr.aria-expanded]="showMenu"
1161
- [disabled]="disabled"
1162
- [placeholder]="placeholder"
1163
- [value]="open ? query : value"
1164
- (focus)="onFocus()"
1165
- (click)="onFocus()"
1166
- (input)="onInput($event)"
1167
- (blur)="onBlur()"
1168
- />
1169
- @if (showMenu) {
1170
- <div class="menu" role="listbox" (scroll)="onScroll($event)">
1171
- @if (loading && !visible.length) { <div class="status">Loading…</div> }
1172
- @else if (visible.length) {
1173
- @for (option of visible; track option.id) {
1174
- <button type="button" role="option" (mousedown)="$event.preventDefault()" (click)="choose(option)">
1175
- <strong>{{ option.label }}</strong>
1176
- @if (option.detail) { <small>{{ option.detail }}</small> }
1177
- </button>
1519
+ args: [{ selector: "mindbill-bill-lifecycle", standalone: true, imports: [CommonModule, FormsModule, MindBillBillRejectionNoticeComponent, MindBillBillSubmissionComponent], template: `
1520
+ <section class="mb" [ngStyle]="themeStyle">
1521
+ @if (!billId) {
1522
+ <div class="state error"><strong>A submitted bill ID is required.</strong></div>
1523
+ } @else if (store.loading() && !store.data()) {
1524
+ <div class="state">Loading billing…</div>
1525
+ } @else if (store.error() && !store.data()) {
1526
+ <div class="state error"><strong>Billing is temporarily unavailable</strong><span>{{ store.error()?.message }}</span><button type="button" (click)="store.refresh()">Try again</button></div>
1527
+ } @else if (store.data(); as data) {
1528
+ <header class="summary">
1529
+ <div><span class="eyebrow">Bill {{ data.bill.billNumber }}</span><h2>{{ stateLabel(data) }}</h2><p>{{ lifecycleDetail(data) }}</p></div>
1530
+ <div class="money"><span>{{ data.bill.balanceDue | currency }}</span><small>balance</small></div>
1531
+ </header>
1532
+
1533
+ @if (data.lifecycle.state.toLowerCase() === 'rejected' && data.rejection) {
1534
+ <mindbill-bill-rejection-notice
1535
+ class="rejection"
1536
+ [rejection]="data.rejection"
1537
+ [submittedAt]="data.lifecycle.submittedAt ?? null"
1538
+ [appearance]="appearance"
1539
+ />
1540
+ } @else {
1541
+ <section class="card progress" aria-label="Bill lifecycle">
1542
+ <ol>@for (stage of lifecycleStages; track stage.id; let i=$index) { <li [class.complete]="i < currentStageIndex(data)" [class.current]="i === currentStageIndex(data)"><b>{{ i < currentStageIndex(data) ? '✓' : i + 1 }}</b><span>{{ stage.label }}</span></li> }</ol>
1543
+ </section>
1544
+ }
1545
+
1546
+ <section class="card snapshot" aria-label="Bill snapshot">
1547
+ <div class="card-title"><div><h3>Bill snapshot</h3><p>The immutable values submitted to the payer.</p></div></div>
1548
+ <dl><div><dt>Patient</dt><dd>{{ data.patient.name }}</dd></div><div><dt>Claim</dt><dd>{{ data.injury.claimNumber || '—' }}</dd></div><div><dt>Payer</dt><dd>{{ data.delivery.payerName || data.injury.claimsAdminName || '—' }}</dd></div><div><dt>Date of service</dt><dd>{{ data.bill.dos || '—' }}</dd></div><div><dt>Charged</dt><dd>{{ data.bill.totalCharge | currency }}</dd></div><div><dt>Balance</dt><dd>{{ data.bill.balanceDue | currency }}</dd></div></dl>
1549
+ </section>
1550
+
1551
+ <div class="columns">
1552
+ <section class="card"><div class="card-title"><div><h3>Remittance</h3><p>Amounts reported by the payer and posted to this bill.</p></div></div><dl><div><dt>Billed</dt><dd>{{ data.remittance.billedAmount | currency }}</dd></div><div><dt>Payer reported</dt><dd>{{ data.remittance.payerReportedPaid == null ? '—' : (data.remittance.payerReportedPaid | currency) }}</dd></div><div><dt>Principal posted</dt><dd>{{ data.remittance.postedPrincipal | currency }}</dd></div><div><dt>Penalty &amp; interest</dt><dd>{{ data.remittance.postedAdditional | currency }}</dd></div><div><dt>Total received</dt><dd>{{ data.remittance.totalPostedCash | currency }}</dd></div><div><dt>Balance</dt><dd>{{ data.remittance.balanceDue | currency }}</dd></div></dl></section>
1553
+ <section class="card"><div class="card-title"><div><h3>{{ data.delivery.payerName || 'Payer' }}</h3><p>Billing and follow-up contacts.</p></div></div><p class="muted">{{ payerContact(data) }}</p></section>
1554
+ </div>
1555
+
1556
+ <section class="card"><div class="card-title"><div><h3>Documents</h3><p>Documents preserved with the submitted bill.</p></div><span>{{ data.bill.attachments.length }}</span></div>@if (data.bill.attachments.length) { <ul class="rows">@for (doc of data.bill.attachments; track doc.id) { <li><div><strong>{{ doc.filename }}</strong><small>{{ doc.description || doc.documentType }}</small></div><button type="button" (click)="openAttachment(doc.id, doc.filename)">View</button></li> }</ul> } @else { <p class="muted">No documents available.</p> }</section>
1557
+
1558
+ <section class="card"><div class="card-title"><div><h3>Payments</h3><p>Payments posted to this bill.</p></div><span>{{ data.payments.length }}</span></div>@if (data.payments.length) { <ul class="rows">@for (payment of data.payments; track payment.id) { <li><div><strong>{{ payment.amount | currency }}</strong><small>{{ payment.method | uppercase }} · {{ payment.depositDate || payment.postedAt }}</small></div></li> }</ul> } @else { <p class="muted">No payments posted.</p> }</section>
1559
+
1560
+ <section class="card"><div class="card-title"><div><h3>Bill history</h3><p>Submissions, payer responses, follow-up, and payments.</p></div><span>{{ data.history?.length ?? data.activity.length }}</span></div>
1561
+ @if (data.history?.length) {
1562
+ <!-- daisyBill-style Date/Action/User/Details table: submissions are pale-blue
1563
+ expandable rows (documents + compliance due dates), notes pale-yellow,
1564
+ 277 responses expand into decoded status-code sentences. Rows arrive
1565
+ pre-worded from the MindBill API (BillLifecycleData.history). -->
1566
+ <div class="history">
1567
+ <div class="history-head" aria-hidden="true"><span>Date</span><span>Action</span><span>User</span><span>Details</span></div>
1568
+ <ul class="history-rows">
1569
+ @for (entry of data.history; track entry.id) {
1570
+ <li [class.is-submission]="entry.tone === 'submission'" [class.is-note]="entry.tone === 'note'" [class.is-problem]="entry.tone === 'problem'">
1571
+ <button type="button" class="history-line" [disabled]="!entry.details" [attr.aria-expanded]="entry.details ? isHistoryOpen(entry.id) : null" (click)="toggleHistory(entry.id)">
1572
+ <span class="h-date">{{ historyDate(entry.date) }}</span>
1573
+ <span class="h-action">{{ entry.action }}</span>
1574
+ <span class="h-actor">{{ entry.actor || '—' }}</span>
1575
+ <span class="h-summary">@if (entry.details) { <i [class.open]="isHistoryOpen(entry.id)" aria-hidden="true"></i> }{{ entry.summary }}</span>
1576
+ </button>
1577
+ @if (entry.details; as details) {
1578
+ @if (isHistoryOpen(entry.id)) {
1579
+ <div class="history-details">
1580
+ @if (details.rows?.length) { <dl class="h-kv">@for (row of details.rows ?? []; track row.label) { <div><dt>{{ row.label }}</dt><dd>{{ row.value }}</dd></div> }</dl> }
1581
+ @if (details.documents?.length) { <dl class="h-kv"><div><dt>Documents</dt><dd>@for (doc of details.documents ?? []; track doc.id) { <span class="h-doc">{{ doc.filename }}</span> }</dd></div></dl> }
1582
+ @if (details.codes?.length) { <dl class="h-codes">@for (code of details.codes ?? []; track code.code) { <div><dt>{{ code.code }}</dt><dd>{{ code.text }}</dd></div> }</dl> }
1583
+ @if (details.complianceDueDates?.length) { <div class="h-due"><em>Compliance Due Dates</em><dl>@for (due of details.complianceDueDates ?? []; track due.text) { <div><dt>{{ historyDate(due.date) }}</dt><dd>{{ due.text }}</dd></div> }</dl></div> }
1584
+ @if (details.text) { <p class="h-text">{{ details.text }}</p> }
1585
+ </div>
1586
+ }
1587
+ }
1588
+ </li>
1589
+ }
1590
+ </ul>
1591
+ </div>
1592
+ } @else {
1593
+ <ul class="timeline">@for (event of data.activity; track event.id) { <li><i></i><div><strong>{{ activityLabel(event.type) }}</strong><p>{{ event.description }}</p><small>{{ event.createdAt | date:'medium' }}</small></div></li> }</ul>
1594
+ }
1595
+ </section>
1596
+
1597
+ @if (notice) { <p class="notice">{{ notice }}</p> }
1598
+ <div class="actions">@for (action of data.lifecycle.actions; track action.id) { @if (action.id !== 'view_eor' || data.eors.length) { <button type="button" [class.primary]="action.primary" [disabled]="!action.enabled || store.mutating()" (click)="beginAction(action.id, data)">{{ action.label }}</button> } }</div>
1599
+
1600
+ @if (panel === 'payment') { <form class="card action-form" (submit)="$event.preventDefault(); postPayment()"><h3>Post payment</h3><label>Amount<input required type="number" min="0.01" step="0.01" [(ngModel)]="payment.amount" name="amount"></label><label>Method<select [(ngModel)]="payment.method" name="method"><option value="check">Check</option><option value="eft">EFT</option></select></label><label>Deposit date<input required type="date" [(ngModel)]="payment.depositDate" name="depositDate"></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Post payment</button></div></form> }
1601
+ @if (panel === 'review') { <form class="card action-form" (submit)="$event.preventDefault(); submitReview()"><h3>Submit Second Bill Review</h3><label>Reason<textarea required [(ngModel)]="review.reason" name="reason"></textarea></label><label>Payer claim control number<input required [(ngModel)]="review.payerClaimControlNumber" name="control"></label><label>Disputed amount<input type="number" min="0" step="0.01" [(ngModel)]="review.disputedAmount" name="disputed"></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Submit review</button></div></form> }
1602
+ @if (panel === 'close') { <form class="card action-form" (submit)="$event.preventDefault(); closeBill()"><h3>Close bill</h3><label>Reason<textarea required [(ngModel)]="closeReason" name="closeReason"></textarea></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Close bill</button></div></form> }
1603
+ @if (panel === 'duplicate') {
1604
+ <form class="card action-form" (submit)="$event.preventDefault(); sendDuplicate()">
1605
+ <h3>Send duplicate bill</h3>
1606
+ @if (!duplicateDelivery) {
1607
+ <p class="muted">{{ duplicateDeliveryError || 'Loading delivery options…' }}</p>
1608
+ <div><button type="button" (click)="panel=''">Cancel</button>@if (duplicateDeliveryError) { <button class="primary" type="button" (click)="loadDuplicateDelivery()">Try again</button> }</div>
1609
+ } @else {
1610
+ <p class="muted">Resend the unchanged submission packet to {{ duplicateDelivery.payerName }}.</p>
1611
+ <label>Delivery method<select [(ngModel)]="duplicate.route" name="duplicateRoute">@for (option of duplicateRoutes(); track option.route) { <option [ngValue]="option.route">{{ option.label }}</option> }</select></label>
1612
+ @if (duplicate.route === 'fax') { <label>Fax number<input required [(ngModel)]="duplicate.faxNumber" name="duplicateFax" placeholder="(000) 000-0000"></label> }
1613
+ @if (duplicate.route === 'email') { <label>Email<input required type="email" [(ngModel)]="duplicate.email" name="duplicateEmail" placeholder="claims@payer.example"></label> }
1614
+ @if (duplicate.route === 'mail') { <label>Mailing address<textarea required [(ngModel)]="duplicate.mailingAddress" name="duplicateMail"></textarea></label> }
1615
+ <div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit" [disabled]="store.mutating() || !duplicateReady()">Send duplicate</button></div>
1178
1616
  }
1179
- } @else { <div class="status">No matches</div> }
1180
- @if (loadingMore) { <div class="status">Loading more…</div> }
1181
- </div>
1617
+ </form>
1618
+ }
1619
+ @if (panel === 'report_status') {
1620
+ <form class="card action-form" (submit)="$event.preventDefault(); reportStatus()">
1621
+ <h3>Report Bill Status</h3>
1622
+ <p class="muted">{{ reportContactSummary }}</p>
1623
+ <div class="report-options" role="radiogroup" aria-label="Reported bill payment status">
1624
+ @for (option of reportStatusOptions; track option.id) {
1625
+ <label class="report-option"><input type="radio" [value]="option.id" [(ngModel)]="report.status" name="reportStatus"><span><strong>{{ option.label }}</strong><small>{{ option.description }}</small></span></label>
1626
+ }
1627
+ </div>
1628
+ <label>Company<input [(ngModel)]="report.company" name="reportCompany"></label>
1629
+ <label>Name<input [(ngModel)]="report.representativeName" name="reportRepresentativeName"></label>
1630
+ <label>Role<input [(ngModel)]="report.representativeRole" name="reportRepresentativeRole"></label>
1631
+ <label>Phone Number<input [(ngModel)]="report.phone" name="reportPhone"></label>
1632
+ <label>Call Reference Number<input [(ngModel)]="report.callReference" name="reportCallReference"></label>
1633
+ <label>Message Note<textarea [(ngModel)]="report.note" name="reportNote"></textarea></label>
1634
+ <div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit" [disabled]="!report.status || store.mutating()">Save</button></div>
1635
+ </form>
1636
+ }
1637
+ @if (panel === 'resubmit' && correctionInitialBill) {
1638
+ <div class="correction-backdrop" (mousedown)="closeCorrectionBackdrop($event)">
1639
+ <section class="correction-dialog" role="dialog" aria-modal="true" aria-label="Correct and resubmit">
1640
+ <button type="button" class="dialog-close" aria-label="Close" (click)="panel=''">×</button>
1641
+ <header><h2>Correct and resubmit</h2><p>The original submission remains immutable. Review the rejected values and create the next submission attempt on this bill.</p></header>
1642
+ @if (data.rejection; as rejection) {
1643
+ <div class="correction-reason"><strong>{{ rejection.reason || 'Why it was rejected' }}</strong>@for (issue of rejection.issues || []; track issue.code) { <span><b>{{ issue.code }}</b> {{ issue.description }}</span> }</div>
1644
+ }
1645
+ @if (payerContact(data) !== 'No payer contact details are available.') { <p class="contact-hint"><strong>Questions?</strong> {{ payerContact(data) }}</p> }
1646
+ <label class="correction-note">Correction note (optional)<textarea [(ngModel)]="correctionReason" name="correctionReason" placeholder="What changed before resubmission?"></textarea></label>
1647
+ <mindbill-bill-submission
1648
+ [initialBill]="correctionInitialBill"
1649
+ [attachments]="correctionAttachments"
1650
+ [attentionFields]="correctionAttentionFields"
1651
+ [appearance]="appearance"
1652
+ [sessionEndpoint]="sessionEndpoint"
1653
+ [getSession]="getSession"
1654
+ [apiBaseUrl]="apiBaseUrl"
1655
+ [submitter]="resubmitFromForm"
1656
+ heading="Corrected bill information"
1657
+ description="Correct the highlighted values, confirm every required field, then resubmit."
1658
+ submitLabel="Resubmit bill"
1659
+ (submitted)="finishCorrection()"
1660
+ />
1661
+ </section>
1662
+ </div>
1663
+ }
1182
1664
  }
1183
- </div>
1184
- `, styles: [":host{display:block}.combo{position:relative}input{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}input[aria-invalid=true]{border-color:#c83c3c}.menu{position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:30;max-height:320px;overflow:auto;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);box-shadow:0 14px 35px #172b3730}.menu button{display:grid;width:100%;gap:3px;border:0;border-bottom:1px solid color-mix(in srgb,var(--b) 60%,transparent);background:var(--s);padding:11px 14px;text-align:left;color:var(--t);font:inherit;cursor:pointer}.menu button:last-child{border-bottom:0}.menu button:hover{background:color-mix(in srgb,var(--a) 7%,var(--s))}.menu strong{font-weight:720}.menu small{color:var(--m);font-size:12px}.status{padding:11px 14px;color:var(--m);font-size:13px}\n"] }]
1185
- }], propDecorators: { value: [{
1186
- type: Input
1187
- }], placeholder: [{
1665
+ </section>
1666
+ `, styles: [":host{display:block}.mb{font-family:var(--font);color:var(--t);background:var(--bg);padding:20px}.summary,.card,.columns,.rejection{display:block;max-width:1120px;margin:0 auto 16px}.summary{display:flex;justify-content:space-between;align-items:end}.summary h2{margin:4px 0}.summary p,.muted{color:var(--m)}.eyebrow,dt{font-size:12px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--m)}.money{text-align:right}.money span{display:block;font-size:28px;font-weight:800}.money small{color:var(--m)}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:20px}.card-title{display:flex;justify-content:space-between;gap:20px}.card-title h3{margin:0}.card-title p{margin:5px 0;color:var(--m)}.progress ol{display:flex;list-style:none;padding:0;margin:0}.progress li{flex:1;text-align:center;position:relative;color:var(--m)}.progress li:before{content:\"\";position:absolute;top:16px;left:0;right:0;border-top:2px solid var(--b)}.progress li:first-child:before{left:50%}.progress li:last-child:before{right:50%}.progress b{position:relative;z-index:1;display:grid;place-items:center;width:32px;height:32px;margin:auto;border:2px solid var(--b);border-radius:50%;background:var(--s)}.progress .complete b,.progress .current b{border-color:var(--a);background:var(--a);color:var(--ac)}.progress span{display:block;margin-top:8px;font-size:12px;font-weight:700}.snapshot dl,.columns dl{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}.snapshot dd,.columns dd{font-weight:700;margin:6px 0}.columns{display:grid;grid-template-columns:1fr 1fr;gap:16px}.columns .card{margin:0}.rows,.timeline{list-style:none;margin:12px 0 0;padding:0}.rows li{display:flex;justify-content:space-between;align-items:center;padding:12px 0;border-top:1px solid var(--b)}.rows small{display:block;color:var(--m);margin-top:3px}.timeline li{display:flex;gap:14px;padding:10px 0}.timeline i{width:10px;height:10px;margin-top:5px;border:3px solid var(--a);border-radius:50%}.timeline p{margin:4px 0}.timeline small{color:var(--m)}button,input,select,textarea{font:inherit;border:1px solid var(--b);border-radius:var(--cr);padding:9px 12px;background:var(--s);color:var(--t)}button{cursor:pointer;font-weight:700}.primary{background:var(--a);border-color:var(--a);color:var(--ac)}.actions{max-width:1120px;margin:0 auto;display:flex;justify-content:flex-end;gap:10px}.action-form{display:grid;gap:12px}.action-form label{display:grid;gap:5px;font-weight:700}.action-form div{display:flex;justify-content:flex-end;gap:10px}.state{max-width:720px;margin:auto;padding:32px;text-align:center}.state span{display:block;margin:8px}.error{color:#9b1c1c}.notice{max-width:1120px;margin:0 auto 12px;color:var(--m)}@media(max-width:760px){.mb{padding:12px}.summary,.columns{display:block}.money{text-align:left;margin-top:12px}.columns .card{margin-bottom:12px}.snapshot dl,.columns dl{grid-template-columns:1fr 1fr}.progress span{font-size:10px}.card{padding:15px}}.history{border:1px solid var(--b);border-radius:10px;overflow:hidden;margin-top:12px}.history-head{display:grid;grid-template-columns:96px 150px 130px minmax(0,1fr);gap:12px;padding:8px 14px;background:color-mix(in srgb,var(--b) 26%,var(--s));color:var(--m);font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}.history-rows{list-style:none;margin:0;padding:0}.history-rows>li{border-top:1px solid var(--b)}.history-rows>li.is-submission{background:color-mix(in srgb,var(--a) 9%,var(--s))}.history-rows>li.is-note{background:color-mix(in srgb,#f2c344 13%,var(--s))}.history-line{display:grid;grid-template-columns:96px 150px 130px minmax(0,1fr);gap:12px;width:100%;padding:10px 14px;border:0;border-radius:0;background:transparent;text-align:left;font-weight:400;cursor:default}.history-line:not(:disabled){cursor:pointer}.history-line:not(:disabled):hover .h-summary{text-decoration:underline}.h-date,.h-actor{color:var(--m);font-size:13px;overflow-wrap:anywhere}.h-action{font-weight:700;font-size:13.5px}.is-problem .h-action{color:#9b1c1c}.h-summary{display:flex;gap:7px;min-width:0;font-size:13.5px;line-height:1.45;overflow-wrap:anywhere}.h-summary i{flex:0 0 auto;margin-top:5px;width:0;height:0;border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:6px solid var(--m);border-radius:0;transition:transform .12s ease}.h-summary i.open{transform:rotate(90deg)}.history-details{padding:4px 14px 14px 122px;font-size:13.5px;line-height:1.5}.h-kv,.h-codes,.h-due dl{display:grid;gap:4px;margin:0}.h-kv>div{display:grid;grid-template-columns:180px minmax(0,1fr);gap:12px;border-top:1px solid color-mix(in srgb,var(--b) 60%,transparent);padding-top:4px}.h-kv dt,.h-codes dt,.h-due dt{font-size:13px;font-weight:700;letter-spacing:0;text-transform:none;color:var(--t)}.h-kv dd,.h-codes dd,.h-due dd{margin:0;overflow-wrap:anywhere}.h-doc{display:block}.h-codes>div{display:grid;grid-template-columns:56px minmax(0,1fr);gap:12px}.h-codes dt{font-weight:800}.h-due{margin-top:8px}.h-due em{display:block;color:var(--m);font-size:12.5px;margin-bottom:4px}.h-due dl>div{display:grid;grid-template-columns:96px minmax(0,1fr);gap:12px;border-top:1px solid color-mix(in srgb,var(--b) 60%,transparent);padding-top:4px}.h-text{margin:8px 0 0;white-space:pre-wrap}@media(max-width:760px){.history-head{display:none}.history-line{grid-template-columns:1fr;gap:2px;padding:10px 12px}.history-details{padding:4px 12px 12px}.h-kv>div,.h-due dl>div{grid-template-columns:1fr}}.report-options{display:grid;gap:8px}.action-form label.report-option{display:flex;gap:10px;align-items:flex-start;border:1px solid var(--b);border-radius:var(--cr);padding:10px 12px;font-weight:400;cursor:pointer}.report-option input{width:16px;min-height:16px;margin-top:3px;padding:0;border:0}.report-option strong{display:block;font-size:13.5px}.report-option small{display:block;color:var(--m);margin-top:2px;font-size:12.5px}.correction-backdrop{position:fixed;inset:0;z-index:100;overflow:auto;background:#172b37b8;padding:28px}.correction-dialog{position:relative;max-width:1180px;margin:0 auto;background:var(--bg);border-radius:var(--r);box-shadow:0 24px 80px #0006;padding:26px}.correction-dialog>header{padding-right:48px;margin-bottom:18px}.correction-dialog h2{margin:0 0 6px}.correction-dialog p{margin:0;color:var(--m)}.dialog-close{position:absolute;right:20px;top:20px;width:40px;height:40px;padding:0;font-size:24px}.correction-reason{display:grid;gap:8px;margin-bottom:14px;border-left:4px solid #c43d3d;border-radius:8px;background:#fff5f5;padding:14px;color:#8f2525}.correction-reason span{display:block}.correction-reason b{margin-right:6px}.contact-hint{margin:0 0 14px!important;border:1px solid var(--b);border-radius:8px;background:var(--s);padding:12px!important;color:var(--t)!important}.correction-note{display:grid;gap:6px;margin-bottom:18px;font-weight:700}.correction-note textarea{min-height:76px}.correction-dialog mindbill-bill-submission{display:block}.correction-dialog .mb{padding:0}@media(max-width:760px){.correction-backdrop{padding:0}.correction-dialog{min-height:100%;border-radius:0;padding:18px 12px}.dialog-close{right:12px;top:12px}}\n"] }]
1667
+ }], ctorParameters: () => [], propDecorators: { billId: [{
1188
1668
  type: Input
1189
- }], options: [{
1669
+ }], sessionEndpoint: [{
1190
1670
  type: Input
1191
- }], disabled: [{
1671
+ }], apiBaseUrl: [{
1192
1672
  type: Input
1193
- }], loading: [{
1673
+ }], getSession: [{
1194
1674
  type: Input
1195
- }], loadingMore: [{
1675
+ }], refreshInterval: [{
1196
1676
  type: Input
1197
- }], invalid: [{
1677
+ }], appearance: [{
1198
1678
  type: Input
1199
- }], preserveValueOnOpen: [{
1679
+ }], billingError: [{
1680
+ type: Output
1681
+ }] } });
1682
+ const CORRECTION_DOCUMENT_TYPES = new Set(["final_report", "letter_of_attestation", "proof_of_service", "form_122", "return_to_work_voucher", "w9", "medical_records", "appeal", "other"]);
1683
+ function correctionDocumentType(value) { return (CORRECTION_DOCUMENT_TYPES.has(value) ? value : "other"); }
1684
+ function correctionBill(data) {
1685
+ const billing = data.bill.billingSnapshot?.billingProvider;
1686
+ const rendering = data.bill.billingSnapshot?.renderingProvider;
1687
+ const location = data.bill.billingSnapshot?.placeOfService;
1688
+ const nameParts = data.patient.name.trim().split(/\s+/);
1689
+ return {
1690
+ billingMode: data.bill.billingMode,
1691
+ patient: {
1692
+ firstName: data.patient.firstName || nameParts[0] || "",
1693
+ ...(data.patient.middleName ? { middleName: data.patient.middleName } : {}),
1694
+ lastName: data.patient.lastName || nameParts.slice(1).join(" ") || "",
1695
+ dateOfBirth: data.patient.dob || "",
1696
+ ...(data.patient.phone ? { phone: data.patient.phone } : {}),
1697
+ address: { line1: data.patient.address?.line1 || "", city: data.patient.address?.city || "", state: data.patient.address?.state || "", postalCode: data.patient.address?.postalCode || "" },
1698
+ },
1699
+ claim: {
1700
+ claimNumber: data.injury.claimNumber || "",
1701
+ ...(data.injury.adjNumber ? { adjNumber: data.injury.adjNumber } : {}),
1702
+ employer: data.injury.employer || "",
1703
+ dateOfInjury: data.injury.doi || "",
1704
+ ...(data.injury.injuryDescription ? { description: data.injury.injuryDescription } : {}),
1705
+ claimsAdministrator: { id: data.injury.claimsAdminId || "", name: data.injury.claimsAdminName || "" },
1706
+ },
1707
+ service: { date: data.bill.dos, ...(data.bill.dosEnd !== undefined ? { endDate: data.bill.dosEnd } : {}), ...(data.bill.authorizationNumber !== undefined ? { authorizationNumber: data.bill.authorizationNumber } : {}) },
1708
+ billingProvider: {
1709
+ name: billing?.name || "", taxId: billing?.taxId || "", npi: billing?.npi || "", phone: billing?.phone || "",
1710
+ address: { line1: billing?.billingStreet || "", city: billing?.billingCity || "", state: billing?.billingState || "", postalCode: billing?.billingZip || "" },
1711
+ },
1712
+ renderingProvider: {
1713
+ name: rendering?.name || "", npi: rendering?.npi || "", taxonomy: rendering?.taxonomy || "",
1714
+ ...(rendering?.specialty ? { specialty: rendering.specialty } : {}), ...(rendering?.licenseNumber ? { licenseNumber: rendering.licenseNumber } : {}), ...(rendering?.licenseState ? { licenseState: rendering.licenseState } : {}),
1715
+ ...(rendering?.isQME !== undefined ? { isQme: rendering.isQME } : {}), ...(rendering?.isAME !== undefined ? { isAme: rendering.isAME } : {}),
1716
+ },
1717
+ serviceLocation: {
1718
+ ...(location?.name ? { name: location.name } : {}),
1719
+ address: { line1: location?.street || "", city: location?.city || "", state: location?.state || "", postalCode: location?.zip || "" },
1720
+ placeOfServiceCode: location?.posCode || "",
1721
+ },
1722
+ diagnoses: [...(data.injury.diagnosisCodes || [])],
1723
+ serviceLines: data.bill.lineItems.map((line) => ({ code: line.code, modifiers: [...line.modifiers], units: line.units, charge: line.charge, ...(line.serviceDate ? { serviceDate: line.serviceDate } : {}), ...(line.serviceDateEnd !== undefined ? { serviceDateEnd: line.serviceDateEnd } : {}), ...(line.diagnosisPointers ? { diagnosisPointers: [...line.diagnosisPointers] } : {}) })),
1724
+ };
1725
+ }
1726
+
1727
+ const EMPTY_PROCEDURE_LINE = {
1728
+ code: "",
1729
+ modifiers: [],
1730
+ units: 1,
1731
+ charge: 0,
1732
+ };
1733
+ function isEmptyProcedureLine(line) {
1734
+ return !line.code.trim()
1735
+ && line.modifiers.length === 0
1736
+ && line.units === 1
1737
+ && !line.serviceDate
1738
+ && !line.serviceDateEnd
1739
+ && !(line.diagnosisPointers?.length)
1740
+ && !line.charge;
1741
+ }
1742
+ /** Keeps completed/partial lines plus one keyboard-ready empty row. */
1743
+ function ensureTrailingProcedureLine(lines) {
1744
+ const entered = lines
1745
+ .filter((line) => !isEmptyProcedureLine(line))
1746
+ .map((line) => ({ ...line, modifiers: [...line.modifiers] }));
1747
+ return entered.length < 50
1748
+ ? [...entered, { ...EMPTY_PROCEDURE_LINE, modifiers: [] }]
1749
+ : entered;
1750
+ }
1751
+
1752
+ const THEME$3 = {
1753
+ mindbill: { a: "#238dbd", ac: "#fff", bg: "#f3f8fa", s: "#fff", t: "#203743", m: "#657982", b: "#dbe6ea", r: "14px", cr: "8px", font: "Inter,system-ui,sans-serif" },
1754
+ "qme-companion": { a: "#53b5dc", ac: "#173542", bg: "#f2f8fb", s: "#fff", t: "#1d3440", m: "#617783", b: "#d7e5eb", r: "12px", cr: "8px", font: "Inter,system-ui,sans-serif" },
1755
+ "orange-bright": { a: "#f4510b", ac: "#fff", bg: "#fffaf6", s: "#fffefd", t: "#090f1f", m: "#626a73", b: "#e7e1da", r: "16px", cr: "10px", font: "Inter,system-ui,sans-serif" },
1756
+ "clinical-blue": { a: "#1677ff", ac: "#fff", bg: "#f5f7fa", s: "#fff", t: "#1f2d3d", m: "#66788a", b: "#d9e2ec", r: "8px", cr: "6px", font: "Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif" },
1757
+ };
1758
+ function mindBillAgingDays(bill, now = new Date()) {
1759
+ if (bill.agingDays != null)
1760
+ return Math.max(0, Math.floor(bill.agingDays));
1761
+ const source = bill.submittedAt ?? bill.updatedAt;
1762
+ if (!source)
1763
+ return 0;
1764
+ const time = new Date(source).getTime();
1765
+ return Number.isFinite(time) ? Math.max(0, Math.floor((now.getTime() - time) / 86_400_000)) : 0;
1766
+ }
1767
+ function mindBillAgingBucket(days) {
1768
+ if (days <= 30)
1769
+ return "current";
1770
+ if (days <= 60)
1771
+ return "31-60";
1772
+ if (days <= 90)
1773
+ return "61-90";
1774
+ return "91+";
1775
+ }
1776
+ function summarizeMindBillDashboard(bills, now = new Date()) {
1777
+ const aging = [
1778
+ { id: "current", label: "0–30 days", count: 0, balance: 0 },
1779
+ { id: "31-60", label: "31–60 days", count: 0, balance: 0 },
1780
+ { id: "61-90", label: "61–90 days", count: 0, balance: 0 },
1781
+ { id: "91+", label: "91+ days", count: 0, balance: 0 },
1782
+ ];
1783
+ for (const bill of bills) {
1784
+ const bucket = aging.find((item) => item.id === mindBillAgingBucket(mindBillAgingDays(bill, now)));
1785
+ bucket.count += 1;
1786
+ bucket.balance += bill.balanceDue;
1787
+ }
1788
+ return {
1789
+ totalBilled: bills.reduce((sum, item) => sum + item.totalCharge, 0),
1790
+ totalPaid: bills.reduce((sum, item) => sum + item.totalPaid, 0),
1791
+ outstanding: bills.reduce((sum, item) => sum + item.balanceDue, 0),
1792
+ openCount: bills.filter((item) => !/closed|paid|written.off/i.test(item.state)).length,
1793
+ submittedThisMonth: bills.filter((item) => sameMonth(item.submittedAt, now)).length,
1794
+ closedThisMonth: bills.filter((item) => /closed|paid|written.off/i.test(item.state) && sameMonth(item.updatedAt ?? item.submittedAt, now)).length,
1795
+ bills,
1796
+ aging,
1797
+ };
1798
+ }
1799
+ function sameMonth(value, now) {
1800
+ if (!value)
1801
+ return false;
1802
+ const date = new Date(value);
1803
+ return Number.isFinite(date.getTime()) && date.getUTCFullYear() === now.getUTCFullYear() && date.getUTCMonth() === now.getUTCMonth();
1804
+ }
1805
+ function buildMindBillReportRows(bills, dimension) {
1806
+ const rows = new Map();
1807
+ for (const bill of bills) {
1808
+ const key = dimension === "payer" ? bill.payerName || "Unassigned payer" : dimension === "aging" ? mindBillAgingBucket(mindBillAgingDays(bill)) : bill.state;
1809
+ const label = key.replace(/_/g, " ").replace(/\b\w/g, (value) => value.toUpperCase());
1810
+ const row = rows.get(key) ?? { key, label, billCount: 0, totalBilled: 0, totalPaid: 0, balanceDue: 0 };
1811
+ row.billCount += 1;
1812
+ row.totalBilled += bill.totalCharge;
1813
+ row.totalPaid += bill.totalPaid;
1814
+ row.balanceDue += bill.balanceDue;
1815
+ rows.set(key, row);
1816
+ }
1817
+ return [...rows.values()].sort((a, b) => b.balanceDue - a.balanceDue || a.label.localeCompare(b.label));
1818
+ }
1819
+ function buildMindBillReportCsv(rows) {
1820
+ const quote = (value) => `"${String(value).replace(/"/g, '""')}"`;
1821
+ return ["Group,Bills,Total billed,Total paid,Balance due", ...rows.map((row) => [row.label, row.billCount, row.totalBilled, row.totalPaid, row.balanceDue].map(quote).join(","))].join("\n");
1822
+ }
1823
+ class DashboardBase {
1824
+ appearance = { preset: "mindbill" };
1825
+ get themeStyle() {
1826
+ const base = THEME$3[this.appearance.preset ?? "mindbill"] ?? THEME$3["mindbill"];
1827
+ return {
1828
+ "--a": this.appearance.accentColor ?? base["a"], "--ac": this.appearance.accentTextColor ?? base["ac"],
1829
+ "--bg": this.appearance.backgroundColor ?? base["bg"], "--s": this.appearance.surfaceColor ?? base["s"],
1830
+ "--t": this.appearance.textColor ?? base["t"], "--m": this.appearance.mutedColor ?? base["m"],
1831
+ "--b": this.appearance.borderColor ?? base["b"], "--r": this.appearance.borderRadius ?? base["r"],
1832
+ "--cr": this.appearance.controlRadius ?? base["cr"], "--font": this.appearance.fontFamily ?? base["font"],
1833
+ };
1834
+ }
1835
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DashboardBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1836
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.22", type: DashboardBase, isStandalone: true, inputs: { appearance: "appearance" }, ngImport: i0 });
1837
+ }
1838
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DashboardBase, decorators: [{
1839
+ type: Directive
1840
+ }], propDecorators: { appearance: [{
1200
1841
  type: Input
1201
- }], ariaLabel: [{
1842
+ }] } });
1843
+ const sharedStyles = `
1844
+ :host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}
1845
+ .money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}
1846
+ `;
1847
+ class MindBillBillAgingSummaryComponent extends DashboardBase {
1848
+ buckets = [];
1849
+ selectedBucket = "";
1850
+ bucketSelected = new EventEmitter();
1851
+ width(balance) { const max = Math.max(1, ...this.buckets.map((item) => item.balance)); return Math.max(balance ? 8 : 0, balance / max * 100); }
1852
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillAgingSummaryComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
1853
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillAgingSummaryComponent, isStandalone: true, selector: "mindbill-bill-aging-summary", inputs: { buckets: "buckets", selectedBucket: "selectedBucket" }, outputs: { bucketSelected: "bucketSelected" }, usesInheritance: true, ngImport: i0, template: `<section class="mbd aging" [ngStyle]="themeStyle" aria-label="Accounts receivable aging">@for (bucket of buckets; track bucket.id) { <button type="button" class="card" [class.selected]="selectedBucket === bucket.id" (click)="bucketSelected.emit(bucket.id)"><div><strong>{{ bucket.label }}</strong><span>{{ bucket.count }} bill{{ bucket.count === 1 ? '' : 's' }}</span></div><b class="money">{{ bucket.balance | currency }}</b><i><em [style.width.%]="width(bucket.balance)"></em></i></button> }</section>`, isInline: true, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".aging{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.aging>button{border:1px solid var(--b);padding:16px;text-align:left;color:var(--t);cursor:pointer}.aging>button:hover,.aging>button.selected{border-color:var(--a);background:color-mix(in srgb,var(--a) 6%,var(--s))}.aging button>div{display:flex;justify-content:space-between;gap:8px}.aging span{color:var(--m);font-size:12px}.aging b{display:block;font-size:20px;margin:14px 0 12px}.aging i{display:block;height:5px;border-radius:99px;background:color-mix(in srgb,var(--b) 70%,transparent);overflow:hidden}.aging em{display:block;height:100%;background:var(--a)}@media(max-width:760px){.aging{grid-template-columns:1fr 1fr}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
1854
+ }
1855
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillAgingSummaryComponent, decorators: [{
1856
+ type: Component,
1857
+ args: [{ selector: "mindbill-bill-aging-summary", standalone: true, imports: [CommonModule], template: `<section class="mbd aging" [ngStyle]="themeStyle" aria-label="Accounts receivable aging">@for (bucket of buckets; track bucket.id) { <button type="button" class="card" [class.selected]="selectedBucket === bucket.id" (click)="bucketSelected.emit(bucket.id)"><div><strong>{{ bucket.label }}</strong><span>{{ bucket.count }} bill{{ bucket.count === 1 ? '' : 's' }}</span></div><b class="money">{{ bucket.balance | currency }}</b><i><em [style.width.%]="width(bucket.balance)"></em></i></button> }</section>`, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".aging{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.aging>button{border:1px solid var(--b);padding:16px;text-align:left;color:var(--t);cursor:pointer}.aging>button:hover,.aging>button.selected{border-color:var(--a);background:color-mix(in srgb,var(--a) 6%,var(--s))}.aging button>div{display:flex;justify-content:space-between;gap:8px}.aging span{color:var(--m);font-size:12px}.aging b{display:block;font-size:20px;margin:14px 0 12px}.aging i{display:block;height:5px;border-radius:99px;background:color-mix(in srgb,var(--b) 70%,transparent);overflow:hidden}.aging em{display:block;height:100%;background:var(--a)}@media(max-width:760px){.aging{grid-template-columns:1fr 1fr}}\n"] }]
1858
+ }], propDecorators: { buckets: [{
1202
1859
  type: Input
1203
- }], createOption: [{
1860
+ }], selectedBucket: [{
1204
1861
  type: Input
1205
- }], opened: [{
1206
- type: Output
1207
- }], queryChange: [{
1208
- type: Output
1209
- }], endReached: [{
1862
+ }], bucketSelected: [{
1210
1863
  type: Output
1211
- }], selected: [{
1864
+ }] } });
1865
+ class MindBillBillListComponent extends DashboardBase {
1866
+ bills = [];
1867
+ billSelected = new EventEmitter();
1868
+ age(bill) { return mindBillAgingDays(bill); }
1869
+ label(value) { return value.replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); }
1870
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillListComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
1871
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillListComponent, isStandalone: true, selector: "mindbill-bill-list", inputs: { bills: "bills" }, outputs: { billSelected: "billSelected" }, usesInheritance: true, ngImport: i0, template: `<section class="mbd list card" [ngStyle]="themeStyle"><div class="head"><strong>Bill</strong><strong>Payer</strong><strong>Status</strong><strong>Age</strong><strong>Balance</strong></div>@for (bill of bills; track bill.id) { <button type="button" (click)="billSelected.emit(bill)"><span><b>{{ bill.patientName }}</b><small>{{ bill.claimNumber || bill.workItemLabel || bill.externalId || bill.id }}</small></span><span>{{ bill.payerName || '—' }}</span><span><i class="pill">{{ label(bill.state) }}</i></span><span>{{ age(bill) }} days</span><span class="money"><b>{{ bill.balanceDue | currency }}</b><small>{{ bill.totalPaid | currency }} paid</small></span></button> } @empty { <p class="empty">No bills match these filters.</p> }</section>`, isInline: true, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".list{overflow:hidden}.head,.list button{display:grid;grid-template-columns:1.6fr 1.2fr .85fr .55fr .8fr;gap:18px;align-items:center;text-align:left}.head{padding:11px 16px;background:color-mix(in srgb,var(--bg) 78%,var(--s));color:var(--m);font-size:11px;letter-spacing:.08em;text-transform:uppercase}.list button{width:100%;border:0;border-top:1px solid var(--b);background:var(--s);color:var(--t);padding:15px 16px;cursor:pointer}.list button:hover{background:color-mix(in srgb,var(--a) 5%,var(--s))}.list span,.list small{min-width:0}.list small{display:block;color:var(--m);margin-top:4px}.money{text-align:right}.empty{padding:24px;text-align:center;color:var(--m)}@media(max-width:760px){.head{display:none}.list button{grid-template-columns:1fr auto;gap:10px}.list button>span:nth-child(2){grid-column:1}.list button>span:nth-child(3){grid-row:1;grid-column:2}.list button>span:nth-child(4){grid-column:1}.money{grid-column:2;grid-row:2/4}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
1872
+ }
1873
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillListComponent, decorators: [{
1874
+ type: Component,
1875
+ args: [{ selector: "mindbill-bill-list", standalone: true, imports: [CommonModule], template: `<section class="mbd list card" [ngStyle]="themeStyle"><div class="head"><strong>Bill</strong><strong>Payer</strong><strong>Status</strong><strong>Age</strong><strong>Balance</strong></div>@for (bill of bills; track bill.id) { <button type="button" (click)="billSelected.emit(bill)"><span><b>{{ bill.patientName }}</b><small>{{ bill.claimNumber || bill.workItemLabel || bill.externalId || bill.id }}</small></span><span>{{ bill.payerName || '—' }}</span><span><i class="pill">{{ label(bill.state) }}</i></span><span>{{ age(bill) }} days</span><span class="money"><b>{{ bill.balanceDue | currency }}</b><small>{{ bill.totalPaid | currency }} paid</small></span></button> } @empty { <p class="empty">No bills match these filters.</p> }</section>`, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".list{overflow:hidden}.head,.list button{display:grid;grid-template-columns:1.6fr 1.2fr .85fr .55fr .8fr;gap:18px;align-items:center;text-align:left}.head{padding:11px 16px;background:color-mix(in srgb,var(--bg) 78%,var(--s));color:var(--m);font-size:11px;letter-spacing:.08em;text-transform:uppercase}.list button{width:100%;border:0;border-top:1px solid var(--b);background:var(--s);color:var(--t);padding:15px 16px;cursor:pointer}.list button:hover{background:color-mix(in srgb,var(--a) 5%,var(--s))}.list span,.list small{min-width:0}.list small{display:block;color:var(--m);margin-top:4px}.money{text-align:right}.empty{padding:24px;text-align:center;color:var(--m)}@media(max-width:760px){.head{display:none}.list button{grid-template-columns:1fr auto;gap:10px}.list button>span:nth-child(2){grid-column:1}.list button>span:nth-child(3){grid-row:1;grid-column:2}.list button>span:nth-child(4){grid-column:1}.money{grid-column:2;grid-row:2/4}}\n"] }]
1876
+ }], propDecorators: { bills: [{
1877
+ type: Input
1878
+ }], billSelected: [{
1212
1879
  type: Output
1213
1880
  }] } });
1214
- /** MM/DD/YYYY masked date input. Emits only parseable ISO dates (or "" when
1215
- * cleared) and reformats the display once a complete date round-trips, exactly
1216
- * like the React package's TextDateInput. */
1217
- class MindBillDateInputComponent {
1218
- value;
1219
- disabled = false;
1220
- ariaLabel = "";
1221
- valueChange = new EventEmitter();
1222
- display = "";
1223
- invalid = false;
1224
- ngOnChanges(changes) {
1225
- if (changes["value"]) {
1226
- this.display = formatMindBillSubmissionDate(this.value);
1227
- this.invalid = false;
1228
- }
1229
- }
1230
- onInput(event) {
1231
- const next = event.target.value;
1232
- const parsed = parseMindBillSubmissionDate(next);
1233
- this.display = next;
1234
- this.invalid = Boolean(next && !parsed);
1235
- if (!next.trim())
1236
- this.valueChange.emit("");
1237
- else if (parsed)
1238
- this.valueChange.emit(parsed);
1239
- }
1240
- onBlur() {
1241
- this.invalid = Boolean(this.display && !parseMindBillSubmissionDate(this.display));
1242
- }
1243
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillDateInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1244
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillDateInputComponent, isStandalone: true, selector: "mindbill-date-input", inputs: { value: "value", disabled: "disabled", ariaLabel: "ariaLabel" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
1245
- <input
1246
- type="text"
1247
- inputmode="numeric"
1248
- autocomplete="off"
1249
- placeholder="MM/DD/YYYY"
1250
- [attr.aria-label]="ariaLabel"
1251
- [attr.aria-invalid]="invalid || null"
1252
- [disabled]="disabled"
1253
- [value]="display"
1254
- (input)="onInput($event)"
1255
- (blur)="onBlur()"
1256
- />
1257
- @if (invalid) { <small>Use MM/DD/YYYY</small> }
1258
- `, isInline: true, styles: [":host{display:block}input{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}input[aria-invalid=true]{border-color:#c83c3c}small{display:block;margin-top:6px;color:#c83c3c;font-weight:450}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
1881
+ class MindBillBillingDashboardComponent extends DashboardBase {
1882
+ bills = [];
1883
+ heading = "Billing dashboard";
1884
+ description = "Track every submitted bill from payer acceptance through payment.";
1885
+ billSelected = new EventEmitter();
1886
+ createBill = new EventEmitter();
1887
+ query = "";
1888
+ state = "";
1889
+ agingBucket = "";
1890
+ get summary() { return summarizeMindBillDashboard(this.bills); }
1891
+ get states() { return [...new Set(this.bills.map((bill) => bill.state))].sort(); }
1892
+ get filteredBills() { const query = this.query.trim().toLowerCase(); return this.bills.filter((bill) => (!this.state || bill.state === this.state) && (!this.agingBucket || mindBillAgingBucket(mindBillAgingDays(bill)) === this.agingBucket) && (!query || [bill.patientName, bill.claimNumber, bill.payerName, bill.externalId, bill.billNumber].some((value) => String(value ?? "").toLowerCase().includes(query)))); }
1893
+ get agingLabel() { return this.summary.aging.find((item) => item.id === this.agingBucket)?.label ?? ""; }
1894
+ toggleAging(bucket) { this.agingBucket = this.agingBucket === bucket ? "" : bucket; }
1895
+ label(value) { return value.replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); }
1896
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingDashboardComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
1897
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillingDashboardComponent, isStandalone: true, selector: "mindbill-billing-dashboard", inputs: { bills: "bills", heading: "heading", description: "description" }, outputs: { billSelected: "billSelected", createBill: "createBill" }, usesInheritance: true, ngImport: i0, template: `<section class="mbd dashboard" [ngStyle]="themeStyle"><header><div><p>Billing operations</p><h2>{{ heading }}</h2><span class="muted">{{ description }}</span></div><button type="button" (click)="createBill.emit()">+ New bill</button></header><div class="metrics"><article class="card"><span>Total billed</span><b>{{ summary.totalBilled | currency }}</b></article><article class="card"><span>Outstanding</span><b>{{ summary.outstanding | currency }}</b></article><article class="card"><span>Submitted this month</span><b>{{ summary.submittedThisMonth }}</b></article><article class="card"><span>Closed this month</span><b>{{ summary.closedThisMonth }}</b></article></div><mindbill-bill-aging-summary [buckets]="summary.aging" [selectedBucket]="agingBucket" [appearance]="appearance" (bucketSelected)="toggleAging($event)"/><div class="filters"><input [(ngModel)]="query" placeholder="Search patient, claim, or payer" aria-label="Search bills"><select [(ngModel)]="state"><option value="">All statuses</option>@for (option of states; track option) { <option [value]="option">{{ label(option) }}</option> }</select>@if (agingBucket) { <button type="button" class="clear" (click)="agingBucket=''">{{ agingLabel }} ×</button> }</div><mindbill-bill-list [bills]="filteredBills" [appearance]="appearance" (billSelected)="billSelected.emit($event)"/></section>`, isInline: true, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".dashboard{display:grid;gap:18px}.dashboard header{display:flex;justify-content:space-between;align-items:end;gap:20px}.dashboard h2{font-size:28px;margin:2px 0 5px}.dashboard header p{margin:0;color:var(--a);font-size:12px;font-weight:800;letter-spacing:.12em;text-transform:uppercase}.dashboard header button{border:0;border-radius:var(--cr);background:var(--a);color:var(--ac);padding:11px 16px;font-weight:750;cursor:pointer}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.metrics article{padding:18px}.metrics span{display:block;color:var(--m);font-size:12px}.metrics b{display:block;margin-top:8px;font-size:24px}.filters{display:grid;grid-template-columns:1fr 190px auto;gap:10px}.filters input,.filters select,.filters .clear{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);color:var(--t);padding:11px 13px}.filters .clear{color:var(--a);font-weight:700;cursor:pointer}@media(max-width:760px){.dashboard header{align-items:start}.metrics{grid-template-columns:1fr 1fr}.filters{grid-template-columns:1fr}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { 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.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.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MindBillBillAgingSummaryComponent, selector: "mindbill-bill-aging-summary", inputs: ["buckets", "selectedBucket"], outputs: ["bucketSelected"] }, { kind: "component", type: MindBillBillListComponent, selector: "mindbill-bill-list", inputs: ["bills"], outputs: ["billSelected"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
1259
1898
  }
1260
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillDateInputComponent, decorators: [{
1899
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingDashboardComponent, decorators: [{
1261
1900
  type: Component,
1262
- args: [{ selector: "mindbill-date-input", standalone: true, imports: [CommonModule], template: `
1263
- <input
1264
- type="text"
1265
- inputmode="numeric"
1266
- autocomplete="off"
1267
- placeholder="MM/DD/YYYY"
1268
- [attr.aria-label]="ariaLabel"
1269
- [attr.aria-invalid]="invalid || null"
1270
- [disabled]="disabled"
1271
- [value]="display"
1272
- (input)="onInput($event)"
1273
- (blur)="onBlur()"
1274
- />
1275
- @if (invalid) { <small>Use MM/DD/YYYY</small> }
1276
- `, styles: [":host{display:block}input{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}input[aria-invalid=true]{border-color:#c83c3c}small{display:block;margin-top:6px;color:#c83c3c;font-weight:450}\n"] }]
1277
- }], propDecorators: { value: [{
1901
+ args: [{ selector: "mindbill-billing-dashboard", standalone: true, imports: [CommonModule, FormsModule, MindBillBillAgingSummaryComponent, MindBillBillListComponent], template: `<section class="mbd dashboard" [ngStyle]="themeStyle"><header><div><p>Billing operations</p><h2>{{ heading }}</h2><span class="muted">{{ description }}</span></div><button type="button" (click)="createBill.emit()">+ New bill</button></header><div class="metrics"><article class="card"><span>Total billed</span><b>{{ summary.totalBilled | currency }}</b></article><article class="card"><span>Outstanding</span><b>{{ summary.outstanding | currency }}</b></article><article class="card"><span>Submitted this month</span><b>{{ summary.submittedThisMonth }}</b></article><article class="card"><span>Closed this month</span><b>{{ summary.closedThisMonth }}</b></article></div><mindbill-bill-aging-summary [buckets]="summary.aging" [selectedBucket]="agingBucket" [appearance]="appearance" (bucketSelected)="toggleAging($event)"/><div class="filters"><input [(ngModel)]="query" placeholder="Search patient, claim, or payer" aria-label="Search bills"><select [(ngModel)]="state"><option value="">All statuses</option>@for (option of states; track option) { <option [value]="option">{{ label(option) }}</option> }</select>@if (agingBucket) { <button type="button" class="clear" (click)="agingBucket=''">{{ agingLabel }} ×</button> }</div><mindbill-bill-list [bills]="filteredBills" [appearance]="appearance" (billSelected)="billSelected.emit($event)"/></section>`, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".dashboard{display:grid;gap:18px}.dashboard header{display:flex;justify-content:space-between;align-items:end;gap:20px}.dashboard h2{font-size:28px;margin:2px 0 5px}.dashboard header p{margin:0;color:var(--a);font-size:12px;font-weight:800;letter-spacing:.12em;text-transform:uppercase}.dashboard header button{border:0;border-radius:var(--cr);background:var(--a);color:var(--ac);padding:11px 16px;font-weight:750;cursor:pointer}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.metrics article{padding:18px}.metrics span{display:block;color:var(--m);font-size:12px}.metrics b{display:block;margin-top:8px;font-size:24px}.filters{display:grid;grid-template-columns:1fr 190px auto;gap:10px}.filters input,.filters select,.filters .clear{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);color:var(--t);padding:11px 13px}.filters .clear{color:var(--a);font-weight:700;cursor:pointer}@media(max-width:760px){.dashboard header{align-items:start}.metrics{grid-template-columns:1fr 1fr}.filters{grid-template-columns:1fr}}\n"] }]
1902
+ }], propDecorators: { bills: [{
1278
1903
  type: Input
1279
- }], disabled: [{
1904
+ }], heading: [{
1280
1905
  type: Input
1281
- }], ariaLabel: [{
1906
+ }], description: [{
1282
1907
  type: Input
1283
- }], valueChange: [{
1908
+ }], billSelected: [{
1909
+ type: Output
1910
+ }], createBill: [{
1284
1911
  type: Output
1285
1912
  }] } });
1913
+ class MindBillBillingReportComponent extends DashboardBase {
1914
+ bills = [];
1915
+ heading = "Billing report";
1916
+ dimension = "status";
1917
+ get rows() { return buildMindBillReportRows(this.bills, this.dimension); }
1918
+ downloadCsv() { const blob = new Blob([buildMindBillReportCsv(this.rows)], { type: "text/csv;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `mindbill-${this.dimension}-report.csv`; link.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); }
1919
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingReportComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
1920
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillingReportComponent, isStandalone: true, selector: "mindbill-billing-report", inputs: { bills: "bills", heading: "heading" }, usesInheritance: true, ngImport: i0, template: `<section class="mbd report card" [ngStyle]="themeStyle"><header><div><h2>{{ heading }}</h2><p>Operational totals grouped for reconciliation and follow-up.</p></div><div><select [(ngModel)]="dimension"><option value="status">Status</option><option value="payer">Payer</option><option value="aging">Aging</option></select><button type="button" (click)="downloadCsv()">Export CSV</button></div></header><div class="thead"><b>Group</b><b>Bills</b><b>Total billed</b><b>Total paid</b><b>Balance due</b></div>@for (row of rows; track row.key) { <div class="row"><strong>{{ row.label }}</strong><span>{{ row.billCount }}</span><span>{{ row.totalBilled | currency }}</span><span>{{ row.totalPaid | currency }}</span><b>{{ row.balanceDue | currency }}</b></div> }</section>`, isInline: true, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".report{overflow:hidden}.report header{display:flex;justify-content:space-between;align-items:center;padding:18px}.report h2{margin:0;font-size:22px}.report p{margin:4px 0 0;color:var(--m)}.report header div:last-child{display:flex;gap:8px}.report select,.report button{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);color:var(--t);padding:9px 12px}.report button{background:var(--a);border-color:var(--a);color:var(--ac);font-weight:750;cursor:pointer}.thead,.row{display:grid;grid-template-columns:1.5fr .5fr 1fr 1fr 1fr;gap:14px;padding:12px 18px;border-top:1px solid var(--b);align-items:center}.thead{background:color-mix(in srgb,var(--bg) 78%,var(--s));color:var(--m);font-size:11px;text-transform:uppercase}.row span,.row b:not(:first-child){text-align:right;font-variant-numeric:tabular-nums}@media(max-width:700px){.report header{align-items:flex-start}.thead{display:none}.row{grid-template-columns:1fr 1fr}.row>*:nth-child(n+2){text-align:right}.row strong{grid-column:1}.row span:nth-child(2){grid-column:2}.row span:nth-child(3):before{content:\"Billed \";color:var(--m)}.row span:nth-child(4):before{content:\"Paid \";color:var(--m)}.row b:before{content:\"Due \";color:var(--m)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { 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.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
1921
+ }
1922
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingReportComponent, decorators: [{
1923
+ type: Component,
1924
+ args: [{ selector: "mindbill-billing-report", standalone: true, imports: [CommonModule, FormsModule], template: `<section class="mbd report card" [ngStyle]="themeStyle"><header><div><h2>{{ heading }}</h2><p>Operational totals grouped for reconciliation and follow-up.</p></div><div><select [(ngModel)]="dimension"><option value="status">Status</option><option value="payer">Payer</option><option value="aging">Aging</option></select><button type="button" (click)="downloadCsv()">Export CSV</button></div></header><div class="thead"><b>Group</b><b>Bills</b><b>Total billed</b><b>Total paid</b><b>Balance due</b></div>@for (row of rows; track row.key) { <div class="row"><strong>{{ row.label }}</strong><span>{{ row.billCount }}</span><span>{{ row.totalBilled | currency }}</span><span>{{ row.totalPaid | currency }}</span><b>{{ row.balanceDue | currency }}</b></div> }</section>`, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".report{overflow:hidden}.report header{display:flex;justify-content:space-between;align-items:center;padding:18px}.report h2{margin:0;font-size:22px}.report p{margin:4px 0 0;color:var(--m)}.report header div:last-child{display:flex;gap:8px}.report select,.report button{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);color:var(--t);padding:9px 12px}.report button{background:var(--a);border-color:var(--a);color:var(--ac);font-weight:750;cursor:pointer}.thead,.row{display:grid;grid-template-columns:1.5fr .5fr 1fr 1fr 1fr;gap:14px;padding:12px 18px;border-top:1px solid var(--b);align-items:center}.thead{background:color-mix(in srgb,var(--bg) 78%,var(--s));color:var(--m);font-size:11px;text-transform:uppercase}.row span,.row b:not(:first-child){text-align:right;font-variant-numeric:tabular-nums}@media(max-width:700px){.report header{align-items:flex-start}.thead{display:none}.row{grid-template-columns:1fr 1fr}.row>*:nth-child(n+2){text-align:right}.row strong{grid-column:1}.row span:nth-child(2){grid-column:2}.row span:nth-child(3):before{content:\"Billed \";color:var(--m)}.row span:nth-child(4):before{content:\"Paid \";color:var(--m)}.row b:before{content:\"Due \";color:var(--m)}}\n"] }]
1925
+ }], propDecorators: { bills: [{
1926
+ type: Input
1927
+ }], heading: [{
1928
+ type: Input
1929
+ }] } });
1286
1930
 
1287
- const MAX_PDF_BYTES = 25 * 1024 * 1024;
1288
- const MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
1289
- const MAX_DOCUMENTS = 20;
1290
- const PROCEDURES = [
1291
- { code: "ML200", description: "Missed appointment for a medical-legal evaluation", allowedAmount: 503.75 },
1292
- { code: "ML201", description: "Comprehensive medical-legal evaluation", allowedAmount: 2015 },
1293
- { code: "ML202", description: "Follow-up medical-legal evaluation", allowedAmount: 1316.25 },
1294
- { code: "ML203", description: "Supplemental medical-legal evaluation", allowedAmount: 650 },
1295
- { code: "ML204", description: "Medical-legal testimony", allowedAmount: 113.75 },
1296
- { code: "ML205", description: "Review of sub rosa recordings", allowedAmount: 81.25 },
1297
- { code: "MLPRR", description: "Medical-legal record review", allowedAmount: 3 },
1298
- { code: "90791", description: "Psychiatric diagnostic evaluation" },
1299
- { code: "90792", description: "Psychiatric diagnostic evaluation with medical services" },
1300
- { code: "90832", description: "Psychotherapy, 30 minutes" },
1301
- { code: "90834", description: "Psychotherapy, 45 minutes" },
1302
- { code: "90837", description: "Psychotherapy, 60 minutes" },
1303
- { code: "99203", description: "New patient office visit, level 3" },
1304
- { code: "99204", description: "New patient office visit, level 4" },
1305
- { code: "99205", description: "New patient office visit, level 5", allowedAmount: 349.48 },
1306
- { code: "99213", description: "Established patient office visit, level 3" },
1307
- { code: "99214", description: "Established patient office visit, level 4" },
1308
- { code: "99215", description: "Established patient office visit, level 5" },
1309
- { code: "96130", description: "Psychological testing evaluation, first hour" },
1310
- { code: "96131", description: "Psychological testing evaluation, each additional hour", allowedAmount: 131.96 },
1311
- { code: "97161", description: "Physical therapy evaluation, low complexity" },
1312
- { code: "97162", description: "Physical therapy evaluation, moderate complexity" },
1313
- { code: "97163", description: "Physical therapy evaluation, high complexity" },
1314
- { code: "97110", description: "Therapeutic exercises" },
1315
- { code: "97140", description: "Manual therapy techniques" },
1316
- { code: "98940", description: "Chiropractic manipulative treatment, 1–2 regions" },
1317
- { code: "98941", description: "Chiropractic manipulative treatment, 3–4 regions" },
1318
- { code: "72141", description: "MRI cervical spine without contrast" },
1319
- { code: "72148", description: "MRI lumbar spine without contrast" },
1320
- { code: "73721", description: "MRI lower-extremity joint without contrast" },
1321
- { code: "20610", description: "Major joint injection or aspiration" },
1322
- { code: "99070", description: "Supplies and materials" },
1323
- ];
1324
- const MODIFIERS = [
1325
- { code: "92", description: "Primary Treating Physician evaluation" },
1326
- { code: "93", description: "Interpreter required" },
1327
- { code: "94", description: "Agreed Medical Evaluator" },
1328
- { code: "95", description: "Qualified Medical Evaluator" },
1329
- { code: "96", description: "Psychiatric/psychological evaluation" },
1330
- { code: "97", description: "Toxicology evaluation" },
1331
- { code: "98", description: "Oncology evaluation" },
1332
- { code: "59", description: "Distinct procedural service" },
1333
- { code: "XE", description: "Separate encounter" },
1334
- { code: "XS", description: "Separate structure" },
1335
- { code: "XP", description: "Separate practitioner" },
1336
- { code: "XU", description: "Unusual non-overlapping service" },
1337
- ];
1338
- const TAXONOMIES = [
1339
- { code: "103G00000X", description: "Clinical Neuropsychologist" },
1340
- { code: "103T00000X", description: "Psychologist" },
1341
- { code: "111N00000X", description: "Chiropractor" },
1342
- { code: "207Q00000X", description: "Family Medicine" },
1343
- { code: "207R00000X", description: "Internal Medicine" },
1344
- { code: "207X00000X", description: "Orthopaedic Surgery" },
1345
- { code: "207XS0117X", description: "Orthopaedic Surgery of the Spine" },
1346
- { code: "208100000X", description: "Physical Medicine & Rehabilitation" },
1347
- { code: "2083X0100X", description: "Occupational Medicine" },
1348
- { code: "2084N0400X", description: "Neurology" },
1349
- { code: "2084P0800X", description: "Psychiatry" },
1350
- { code: "208VP0000X", description: "Pain Medicine" },
1351
- { code: "213E00000X", description: "Podiatrist" },
1352
- { code: "225100000X", description: "Physical Therapist" },
1353
- { code: "225X00000X", description: "Occupational Therapist" },
1354
- { code: "363A00000X", description: "Physician Assistant" },
1355
- { code: "363L00000X", description: "Nurse Practitioner" },
1931
+ const MINDBILL_STATUS_AGING_BUCKETS = [
1932
+ { id: "current", label: "0–30 days" },
1933
+ { id: "31-60", label: "31–60 days" },
1934
+ { id: "61-90", label: "61–90 days" },
1935
+ { id: "91+", label: "91+ days" },
1356
1936
  ];
1357
- const QUICK_DIAGNOSES = [
1358
- { label: "Psych", code: "Z04.6", description: "General psychiatric examination requested by authority" },
1359
- { label: "Back", code: "M54.50", description: "Low back pain, unspecified" },
1360
- { label: "Neck", code: "M54.2", description: "Cervicalgia" },
1361
- { label: "Left hand", code: "M79.642", description: "Pain in left hand" },
1362
- { label: "Right hand", code: "M79.641", description: "Pain in right hand" },
1363
- { label: "Left knee", code: "M25.562", description: "Pain in left knee" },
1364
- { label: "Right knee", code: "M25.561", description: "Pain in right knee" },
1937
+ const DEFAULT_STATE_ORDER = [
1938
+ "draft", "incomplete", "created", "sent", "submitted", "accepted", "processed",
1939
+ "paid", "underpaid", "denied", "rejected", "appealing", "second_review", "ibr", "lien", "closed",
1365
1940
  ];
1941
+ const agingDays = (bill, now) => {
1942
+ if (bill.agingDays != null)
1943
+ return Math.max(0, Math.floor(bill.agingDays));
1944
+ const source = bill.submittedAt ?? bill.updatedAt;
1945
+ if (!source)
1946
+ return 0;
1947
+ const time = new Date(source).getTime();
1948
+ return Number.isFinite(time) ? Math.max(0, Math.floor((now.getTime() - time) / 86_400_000)) : 0;
1949
+ };
1950
+ const agingBucketIndex = (days) => (days <= 30 ? 0 : days <= 60 ? 1 : days <= 90 ? 2 : 3);
1951
+ const stateLabel = (state) => state.replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
1952
+ const emptyCell = (state, bucket) => ({ state, bucket, count: 0, balance: 0, bills: [] });
1953
+ function buildMindBillStatusAgingMatrix(bills, stateOrder = DEFAULT_STATE_ORDER, now = new Date()) {
1954
+ const orderIndex = new Map(stateOrder.map((state, index) => [state.toLowerCase(), index]));
1955
+ const states = [...new Set(bills.map((bill) => bill.state))].sort((a, b) => {
1956
+ const left = orderIndex.get(a.toLowerCase()) ?? stateOrder.length;
1957
+ const right = orderIndex.get(b.toLowerCase()) ?? stateOrder.length;
1958
+ return left - right || a.localeCompare(b);
1959
+ });
1960
+ const columnTotals = MINDBILL_STATUS_AGING_BUCKETS.map((bucket) => emptyCell("all", bucket.id));
1961
+ const grandTotal = emptyCell("all", "total");
1962
+ const rows = states.map((state) => ({
1963
+ state,
1964
+ label: stateLabel(state),
1965
+ cells: MINDBILL_STATUS_AGING_BUCKETS.map((bucket) => emptyCell(state, bucket.id)),
1966
+ total: emptyCell(state, "total"),
1967
+ }));
1968
+ const rowByState = new Map(rows.map((row) => [row.state, row]));
1969
+ for (const bill of bills) {
1970
+ const row = rowByState.get(bill.state);
1971
+ const bucketIndex = agingBucketIndex(agingDays(bill, now));
1972
+ for (const cell of [row.cells[bucketIndex], row.total, columnTotals[bucketIndex], grandTotal]) {
1973
+ cell.count += 1;
1974
+ cell.balance += bill.balanceDue;
1975
+ cell.bills.push(bill);
1976
+ }
1977
+ }
1978
+ return { rows, columnTotals, grandTotal };
1979
+ }
1980
+ function buildMindBillStatusAgingCsv(bills, stateOrder) {
1981
+ const matrix = buildMindBillStatusAgingMatrix(bills, stateOrder);
1982
+ const quote = (value) => `"${String(value).replace(/"/g, '""')}"`;
1983
+ const header = ["Status", ...MINDBILL_STATUS_AGING_BUCKETS.map((bucket) => bucket.label), "Total"];
1984
+ const lines = matrix.rows.map((row) => [row.label, ...row.cells.map((cell) => cell.count), row.total.count]);
1985
+ lines.push(["Total", ...matrix.columnTotals.map((cell) => cell.count), matrix.grandTotal.count]);
1986
+ return [header, ...lines].map((line) => line.map(quote).join(",")).join("\n");
1987
+ }
1988
+
1366
1989
  const THEME$2 = {
1367
1990
  mindbill: { a: "#238dbd", ac: "#fff", bg: "#f3f8fa", s: "#fff", t: "#203743", m: "#657982", b: "#dbe6ea", r: "14px", cr: "8px", font: "Inter,system-ui,sans-serif" },
1368
1991
  "qme-companion": { a: "#53b5dc", ac: "#173542", bg: "#f2f8fb", s: "#fff", t: "#1d3440", m: "#617783", b: "#d7e5eb", r: "12px", cr: "8px", font: "Inter,system-ui,sans-serif" },
1369
1992
  "orange-bright": { a: "#f4510b", ac: "#fff", bg: "#fffaf6", s: "#fffefd", t: "#090f1f", m: "#626a73", b: "#e7e1da", r: "16px", cr: "10px", font: "Inter,system-ui,sans-serif" },
1370
- "clinical-blue": { a: "#1677ff", ac: "#fff", bg: "#fff", s: "#fff", t: "#1f2d3d", m: "#66788a", b: "#d9e2ec", r: "8px", cr: "6px", font: "Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif" },
1993
+ "clinical-blue": { a: "#1677ff", ac: "#fff", bg: "#f5f7fa", s: "#fff", t: "#1f2d3d", m: "#66788a", b: "#d9e2ec", r: "8px", cr: "6px", font: "Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif" },
1371
1994
  };
1372
- function deepCopyBill(value) {
1373
- return JSON.parse(JSON.stringify(value));
1374
- }
1375
- function required(value) {
1376
- return typeof value === "string" ? value.trim().length > 0 : value != null;
1377
- }
1378
- async function blobToBase64(blob) {
1379
- const buffer = new Uint8Array(await blob.arrayBuffer());
1380
- let binary = "";
1381
- for (const byte of buffer)
1382
- binary += String.fromCharCode(byte);
1383
- return btoa(binary);
1384
- }
1385
- class MindBillBillSubmissionComponent {
1386
- initialBill;
1387
- attachments = [];
1388
- appearance = { preset: "mindbill" };
1389
- sessionEndpoint = "/api/mindbill/session";
1390
- getSession;
1391
- apiBaseUrl;
1392
- heading = "Bill information";
1393
- description = "Review the bill details, add attachments, and submit.";
1394
- submitLabel = "Submit bill";
1395
- procedureOptions = PROCEDURES;
1396
- modifierOptions = MODIFIERS;
1397
- taxonomyOptions = TAXONOMIES;
1398
- submitter;
1399
- submitted = new EventEmitter();
1400
- submissionError = new EventEmitter();
1401
- billChange = new EventEmitter();
1402
- bill = {};
1403
- workingAttachments = [];
1404
- payerQuery = "";
1405
- payerResults = [];
1406
- payerBusy = false;
1407
- diagnosisQuery = "";
1408
- diagnosisResults = [];
1409
- diagnosisOffset = 0;
1410
- diagnosisBusy = false;
1411
- submitting = false;
1412
- errorMessage = "";
1413
- postalStatus = "";
1414
- dragActive = false;
1415
- invalidFields = new Set();
1416
- quickDiagnoses = QUICK_DIAGNOSES;
1417
- // Zoneless-safe rendering: async work (debounced lookups, fetches, file reads)
1418
- // must schedule change detection explicitly for zone-less Angular hosts.
1419
- changeDetector = inject(ChangeDetectorRef);
1420
- customProcedureOption = mindBillCustomProcedureOption;
1421
- customTaxonomyOption = (query) => {
1422
- const code = query.trim().toUpperCase();
1423
- return /^[A-Z0-9]{10}$/.test(code) ? { id: code, label: code, detail: "Use this taxonomy code" } : null;
1424
- };
1425
- searchTimer;
1426
- postalTimer;
1427
- ngOnChanges(changes) {
1428
- if (changes["initialBill"] && this.initialBill) {
1429
- this.bill = deepCopyBill(this.initialBill);
1430
- if (!this.bill.serviceLines.length)
1431
- this.bill.serviceLines = [{ code: "", units: 1 }];
1432
- this.ensureTrailingLine();
1433
- this.payerQuery = this.bill.claim.claimsAdministrator.name ?? "";
1434
- }
1435
- if (changes["attachments"])
1436
- this.workingAttachments = this.attachments.map((item) => ({ ...item }));
1995
+ class MindBillStatusAgingMatrixComponent {
1996
+ bills = [];
1997
+ heading = "Bills by status and age";
1998
+ description = "Every bill grouped by lifecycle status, then by days outstanding.";
1999
+ stateOrder;
2000
+ showBalances = true;
2001
+ appearance = { preset: "mindbill" };
2002
+ cellSelected = new EventEmitter();
2003
+ buckets = MINDBILL_STATUS_AGING_BUCKETS;
2004
+ get matrix() {
2005
+ return buildMindBillStatusAgingMatrix(this.bills, this.stateOrder);
1437
2006
  }
1438
2007
  get themeStyle() {
1439
2008
  const base = THEME$2[this.appearance.preset ?? "mindbill"] ?? THEME$2["mindbill"];
@@ -1445,547 +2014,282 @@ class MindBillBillSubmissionComponent {
1445
2014
  "--cr": this.appearance.controlRadius ?? base["cr"], "--font": this.appearance.fontFamily ?? base["font"],
1446
2015
  };
1447
2016
  }
1448
- get procedureComboOptions() {
1449
- return this.procedureOptions.map((item) => ({ id: item.code, label: item.code, detail: item.description }));
1450
- }
1451
- get taxonomyComboOptions() {
1452
- return this.taxonomyOptions.map((item) => ({ id: item.code, label: item.description, detail: item.code }));
1453
- }
1454
- modifierComboOptions(line) {
1455
- return this.modifierOptions
1456
- .filter((item) => !(line.modifiers ?? []).includes(item.code))
1457
- .map((item) => ({ id: item.code, label: `−${item.code}`, detail: item.description }));
1458
- }
1459
- referenceClient() {
1460
- return createBillReferenceClient({ sessionEndpoint: this.sessionEndpoint, ...(this.getSession ? { getSession: this.getSession } : {}), ...(this.apiBaseUrl ? { apiBaseUrl: this.apiBaseUrl } : {}) });
1461
- }
1462
- setDate(path, value) {
1463
- if (path === "patient.dateOfBirth")
1464
- this.bill.patient.dateOfBirth = value;
1465
- else if (path === "service.date")
1466
- this.bill.service.date = value;
1467
- else
1468
- this.bill.claim.dateOfInjury = value;
1469
- if (value)
1470
- this.invalidFields.delete(path);
1471
- this.billChange.emit(this.bill);
1472
- }
1473
- postalChanged(value) {
1474
- clearTimeout(this.postalTimer);
1475
- this.postalStatus = "";
1476
- if (!/^\d{5}$/.test(value.trim()))
1477
- return;
1478
- this.postalStatus = "Looking up ZIP…";
1479
- this.postalTimer = setTimeout(async () => {
1480
- try {
1481
- const place = await this.referenceClient().lookupPostalCode(value);
1482
- if (place) {
1483
- this.bill.patient.address.city = place.city;
1484
- this.bill.patient.address.state = place.state.toUpperCase();
1485
- this.postalStatus = `${place.city}, ${place.state.toUpperCase()} filled from ZIP`;
1486
- this.invalidFields.delete("patient.address.city");
1487
- this.invalidFields.delete("patient.address.state");
1488
- this.billChange.emit(this.bill);
1489
- }
1490
- else {
1491
- this.postalStatus = "ZIP not found";
2017
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillStatusAgingMatrixComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2018
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillStatusAgingMatrixComponent, isStandalone: true, selector: "mindbill-status-aging-matrix", inputs: { bills: "bills", heading: "heading", description: "description", stateOrder: "stateOrder", showBalances: "showBalances", appearance: "appearance" }, outputs: { cellSelected: "cellSelected" }, ngImport: i0, template: `
2019
+ <section class="mbmx" [ngStyle]="themeStyle" aria-label="Bills by status and age">
2020
+ <header><h2>{{ heading }}</h2><p>{{ description }}</p></header>
2021
+ <div class="card">
2022
+ <table>
2023
+ <thead>
2024
+ <tr><th class="status">Status</th>@for (bucket of buckets; track bucket.id) { <th>{{ bucket.label }}</th> }<th>Total</th></tr>
2025
+ </thead>
2026
+ <tbody>
2027
+ @for (row of matrix.rows; track row.state) {
2028
+ <tr>
2029
+ <td class="status"><i class="pill">{{ row.label }}</i></td>
2030
+ @for (cell of row.cells; track cell.bucket) {
2031
+ <td><button type="button" [disabled]="cell.count === 0" (click)="cellSelected.emit(cell)"><b [class.zero]="cell.count === 0">{{ cell.count === 0 ? '—' : cell.count }}</b>@if (showBalances && cell.count > 0) { <small>{{ cell.balance | currency }}</small> }</button></td>
1492
2032
  }
2033
+ <td class="rowtotal"><button type="button" [disabled]="row.total.count === 0" (click)="cellSelected.emit(row.total)"><b>{{ row.total.count }}</b>@if (showBalances) { <small>{{ row.total.balance | currency }}</small> }</button></td>
2034
+ </tr>
1493
2035
  }
1494
- catch {
1495
- this.postalStatus = "ZIP lookup unavailable";
1496
- }
1497
- this.changeDetector.markForCheck();
1498
- }, 180);
1499
- }
1500
- searchPayers() {
1501
- clearTimeout(this.searchTimer);
1502
- this.searchTimer = setTimeout(async () => {
1503
- this.payerBusy = true;
1504
- try {
1505
- this.payerResults = await this.referenceClient().searchClaimsAdministrators(this.payerQuery, this.bill.claim.claimNumber);
1506
- }
1507
- catch {
1508
- this.payerResults = [];
1509
- }
1510
- finally {
1511
- this.payerBusy = false;
1512
- this.changeDetector.markForCheck();
1513
- }
1514
- }, 180);
1515
- }
1516
- selectPayer(payer) {
1517
- this.bill.claim.claimsAdministrator = { id: payer.id, name: payer.name };
1518
- this.payerQuery = payer.name;
1519
- this.payerResults = [];
1520
- this.invalidFields.delete("claim.claimsAdministrator");
1521
- this.billChange.emit(this.bill);
1522
- }
1523
- searchDiagnoses(reset) {
1524
- if (this.diagnosisBusy)
1525
- return;
1526
- if (reset) {
1527
- this.diagnosisOffset = 0;
1528
- this.diagnosisResults = [];
1529
- }
1530
- clearTimeout(this.searchTimer);
1531
- this.searchTimer = setTimeout(async () => {
1532
- this.diagnosisBusy = true;
1533
- try {
1534
- const results = await this.referenceClient().searchDiagnosisCodes(this.diagnosisQuery, 60, this.diagnosisOffset);
1535
- this.diagnosisResults = reset ? results : [...this.diagnosisResults, ...results];
1536
- this.diagnosisOffset = this.diagnosisResults.length;
1537
- }
1538
- catch {
1539
- if (reset)
1540
- this.diagnosisResults = [];
1541
- }
1542
- finally {
1543
- this.diagnosisBusy = false;
1544
- this.changeDetector.markForCheck();
2036
+ <tr class="totals">
2037
+ <td class="status">Total</td>
2038
+ @for (cell of matrix.columnTotals; track cell.bucket) {
2039
+ <td><button type="button" [disabled]="cell.count === 0" (click)="cellSelected.emit(cell)"><b [class.zero]="cell.count === 0">{{ cell.count === 0 ? '—' : cell.count }}</b>@if (showBalances && cell.count > 0) { <small>{{ cell.balance | currency }}</small> }</button></td>
2040
+ }
2041
+ <td class="rowtotal"><button type="button" [disabled]="matrix.grandTotal.count === 0" (click)="cellSelected.emit(matrix.grandTotal)"><b>{{ matrix.grandTotal.count }}</b>@if (showBalances) { <small>{{ matrix.grandTotal.balance | currency }}</small> }</button></td>
2042
+ </tr>
2043
+ </tbody>
2044
+ </table>
2045
+ </div>
2046
+ </section>
2047
+ `, isInline: true, styles: [":host{display:block}.mbmx{font-family:var(--font);color:var(--t)}.mbmx header{margin-bottom:16px}.mbmx h2{margin:0;font-size:24px}.mbmx header p{margin:5px 0 0;color:var(--m)}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);overflow:auto}table{width:100%;border-collapse:collapse;min-width:640px}th{padding:13px 16px;border-bottom:1px solid var(--b);color:var(--m);font-size:11px;letter-spacing:.06em;text-transform:uppercase;text-align:right;white-space:nowrap}th.status{text-align:left}td{padding:0;border-bottom:1px solid var(--b);border-left:1px solid color-mix(in srgb,var(--b) 55%,transparent);text-align:right}td.status{border-left:0;padding:14px 16px;text-align:left;white-space:nowrap}tr:last-child td{border-bottom:0}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-style:normal;font-weight:750;padding:5px 9px}td button{display:block;width:100%;min-height:56px;padding:9px 16px;border:0;background:none;color:var(--t);font:inherit;text-align:right;cursor:pointer}td button:disabled{cursor:default}td button:not(:disabled):hover{background:color-mix(in srgb,var(--a) 8%,var(--s))}td button b{display:block;font-size:17px;font-weight:760;font-variant-numeric:tabular-nums}td button b.zero{color:var(--m);font-weight:500}td button small{display:block;margin-top:2px;color:var(--m);font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap}td.rowtotal,tr.totals td{background:color-mix(in srgb,var(--bg) 78%,var(--s));font-weight:750}tr.totals td.status{color:var(--t);font-weight:750}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
2048
+ }
2049
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillStatusAgingMatrixComponent, decorators: [{
2050
+ type: Component,
2051
+ args: [{ selector: "mindbill-status-aging-matrix", standalone: true, imports: [CommonModule], template: `
2052
+ <section class="mbmx" [ngStyle]="themeStyle" aria-label="Bills by status and age">
2053
+ <header><h2>{{ heading }}</h2><p>{{ description }}</p></header>
2054
+ <div class="card">
2055
+ <table>
2056
+ <thead>
2057
+ <tr><th class="status">Status</th>@for (bucket of buckets; track bucket.id) { <th>{{ bucket.label }}</th> }<th>Total</th></tr>
2058
+ </thead>
2059
+ <tbody>
2060
+ @for (row of matrix.rows; track row.state) {
2061
+ <tr>
2062
+ <td class="status"><i class="pill">{{ row.label }}</i></td>
2063
+ @for (cell of row.cells; track cell.bucket) {
2064
+ <td><button type="button" [disabled]="cell.count === 0" (click)="cellSelected.emit(cell)"><b [class.zero]="cell.count === 0">{{ cell.count === 0 ? '—' : cell.count }}</b>@if (showBalances && cell.count > 0) { <small>{{ cell.balance | currency }}</small> }</button></td>
2065
+ }
2066
+ <td class="rowtotal"><button type="button" [disabled]="row.total.count === 0" (click)="cellSelected.emit(row.total)"><b>{{ row.total.count }}</b>@if (showBalances) { <small>{{ row.total.balance | currency }}</small> }</button></td>
2067
+ </tr>
1545
2068
  }
1546
- }, reset ? 160 : 0);
1547
- }
1548
- diagnosisScrolled(event) {
1549
- const element = event.currentTarget;
1550
- if (element.scrollTop + element.clientHeight >= element.scrollHeight - 80)
1551
- this.searchDiagnoses(false);
1552
- }
1553
- hasDiagnosis(code) { return this.bill.diagnoses.includes(code); }
1554
- toggleDiagnosis(item) {
1555
- if (this.hasDiagnosis(item.code))
1556
- this.removeDiagnosis(item.code);
1557
- else
1558
- this.addDiagnosis(item);
1559
- }
1560
- addDiagnosis(item) { if (!this.hasDiagnosis(item.code))
1561
- this.bill.diagnoses.push(item.code); this.diagnosisQuery = ""; this.diagnosisResults = []; this.invalidFields.delete("diagnoses"); this.billChange.emit(this.bill); }
1562
- removeDiagnosis(code) { this.bill.diagnoses = this.bill.diagnoses.filter((item) => item !== code); this.billChange.emit(this.bill); }
1563
- ensureTrailingLine() {
1564
- const last = this.bill.serviceLines.at(-1);
1565
- if (!last || last.code.trim())
1566
- this.bill.serviceLines.push({ code: "", units: 1 });
1567
- this.billChange.emit(this.bill);
1568
- }
1569
- removeLine(index) { this.bill.serviceLines.splice(index, 1); if (!this.bill.serviceLines.length)
1570
- this.bill.serviceLines.push({ code: "", units: 1 }); this.ensureTrailingLine(); }
1571
- selectProcedure(index, option) {
1572
- this.bill.serviceLines[index].code = option.id;
1573
- this.invalidFields.delete("serviceLines");
1574
- this.ensureTrailingLine();
1575
- }
1576
- addModifier(index, option) {
1577
- const line = this.bill.serviceLines[index];
1578
- line.modifiers = [...new Set([...(line.modifiers ?? []), option.id])];
1579
- this.billChange.emit(this.bill);
1580
- }
1581
- removeModifier(index, modifier) {
1582
- const line = this.bill.serviceLines[index];
1583
- line.modifiers = (line.modifiers ?? []).filter((item) => item !== modifier);
1584
- this.billChange.emit(this.bill);
1585
- }
1586
- dxLetter(pointer) { return String.fromCharCode(64 + pointer); }
1587
- hasDiagnosisPointer(line, pointer) {
1588
- return (line.diagnosisPointers ?? []).includes(pointer);
1589
- }
1590
- toggleDiagnosisPointer(index, pointer) {
1591
- const line = this.bill.serviceLines[index];
1592
- line.diagnosisPointers = this.hasDiagnosisPointer(line, pointer)
1593
- ? (line.diagnosisPointers ?? []).filter((item) => item !== pointer)
1594
- : [...(line.diagnosisPointers ?? []), pointer].sort((left, right) => left - right);
1595
- this.billChange.emit(this.bill);
1596
- }
1597
- setAuthorizationNumber(value) {
1598
- this.bill.service.authorizationNumber = value.trim() ? value : null;
1599
- this.billChange.emit(this.bill);
1600
- }
1601
- selectTaxonomy(option) {
1602
- this.bill.renderingProvider.taxonomy = option.id;
1603
- this.invalidFields.delete("renderingProvider.taxonomy");
1604
- this.billChange.emit(this.bill);
1605
- }
1606
- procedureDescription(code) { return this.procedureOptions.find((item) => item.code.toLowerCase() === code.trim().toLowerCase())?.description ?? ""; }
1607
- allowed(line) { const base = this.procedureOptions.find((item) => item.code.toLowerCase() === line.code.trim().toLowerCase())?.allowedAmount; return base == null ? (line.charge ?? null) : Math.round(base * Math.max(1, line.units ?? 1) * 100) / 100; }
1608
- get totalAllowed() { return this.bill.serviceLines.reduce((sum, line) => sum + (this.allowed(line) ?? 0), 0); }
1609
- onWindowDragOver(event) {
1610
- if (event.dataTransfer?.types.includes("Files")) {
1611
- event.preventDefault();
1612
- this.dragActive = true;
1613
- this.changeDetector.markForCheck();
1614
- }
1615
- }
1616
- onWindowDragLeave(event) {
1617
- if (!event.relatedTarget) {
1618
- this.dragActive = false;
1619
- this.changeDetector.markForCheck();
1620
- }
1621
- }
1622
- onWindowDrop(event) {
1623
- if (event.dataTransfer?.files.length) {
1624
- event.preventDefault();
1625
- this.dragActive = false;
1626
- void this.addFiles(event.dataTransfer.files);
1627
- this.changeDetector.markForCheck();
1628
- }
2069
+ <tr class="totals">
2070
+ <td class="status">Total</td>
2071
+ @for (cell of matrix.columnTotals; track cell.bucket) {
2072
+ <td><button type="button" [disabled]="cell.count === 0" (click)="cellSelected.emit(cell)"><b [class.zero]="cell.count === 0">{{ cell.count === 0 ? '—' : cell.count }}</b>@if (showBalances && cell.count > 0) { <small>{{ cell.balance | currency }}</small> }</button></td>
2073
+ }
2074
+ <td class="rowtotal"><button type="button" [disabled]="matrix.grandTotal.count === 0" (click)="cellSelected.emit(matrix.grandTotal)"><b>{{ matrix.grandTotal.count }}</b>@if (showBalances) { <small>{{ matrix.grandTotal.balance | currency }}</small> }</button></td>
2075
+ </tr>
2076
+ </tbody>
2077
+ </table>
2078
+ </div>
2079
+ </section>
2080
+ `, styles: [":host{display:block}.mbmx{font-family:var(--font);color:var(--t)}.mbmx header{margin-bottom:16px}.mbmx h2{margin:0;font-size:24px}.mbmx header p{margin:5px 0 0;color:var(--m)}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);overflow:auto}table{width:100%;border-collapse:collapse;min-width:640px}th{padding:13px 16px;border-bottom:1px solid var(--b);color:var(--m);font-size:11px;letter-spacing:.06em;text-transform:uppercase;text-align:right;white-space:nowrap}th.status{text-align:left}td{padding:0;border-bottom:1px solid var(--b);border-left:1px solid color-mix(in srgb,var(--b) 55%,transparent);text-align:right}td.status{border-left:0;padding:14px 16px;text-align:left;white-space:nowrap}tr:last-child td{border-bottom:0}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-style:normal;font-weight:750;padding:5px 9px}td button{display:block;width:100%;min-height:56px;padding:9px 16px;border:0;background:none;color:var(--t);font:inherit;text-align:right;cursor:pointer}td button:disabled{cursor:default}td button:not(:disabled):hover{background:color-mix(in srgb,var(--a) 8%,var(--s))}td button b{display:block;font-size:17px;font-weight:760;font-variant-numeric:tabular-nums}td button b.zero{color:var(--m);font-weight:500}td button small{display:block;margin-top:2px;color:var(--m);font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap}td.rowtotal,tr.totals td{background:color-mix(in srgb,var(--bg) 78%,var(--s));font-weight:750}tr.totals td.status{color:var(--t);font-weight:750}\n"] }]
2081
+ }], propDecorators: { bills: [{
2082
+ type: Input
2083
+ }], heading: [{
2084
+ type: Input
2085
+ }], description: [{
2086
+ type: Input
2087
+ }], stateOrder: [{
2088
+ type: Input
2089
+ }], showBalances: [{
2090
+ type: Input
2091
+ }], appearance: [{
2092
+ type: Input
2093
+ }], cellSelected: [{
2094
+ type: Output
2095
+ }] } });
2096
+
2097
+ const MINDBILL_BILL_TASKS_TONES = {
2098
+ violet: "#7c53c3",
2099
+ red: "#cf4437",
2100
+ blue: "#2f7fd1",
2101
+ green: "#2c9a5b",
2102
+ amber: "#d9931f",
2103
+ neutral: "#8195a1",
2104
+ };
2105
+ const MINDBILL_BILL_TASKS_PILL_BASES = ["#2c9a5b", "#1f9aa8", "#dcbb28", "#e08a2e", "#d75d8a"];
2106
+ function mindBillBillTasksTone(tone) {
2107
+ return MINDBILL_BILL_TASKS_TONES[tone] ?? MINDBILL_BILL_TASKS_TONES.neutral;
2108
+ }
2109
+ function mindBillBillTasksPillBase(index) {
2110
+ return MINDBILL_BILL_TASKS_PILL_BASES[index % MINDBILL_BILL_TASKS_PILL_BASES.length];
2111
+ }
2112
+ /** Click-through payload for a bucket cell (bucketIndex) or a row total (null). */
2113
+ function mindBillBillTasksCell(section, row, bucketIndex, buckets) {
2114
+ if (bucketIndex === null) {
2115
+ return {
2116
+ sectionId: section.id,
2117
+ rowId: row.id,
2118
+ bucketId: null,
2119
+ refs: row.refs.flat(),
2120
+ count: row.total,
2121
+ };
1629
2122
  }
1630
- async addFiles(fileList) {
1631
- const files = Array.from(fileList);
1632
- if (!files.length)
1633
- return;
1634
- this.errorMessage = "";
1635
- const invalid = files.find((file) => file.type !== "application/pdf" && !file.name.toLowerCase().endsWith(".pdf"));
1636
- if (invalid) {
1637
- this.errorMessage = `${invalid.name} is not a PDF.`;
1638
- return;
1639
- }
1640
- const oversized = files.find((file) => file.size > MAX_PDF_BYTES);
1641
- if (oversized) {
1642
- this.errorMessage = `${oversized.name} is larger than 25 MB.`;
1643
- return;
1644
- }
1645
- if (this.workingAttachments.length + files.length > MAX_DOCUMENTS) {
1646
- this.errorMessage = `A bill can include at most ${MAX_DOCUMENTS} attachments.`;
1647
- return;
1648
- }
1649
- const existingBytes = this.workingAttachments.reduce((sum, item) => sum + (item.contentBase64 ? item.contentBase64.length * 0.75 : 0), 0);
1650
- if (existingBytes + files.reduce((sum, file) => sum + file.size, 0) > MAX_UPLOAD_BYTES) {
1651
- this.errorMessage = "Attachments exceed the 100 MB upload limit.";
1652
- return;
1653
- }
1654
- for (const file of files)
1655
- this.workingAttachments.push({ filename: file.name, description: "Supporting document", documentType: "other", contentBase64: await blobToBase64(file), contentUrl: URL.createObjectURL(file) });
1656
- this.changeDetector.markForCheck();
2123
+ return {
2124
+ sectionId: section.id,
2125
+ rowId: row.id,
2126
+ bucketId: buckets[bucketIndex]?.id ?? null,
2127
+ refs: row.refs[bucketIndex] ?? [],
2128
+ count: row.counts[bucketIndex] ?? 0,
2129
+ };
2130
+ }
2131
+
2132
+ // daisyBill-style "Bill Tasks" dashboard: one rounded, tone-colored card per
2133
+ // task section, rows bucketed by age in days with colored bucket header pills,
2134
+ // per-cell click-through counts, and a closing grand-total card. The pure
2135
+ // aggregation lives in @mindbill/browser (buildBillTasksDashboard) so React
2136
+ // and Angular render the exact same data shape.
2137
+ class MindBillBillTasksDashboardComponent {
2138
+ data = null;
2139
+ buckets = BILL_TASKS_AGING_BUCKETS;
2140
+ heading = "";
2141
+ grandTotalLabel = "Bill Tasks Total";
2142
+ emptyLabel = "No Tasks";
2143
+ appearance = { preset: "mindbill" };
2144
+ cellSelected = new EventEmitter();
2145
+ get themeStyle() {
2146
+ return {
2147
+ ...mindBillAngularAppearanceStyle(this.appearance),
2148
+ "--cols": `minmax(150px,1.6fr) repeat(${this.buckets.length},minmax(84px,1fr)) minmax(84px,1fr)`,
2149
+ };
1657
2150
  }
1658
- async filesSelected(event) {
1659
- const input = event.target;
1660
- await this.addFiles(input.files ?? []);
1661
- input.value = "";
2151
+ tone(section) {
2152
+ return mindBillBillTasksTone(section.tone);
1662
2153
  }
1663
- removeAttachment(index) { const item = this.workingAttachments[index]; if (item?.contentUrl?.startsWith("blob:"))
1664
- URL.revokeObjectURL(item.contentUrl); this.workingAttachments.splice(index, 1); }
1665
- previewAttachment(attachment) { if (attachment.contentUrl)
1666
- window.open(attachment.contentUrl, "_blank", "noopener,noreferrer");
1667
- else if (attachment.contentBase64)
1668
- window.open(`data:application/pdf;base64,${attachment.contentBase64}`, "_blank", "noopener,noreferrer"); }
1669
- bad(path) { return this.invalidFields.has(path); }
1670
- validate() {
1671
- const candidates = [
1672
- ["patient.firstName", this.bill.patient.firstName], ["patient.lastName", this.bill.patient.lastName], ["patient.dateOfBirth", parseMindBillSubmissionDate(this.bill.patient.dateOfBirth ?? "")],
1673
- ["patient.address.line1", this.bill.patient.address.line1], ["patient.address.city", this.bill.patient.address.city], ["patient.address.state", this.bill.patient.address.state], ["patient.address.postalCode", this.bill.patient.address.postalCode],
1674
- ["service.date", parseMindBillSubmissionDate(this.bill.service.date ?? "")], ["claim.dateOfInjury", parseMindBillSubmissionDate(this.bill.claim.dateOfInjury ?? "")], ["claim.claimNumber", this.bill.claim.claimNumber], ["claim.employer", this.bill.claim.employer],
1675
- ["claim.claimsAdministrator", this.bill.claim.claimsAdministrator.id], ["diagnoses", this.bill.diagnoses.length],
1676
- ["billingProvider.name", this.bill.billingProvider.name], ["billingProvider.taxId", this.bill.billingProvider.taxId], ["billingProvider.npi", this.bill.billingProvider.npi], ["billingProvider.phone", this.bill.billingProvider.phone],
1677
- ["billingProvider.address.line1", this.bill.billingProvider.address.line1], ["billingProvider.address.city", this.bill.billingProvider.address.city], ["billingProvider.address.state", this.bill.billingProvider.address.state], ["billingProvider.address.postalCode", this.bill.billingProvider.address.postalCode],
1678
- ["renderingProvider.name", this.bill.renderingProvider.name], ["renderingProvider.npi", this.bill.renderingProvider.npi], ["renderingProvider.taxonomy", this.bill.renderingProvider.taxonomy],
1679
- ["serviceLocation.placeOfServiceCode", this.bill.serviceLocation.placeOfServiceCode], ["serviceLocation.address.line1", this.bill.serviceLocation.address.line1], ["serviceLocation.address.city", this.bill.serviceLocation.address.city], ["serviceLocation.address.state", this.bill.serviceLocation.address.state], ["serviceLocation.address.postalCode", this.bill.serviceLocation.address.postalCode],
1680
- ["serviceLines", this.bill.serviceLines.some((line) => required(line.code) && (line.units ?? 1) > 0)],
1681
- ];
1682
- this.invalidFields = new Set(candidates.filter(([, value]) => !value || !required(value)).map(([path]) => path));
1683
- if (this.invalidFields.size) {
1684
- this.errorMessage = `Complete ${this.invalidFields.size} highlighted required field${this.invalidFields.size === 1 ? "" : "s"} before submitting.`;
1685
- setTimeout(() => document.querySelector(".mbs .invalid,.mbs .invalid-set")?.scrollIntoView({ behavior: "smooth", block: "center" }));
1686
- return false;
1687
- }
1688
- return true;
2154
+ pillBackground(index) {
2155
+ return `color-mix(in srgb,${mindBillBillTasksPillBase(index)} 20%,var(--s))`;
1689
2156
  }
1690
- async submissionDocuments() {
1691
- const documents = [];
1692
- for (const attachment of this.workingAttachments) {
1693
- let contentBase64 = attachment.contentBase64;
1694
- if (!contentBase64 && attachment.contentUrl) {
1695
- const response = await fetch(attachment.contentUrl);
1696
- if (!response.ok)
1697
- throw new Error(`Could not load ${attachment.filename}.`);
1698
- contentBase64 = await blobToBase64(await response.blob());
1699
- }
1700
- if (!contentBase64)
1701
- continue;
1702
- documents.push({
1703
- filename: attachment.filename,
1704
- documentType: attachment.documentType,
1705
- contentBase64,
1706
- ...(attachment.externalId ? { externalId: attachment.externalId } : {}),
1707
- ...(attachment.description ? { description: attachment.description } : {}),
1708
- ...(attachment.reportTypeCode ? { reportTypeCode: attachment.reportTypeCode } : {}),
1709
- });
1710
- }
1711
- return documents;
2157
+ pillColor(index) {
2158
+ return `color-mix(in srgb,${mindBillBillTasksPillBase(index)} 62%,var(--t))`;
1712
2159
  }
1713
- async submit(event) {
1714
- event.preventDefault();
1715
- this.errorMessage = "";
1716
- if (!this.validate())
1717
- return;
1718
- this.submitting = true;
1719
- try {
1720
- const bill = deepCopyBill(this.bill);
1721
- bill.patient.dateOfBirth = parseMindBillSubmissionDate(bill.patient.dateOfBirth ?? "") ?? bill.patient.dateOfBirth;
1722
- bill.service.date = parseMindBillSubmissionDate(bill.service.date ?? "") ?? bill.service.date;
1723
- bill.claim.dateOfInjury = parseMindBillSubmissionDate(bill.claim.dateOfInjury ?? "") ?? bill.claim.dateOfInjury;
1724
- bill.serviceLines = bill.serviceLines
1725
- .filter((line) => line.code.trim())
1726
- .map((line) => {
1727
- const charge = line.charge ?? this.allowed(line);
1728
- return charge == null ? line : { ...line, charge };
1729
- });
1730
- const input = { bill, documents: await this.submissionDocuments() };
1731
- const result = this.submitter
1732
- ? await this.submitter(input)
1733
- : await createBillSubmissionClient({ sessionEndpoint: this.sessionEndpoint, ...(this.getSession ? { getSession: this.getSession } : {}), ...(this.apiBaseUrl ? { apiBaseUrl: this.apiBaseUrl } : {}) }).submitBill(input);
1734
- this.submitted.emit(result);
1735
- }
1736
- catch (value) {
1737
- const error = value instanceof Error ? value : new Error("The bill could not be submitted.");
1738
- this.errorMessage = error.message;
1739
- this.submissionError.emit(error);
1740
- }
1741
- finally {
1742
- this.submitting = false;
1743
- this.changeDetector.markForCheck();
1744
- }
2160
+ select(section, row, bucketIndex) {
2161
+ this.cellSelected.emit(mindBillBillTasksCell(section, row, bucketIndex, this.buckets));
1745
2162
  }
1746
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillSubmissionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1747
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillSubmissionComponent, isStandalone: true, selector: "mindbill-bill-submission", inputs: { initialBill: "initialBill", attachments: "attachments", appearance: "appearance", sessionEndpoint: "sessionEndpoint", getSession: "getSession", apiBaseUrl: "apiBaseUrl", heading: "heading", description: "description", submitLabel: "submitLabel", procedureOptions: "procedureOptions", modifierOptions: "modifierOptions", taxonomyOptions: "taxonomyOptions", submitter: "submitter" }, outputs: { submitted: "submitted", submissionError: "submissionError", billChange: "billChange" }, host: { listeners: { "window:dragover": "onWindowDragOver($event)", "window:dragleave": "onWindowDragLeave($event)", "window:drop": "onWindowDrop($event)" } }, usesOnChanges: true, ngImport: i0, template: `
1748
- <form class="mbs" [ngStyle]="themeStyle" (submit)="submit($event)" novalidate>
1749
- <div class="intro"><div><h2>{{ heading }}</h2><p>{{ description }}</p></div><span><b>*</b> Required</span></div>
1750
-
1751
- <fieldset id="mbs-patient"><legend>Patient</legend><div class="grid">
1752
- <label [class.invalid]="bad('patient.firstName')">First name <b>*</b><input name="firstName" [(ngModel)]="bill.patient.firstName"></label>
1753
- <label [class.invalid]="bad('patient.lastName')">Last name <b>*</b><input name="lastName" [(ngModel)]="bill.patient.lastName"></label>
1754
- <label [class.invalid]="bad('patient.dateOfBirth')">Date of birth <b>*</b><mindbill-date-input ariaLabel="Date of birth" [value]="bill.patient.dateOfBirth" (valueChange)="setDate('patient.dateOfBirth', $event)"/></label>
1755
- <label>Phone (optional)<input name="phone" type="tel" [(ngModel)]="bill.patient.phone"></label>
1756
- <label class="wide" [class.invalid]="bad('patient.address.line1')">Address <b>*</b><input name="address" [(ngModel)]="bill.patient.address.line1"></label>
1757
- <label [class.invalid]="bad('patient.address.postalCode')">ZIP <b>*</b><input name="zip" [(ngModel)]="bill.patient.address.postalCode" (ngModelChange)="postalChanged($event)" inputmode="numeric">@if (postalStatus) { <small>{{ postalStatus }}</small> }</label>
1758
- <label [class.invalid]="bad('patient.address.city')">City <b>*</b><input name="city" [(ngModel)]="bill.patient.address.city"></label>
1759
- <label [class.invalid]="bad('patient.address.state')">State <b>*</b><input name="state" [(ngModel)]="bill.patient.address.state" maxlength="2"></label>
1760
- </div></fieldset>
1761
-
1762
- <fieldset id="mbs-claim"><legend>Injury &amp; claim</legend><div class="grid">
1763
- <label [class.invalid]="bad('service.date')">Date of service <b>*</b><mindbill-date-input ariaLabel="Date of service" [value]="bill.service.date" (valueChange)="setDate('service.date', $event)"/></label>
1764
- <label [class.invalid]="bad('claim.dateOfInjury')">Date of injury <b>*</b><mindbill-date-input ariaLabel="Date of injury" [value]="bill.claim.dateOfInjury" (valueChange)="setDate('claim.dateOfInjury', $event)"/></label>
1765
- <label class="wide">Treatment authorization # (optional)<input name="authNumber" [ngModel]="bill.service.authorizationNumber || ''" (ngModelChange)="setAuthorizationNumber($event)" placeholder="Utilization-review authorization number"><small>Rides on the bill as CMS-1500 Box 23 / 837 REF*G1.</small></label>
1766
- <label [class.invalid]="bad('claim.claimNumber')">Claim number <b>*</b><input name="claimNumber" [(ngModel)]="bill.claim.claimNumber"></label>
1767
- <label [class.invalid]="bad('claim.employer')">Employer <b>*</b><input name="employer" [(ngModel)]="bill.claim.employer"></label>
1768
- <div class="wide lookup" [class.invalid]="bad('claim.claimsAdministrator')"><label>Claims administrator <b>*</b></label>
1769
- <input name="payerQuery" [(ngModel)]="payerQuery" (focus)="searchPayers()" (input)="searchPayers()" placeholder="Search the MindBill payer directory…" autocomplete="off">
1770
- @if (payerBusy) { <small>Searching directory…</small> }
1771
- @if (payerResults.length) { <div class="options">@for (payer of payerResults; track payer.id) { <button type="button" (click)="selectPayer(payer)"><strong>{{ payer.name }}</strong><span>{{ payer.hasElectronic ? 'Electronic routing' : 'Paper / fax' }}@if (payer.confidence) { · {{ payer.confidence }} match }</span></button> }</div> }
1772
- @if (bill.claim.claimsAdministrator.id) { <small class="selected">✓ Routed to {{ bill.claim.claimsAdministrator.name }}</small> }
1773
- </div>
1774
- <label class="wide">Injury description (optional)<input name="description" [(ngModel)]="bill.claim.description"></label>
1775
- <div class="wide diagnosis" [class.invalid]="bad('diagnoses')"><label>Diagnosis codes (ICD-10) <b>*</b></label>
1776
- <div class="quick">@for (item of quickDiagnoses; track item.code) { <button type="button" [class.on]="hasDiagnosis(item.code)" (click)="toggleDiagnosis(item)">{{ hasDiagnosis(item.code) ? '✓' : '+' }} {{ item.label }}</button> }</div>
1777
- @if (bill.diagnoses.length) { <div class="chips">@for (code of bill.diagnoses; track code) { <button type="button" (click)="removeDiagnosis(code)"><strong>{{ code }}</strong> ×</button> }</div> }
1778
- <input name="diagnosisQuery" [(ngModel)]="diagnosisQuery" (focus)="searchDiagnoses(true)" (input)="searchDiagnoses(true)" placeholder="Search ICD-10 codes…" autocomplete="off">
1779
- @if (diagnosisBusy && !diagnosisResults.length) { <small>Searching ICD-10 directory…</small> }
1780
- @if (diagnosisResults.length) { <div class="options diagnoses" (scroll)="diagnosisScrolled($event)">@for (item of diagnosisResults; track item.code) { <button type="button" (click)="addDiagnosis(item)"><strong>{{ item.code }}</strong><span>{{ item.description }}</span></button> }</div> }
1781
- </div>
1782
- </div></fieldset>
1783
-
1784
- <fieldset id="mbs-provider"><legend>Providers &amp; place of service</legend><div class="grid">
1785
- <h3 class="wide">Billing provider</h3>
1786
- <label [class.invalid]="bad('billingProvider.name')">Billing provider name <b>*</b><input name="billingProvider" [(ngModel)]="bill.billingProvider.name"></label>
1787
- <label [class.invalid]="bad('billingProvider.taxId')">Tax ID <b>*</b><input name="taxId" [(ngModel)]="bill.billingProvider.taxId"></label>
1788
- <label [class.invalid]="bad('billingProvider.npi')">Billing NPI <b>*</b><input name="billingNpi" [(ngModel)]="bill.billingProvider.npi" inputmode="numeric" maxlength="10"></label>
1789
- <label [class.invalid]="bad('billingProvider.phone')">Billing phone <b>*</b><input name="billingPhone" type="tel" [(ngModel)]="bill.billingProvider.phone"></label>
1790
- <label class="wide" [class.invalid]="bad('billingProvider.address.line1')">Billing address <b>*</b><input name="billingAddress" [(ngModel)]="bill.billingProvider.address.line1"></label>
1791
- <label [class.invalid]="bad('billingProvider.address.postalCode')">Billing ZIP <b>*</b><input name="billingZip" [(ngModel)]="bill.billingProvider.address.postalCode" inputmode="numeric"></label>
1792
- <label [class.invalid]="bad('billingProvider.address.city')">Billing city <b>*</b><input name="billingCity" [(ngModel)]="bill.billingProvider.address.city"></label>
1793
- <label [class.invalid]="bad('billingProvider.address.state')">Billing state <b>*</b><input name="billingState" [(ngModel)]="bill.billingProvider.address.state" maxlength="2"></label>
1794
- <h3 class="wide">Rendering provider</h3>
1795
- <label [class.invalid]="bad('renderingProvider.name')">Rendering provider name <b>*</b><input name="renderingProvider" [(ngModel)]="bill.renderingProvider.name"></label>
1796
- <label [class.invalid]="bad('renderingProvider.npi')">Rendering provider NPI <b>*</b><input name="renderingNpi" [(ngModel)]="bill.renderingProvider.npi" inputmode="numeric" maxlength="10"></label>
1797
- <div class="wide" [class.invalid]="bad('renderingProvider.taxonomy')"><label>Rendering taxonomy <b>*</b></label>
1798
- <mindbill-combo-box ariaLabel="Rendering taxonomy" [value]="bill.renderingProvider.taxonomy || ''" [invalid]="bad('renderingProvider.taxonomy')" [preserveValueOnOpen]="true" placeholder="Search specialty name or taxonomy code…" [options]="taxonomyComboOptions" [createOption]="customTaxonomyOption" (selected)="selectTaxonomy($event)"/>
1799
- <small>Search by specialty name or 10-character taxonomy code.</small>
1800
- </div>
1801
- <h3 class="wide">Place of service</h3>
1802
- <label [class.invalid]="bad('serviceLocation.placeOfServiceCode')">Place of service code <b>*</b><input name="pos" [(ngModel)]="bill.serviceLocation.placeOfServiceCode" placeholder="11" inputmode="numeric" maxlength="2"></label>
1803
- <label class="wide" [class.invalid]="bad('serviceLocation.address.line1')">Service address <b>*</b><input name="serviceAddress" [(ngModel)]="bill.serviceLocation.address.line1"></label>
1804
- <label [class.invalid]="bad('serviceLocation.address.postalCode')">Service ZIP <b>*</b><input name="serviceZip" [(ngModel)]="bill.serviceLocation.address.postalCode" inputmode="numeric"></label>
1805
- <label [class.invalid]="bad('serviceLocation.address.city')">Service city <b>*</b><input name="serviceCity" [(ngModel)]="bill.serviceLocation.address.city"></label>
1806
- <label [class.invalid]="bad('serviceLocation.address.state')">Service state <b>*</b><input name="serviceState" [(ngModel)]="bill.serviceLocation.address.state" maxlength="2"></label>
1807
- </div></fieldset>
1808
-
1809
- <fieldset id="mbs-lines" [class.invalid-set]="bad('serviceLines')"><legend>Service lines</legend>
1810
- <div class="line-head"><span>Procedure code *</span><span>Modifiers</span><span>Dx</span><span>Units *</span><span>Allowed</span><span></span></div>
1811
- @for (line of bill.serviceLines; track $index; let index = $index) { <div class="service-line">
1812
- <div class="cell"><span class="mobile-label">Procedure</span>
1813
- <mindbill-combo-box [ariaLabel]="'Procedure code ' + (index + 1)" [value]="line.code" placeholder="Search or enter code…" [options]="procedureComboOptions" [createOption]="customProcedureOption" (selected)="selectProcedure(index, $event)"/>
1814
- @if (line.code) { <small>{{ procedureDescription(line.code) || 'Custom CPT, HCPCS, or medical-legal code' }}</small> }
1815
- </div>
1816
- <div class="cell"><span class="mobile-label">Modifiers</span>
1817
- @if (line.modifiers?.length) { <div class="chips">@for (modifier of line.modifiers; track modifier) { <button type="button" (click)="removeModifier(index, modifier)" [attr.aria-label]="'Remove modifier ' + modifier">−{{ modifier }} ×</button> }</div> }
1818
- <mindbill-combo-box [ariaLabel]="'Modifiers ' + (index + 1)" value="" [placeholder]="line.modifiers?.length ? line.modifiers!.length + ' modifier' + (line.modifiers!.length === 1 ? '' : 's') : 'Add modifiers…'" [options]="modifierComboOptions(line)" (selected)="addModifier(index, $event)"/>
2163
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillTasksDashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2164
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillTasksDashboardComponent, isStandalone: true, selector: "mindbill-bill-tasks-dashboard", inputs: { data: "data", buckets: "buckets", heading: "heading", grandTotalLabel: "grandTotalLabel", emptyLabel: "emptyLabel", appearance: "appearance" }, outputs: { cellSelected: "cellSelected" }, ngImport: i0, template: `
2165
+ <div class="mbtk" [ngStyle]="themeStyle">
2166
+ @if (heading) { <header class="heading"><h2>{{ heading }}</h2></header> }
2167
+ @for (section of data?.sections ?? []; track section.id) {
2168
+ <section class="card" [style.borderColor]="tone(section)" [attr.aria-label]="section.label">
2169
+ <div class="grid head" [style.borderBottomColor]="tone(section)">
2170
+ <span class="title">
2171
+ <i class="dot" [style.background]="tone(section)"></i>
2172
+ <span class="title-text"><strong>{{ section.label }}</strong><span class="basis">by {{ section.agingBasisLabel }}</span></span>
2173
+ </span>
2174
+ @for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
2175
+ <span class="pill" [style.background]="pillBackground(bucketIndex)" [style.color]="pillColor(bucketIndex)">{{ bucket.label }}</span>
2176
+ }
2177
+ <span class="colhead">Task Total</span>
1819
2178
  </div>
1820
- <div class="cell"><span class="mobile-label">Dx pointers</span>
1821
- <div class="dx">
1822
- @if (bill.diagnoses.length) {
1823
- @for (code of bill.diagnoses; track code; let dxIndex = $index) {
1824
- <button type="button" [class.on]="hasDiagnosisPointer(line, dxIndex + 1)" [title]="dxLetter(dxIndex + 1) + ' — ' + code" [attr.aria-pressed]="hasDiagnosisPointer(line, dxIndex + 1)" [attr.aria-label]="'Point line ' + (index + 1) + ' at diagnosis ' + code" (click)="toggleDiagnosisPointer(index, dxIndex + 1)">{{ dxLetter(dxIndex + 1) }}</button>
2179
+ @if (section.empty) {
2180
+ <p class="empty">{{ emptyLabel }}</p>
2181
+ } @else {
2182
+ @for (row of section.rows; track row.id) {
2183
+ <div class="grid row">
2184
+ <span class="rowlabel">{{ row.label }}</span>
2185
+ @for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
2186
+ <span class="count">
2187
+ @if ((row.counts[bucketIndex] ?? 0) > 0) {
2188
+ <button type="button" class="link" (click)="select(section, row, bucketIndex)">{{ row.counts[bucketIndex] }}</button>
2189
+ } @else { <span class="zero">0</span> }
2190
+ </span>
1825
2191
  }
1826
- } @else { <small>Add diagnoses above</small> }
2192
+ <span class="total">
2193
+ @if (row.total > 0) {
2194
+ <button type="button" class="link" (click)="select(section, row, null)">{{ row.total }}</button>
2195
+ } @else { <span class="zero">0</span> }
2196
+ </span>
2197
+ </div>
2198
+ }
2199
+ <div class="grid row totals">
2200
+ <span class="rowlabel">Total</span>
2201
+ @for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
2202
+ <span class="count">{{ section.totals[bucketIndex] ?? 0 }}</span>
2203
+ }
2204
+ <span class="total">{{ section.total }}</span>
1827
2205
  </div>
1828
- </div>
1829
- <label><span class="mobile-label">Units</span><input [name]="'units'+index" type="number" min="1" [(ngModel)]="line.units"></label>
1830
- <strong>{{ allowed(line) == null ? '—' : (allowed(line) | currency) }}</strong>
1831
- <button class="remove" type="button" (click)="removeLine(index)" aria-label="Remove service line">×</button>
1832
- </div> }
1833
- <div class="total"><span>Total</span><strong>{{ totalAllowed | currency }}</strong></div>
1834
- </fieldset>
1835
-
1836
- <fieldset><legend>Attachments</legend>
1837
- @for (attachment of workingAttachments; track attachment.filename; let index = $index) { <div class="attachment" [class.locked]="attachment.locked"><div><strong>{{ attachment.filename }}</strong><span>{{ attachment.description || attachment.badge || 'Supporting document' }}</span></div><div class="attachment-actions"><button type="button" (click)="previewAttachment(attachment)">Preview</button>@if (!attachment.locked) { <button type="button" (click)="removeAttachment(index)" aria-label="Remove attachment">×</button> }</div></div> }
1838
- <label class="dropzone" [class.drag-active]="dragActive"><input type="file" accept="application/pdf,.pdf" multiple (change)="filesSelected($event)"><strong>Drop additional PDF files here, or click to choose</strong><span>Add supporting documents by dropping them anywhere on this screen.</span></label>
1839
- </fieldset>
1840
-
1841
- @if (errorMessage) { <div class="error" role="alert">{{ errorMessage }}</div> }
1842
- <div class="submit-row"><span>{{ invalidFields.size ? 'Complete the highlighted required fields.' : 'All required billing data is ready.' }}</span><button type="submit" [disabled]="submitting">{{ submitting ? 'Submitting…' : submitLabel }}</button></div>
1843
- @if (dragActive) { <div class="drop-overlay" aria-hidden="true"><div><strong>Drop PDFs to attach</strong><span>Supporting documents are added to this submission.</span></div></div> }
1844
- </form>
1845
- `, isInline: true, styles: [":host{display:block}.mbs{--danger:#c83c3c;font-family:var(--font);color:var(--t)}*{box-sizing:border-box}.intro{display:flex;justify-content:space-between;align-items:start;margin:0 0 24px}.intro h2{margin:0;font-size:24px;font-weight:600}.intro p{margin:6px 0 0;color:var(--m)}.intro>span{color:var(--m);font-size:13px}.mbs b{color:inherit}.intro b,label>b,.lookup>label>b,.diagnosis>label>b,.cell>label>b{color:var(--danger)}fieldset{border:1px solid var(--b);border-radius:var(--r);margin:0 0 22px;padding:24px 26px 26px;background:var(--s)}legend{padding:0 10px;font-size:18px;font-weight:700}.grid{display:grid;grid-template-columns:1fr 1fr;gap:20px 28px}.wide{grid-column:1/-1}h3{margin:4px 0 -4px;padding-top:12px;border-top:1px solid var(--b);font-size:16px}h3:first-child{border:0;padding-top:0}label,.lookup,.diagnosis{display:grid;gap:8px;font-size:14px;font-weight:700}.grid>label{display:flex;flex-wrap:wrap;align-content:start;align-items:center;column-gap:4px;row-gap:8px}.grid>label>input,.grid>label>select,.grid>label>small,.grid>label>mindbill-date-input{flex:0 0 100%}.grid>div.wide{display:grid;gap:8px;font-size:14px;font-weight:700}input,select{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus,select:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}.invalid input,.invalid select,.invalid-set{border-color:var(--danger)!important;background:color-mix(in srgb,var(--danger) 4%,var(--s))}.invalid>label{color:var(--danger)}small{color:var(--m);font-weight:450}.selected{color:#238655}.options{position:relative;z-index:4;max-height:300px;overflow:auto;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);box-shadow:0 14px 35px #172b3730}.options button{display:grid;width:100%;gap:3px;border:0;border-bottom:1px solid var(--b);background:var(--s);padding:12px 14px;text-align:left;color:var(--t);cursor:pointer}.options button:hover{background:color-mix(in srgb,var(--a) 7%,var(--s))}.options span{color:var(--m);font-size:12px}.quick,.chips{display:flex;flex-wrap:wrap;gap:8px}.quick button,.chips button{border:1px solid var(--b);border-radius:999px;background:var(--s);padding:7px 11px;color:var(--t);font:inherit;font-size:13px;cursor:pointer}.quick .on{border-color:var(--a);color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.chips button{border-color:color-mix(in srgb,var(--a) 40%,var(--b));background:color-mix(in srgb,var(--a) 7%,var(--s));font-weight:700}.dx{display:flex;flex-wrap:wrap;gap:5px;padding-top:6px}.dx button{width:30px;height:30px;border:1px solid var(--b);border-radius:8px;background:var(--s);color:var(--m);font:inherit;font-size:13px;font-weight:750;cursor:pointer;padding:0}.dx button.on{border-color:var(--a);background:color-mix(in srgb,var(--a) 10%,var(--s));color:var(--a)}.line-head,.service-line{display:grid;grid-template-columns:1.5fr 1.35fr .8fr .5fr .7fr 36px;gap:14px;align-items:start}.line-head{padding:0 0 9px;color:var(--m);font-size:12px;font-weight:700}.service-line{border-top:1px solid var(--b);padding:12px 0}.service-line label,.service-line .cell{display:grid;gap:6px;font-size:14px;font-weight:700}.service-line strong{text-align:right;padding-top:12px}.remove{border:0;background:transparent;color:var(--m);font-size:25px;cursor:pointer}.mobile-label{display:none}.total{display:flex;justify-content:flex-end;gap:36px;border-top:1px solid var(--b);padding-top:14px;font-size:17px}.attachment{display:flex;align-items:center;justify-content:space-between;gap:16px;border:1px solid var(--b);border-radius:var(--cr);margin-bottom:10px;padding:14px 16px}.attachment.locked{border-color:#9fd6b4;background:#f3fcf6}.attachment>div:first-child{display:grid;gap:4px}.attachment span{color:var(--m);font-size:13px}.attachment-actions{display:flex;gap:8px}.attachment button{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:8px 12px;color:var(--t);font-weight:700;cursor:pointer}.dropzone{position:relative;display:grid;min-height:170px;place-content:center;gap:8px;border:2px dashed color-mix(in srgb,var(--m) 60%,transparent);border-radius:var(--cr);text-align:center;cursor:pointer;transition:border-color .15s,background .15s}.dropzone.drag-active{border-color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.dropzone input{position:absolute;inset:0;opacity:0;cursor:pointer}.dropzone span{color:var(--m);font-weight:450}.drop-overlay{position:fixed;inset:0;z-index:60;display:grid;place-content:center;background:color-mix(in srgb,var(--a) 14%,#ffffffd9);pointer-events:none}.drop-overlay div{display:grid;gap:6px;border:2px dashed var(--a);border-radius:var(--r);background:var(--s);padding:34px 44px;text-align:center;box-shadow:0 24px 60px #172b3740}.drop-overlay strong{font-size:19px}.drop-overlay span{color:var(--m)}.error{margin:0 0 14px;border-left:4px solid var(--danger);border-radius:var(--cr);background:color-mix(in srgb,var(--danger) 10%,var(--s));padding:14px;color:var(--danger)}.submit-row{position:sticky;bottom:0;z-index:3;display:flex;align-items:center;justify-content:space-between;border:1px solid var(--b);border-radius:var(--r);background:color-mix(in srgb,var(--s) 96%,transparent);box-shadow:0 -8px 24px #172b3718;padding:14px 18px}.submit-row>span{color:#43835f}.submit-row button{min-width:190px;border:0;border-radius:var(--cr);background:var(--a);color:var(--ac);padding:12px 24px;font:inherit;font-weight:800;cursor:pointer}.submit-row button:disabled{opacity:.6;cursor:wait}@media(max-width:760px){fieldset{padding:18px 16px}.grid{grid-template-columns:1fr}.wide{grid-column:auto}.intro{display:grid;gap:10px}.line-head{display:none}.service-line{grid-template-columns:1fr 1fr}.service-line>*{grid-column:1/-1}.service-line label:nth-child(3){grid-column:1}.service-line>strong{grid-column:2;grid-row:3;text-align:left}.service-line>.remove{grid-column:1/-1}.mobile-label{display:block;color:var(--m);font-size:12px}.attachment{align-items:flex-start}.attachment-actions{align-items:center}.submit-row>span{display:none}.submit-row button{width:100%}}\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.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { 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: "component", type: MindBillComboBoxComponent, selector: "mindbill-combo-box", inputs: ["value", "placeholder", "options", "disabled", "loading", "loadingMore", "invalid", "preserveValueOnOpen", "ariaLabel", "createOption"], outputs: ["opened", "queryChange", "endReached", "selected"] }, { kind: "component", type: MindBillDateInputComponent, selector: "mindbill-date-input", inputs: ["value", "disabled", "ariaLabel"], outputs: ["valueChange"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
2206
+ }
2207
+ </section>
2208
+ }
2209
+ <section class="card grand" [attr.aria-label]="grandTotalLabel">
2210
+ <div class="grid row totals">
2211
+ <span class="rowlabel grand-label">{{ grandTotalLabel }}</span>
2212
+ @for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
2213
+ <span class="count">{{ data?.grandTotals?.[bucketIndex] ?? 0 }}</span>
2214
+ }
2215
+ <span class="total">{{ data?.grandTotal ?? 0 }}</span>
2216
+ </div>
2217
+ </section>
2218
+ </div>
2219
+ `, isInline: true, styles: [":host{display:block}.mbtk{color:var(--t);font-family:var(--font);font-size:14px}.heading{margin-bottom:14px}.heading h2{margin:0;font-size:24px}.card{margin-bottom:16px;border:1.5px solid var(--b);border-radius:var(--r);background:var(--s);overflow:hidden}.grid{display:grid;grid-template-columns:var(--cols);gap:0 10px;align-items:center;padding:0 16px}.head{padding:12px 16px;border-bottom:1px solid var(--b)}.title{display:flex;align-items:center;gap:9px;min-width:0}.dot{flex:0 0 auto;width:11px;height:11px;border-radius:50%}.title-text{min-width:0}.title-text strong{display:block;font-size:15.5px;line-height:1.3}.basis{display:block;color:var(--m);font-size:12px;line-height:1.35}.pill{justify-self:stretch;padding:5px 6px;border-radius:999px;font-size:11px;font-weight:750;line-height:1.25;text-align:center;white-space:nowrap}.colhead{justify-self:stretch;color:var(--m);font-size:11px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;text-align:center;white-space:nowrap}.row{border-top:1px solid color-mix(in srgb,var(--b) 60%,transparent);min-height:44px}.row:first-of-type{border-top:0}.rowlabel{color:var(--m);font-size:13px;line-height:1.35;padding:9px 0;overflow-wrap:anywhere}.count,.total{text-align:center;font-variant-numeric:tabular-nums}.zero{color:var(--m)}.link{border:0;background:none;padding:6px 8px;color:var(--a);font:inherit;font-weight:750;font-variant-numeric:tabular-nums;cursor:pointer;border-radius:6px}.link:hover{text-decoration:underline;background:color-mix(in srgb,var(--a) 8%,transparent)}.totals{background:color-mix(in srgb,var(--bg) 78%,var(--s));border-top:1px solid var(--b);font-weight:760}.totals .rowlabel{color:var(--t);font-weight:760}.empty{margin:0;padding:22px 16px;color:var(--m);text-align:center}.grand{border-color:var(--b)}.grand .row{min-height:52px}.grand-label{font-size:14px;font-weight:800}@media(max-width:760px){.pill,.colhead,.count{display:none}.grid{grid-template-columns:minmax(0,1fr) auto}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] });
1846
2220
  }
1847
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillSubmissionComponent, decorators: [{
2221
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillTasksDashboardComponent, decorators: [{
1848
2222
  type: Component,
1849
- args: [{ selector: "mindbill-bill-submission", standalone: true, imports: [CommonModule, FormsModule, MindBillComboBoxComponent, MindBillDateInputComponent], template: `
1850
- <form class="mbs" [ngStyle]="themeStyle" (submit)="submit($event)" novalidate>
1851
- <div class="intro"><div><h2>{{ heading }}</h2><p>{{ description }}</p></div><span><b>*</b> Required</span></div>
1852
-
1853
- <fieldset id="mbs-patient"><legend>Patient</legend><div class="grid">
1854
- <label [class.invalid]="bad('patient.firstName')">First name <b>*</b><input name="firstName" [(ngModel)]="bill.patient.firstName"></label>
1855
- <label [class.invalid]="bad('patient.lastName')">Last name <b>*</b><input name="lastName" [(ngModel)]="bill.patient.lastName"></label>
1856
- <label [class.invalid]="bad('patient.dateOfBirth')">Date of birth <b>*</b><mindbill-date-input ariaLabel="Date of birth" [value]="bill.patient.dateOfBirth" (valueChange)="setDate('patient.dateOfBirth', $event)"/></label>
1857
- <label>Phone (optional)<input name="phone" type="tel" [(ngModel)]="bill.patient.phone"></label>
1858
- <label class="wide" [class.invalid]="bad('patient.address.line1')">Address <b>*</b><input name="address" [(ngModel)]="bill.patient.address.line1"></label>
1859
- <label [class.invalid]="bad('patient.address.postalCode')">ZIP <b>*</b><input name="zip" [(ngModel)]="bill.patient.address.postalCode" (ngModelChange)="postalChanged($event)" inputmode="numeric">@if (postalStatus) { <small>{{ postalStatus }}</small> }</label>
1860
- <label [class.invalid]="bad('patient.address.city')">City <b>*</b><input name="city" [(ngModel)]="bill.patient.address.city"></label>
1861
- <label [class.invalid]="bad('patient.address.state')">State <b>*</b><input name="state" [(ngModel)]="bill.patient.address.state" maxlength="2"></label>
1862
- </div></fieldset>
1863
-
1864
- <fieldset id="mbs-claim"><legend>Injury &amp; claim</legend><div class="grid">
1865
- <label [class.invalid]="bad('service.date')">Date of service <b>*</b><mindbill-date-input ariaLabel="Date of service" [value]="bill.service.date" (valueChange)="setDate('service.date', $event)"/></label>
1866
- <label [class.invalid]="bad('claim.dateOfInjury')">Date of injury <b>*</b><mindbill-date-input ariaLabel="Date of injury" [value]="bill.claim.dateOfInjury" (valueChange)="setDate('claim.dateOfInjury', $event)"/></label>
1867
- <label class="wide">Treatment authorization # (optional)<input name="authNumber" [ngModel]="bill.service.authorizationNumber || ''" (ngModelChange)="setAuthorizationNumber($event)" placeholder="Utilization-review authorization number"><small>Rides on the bill as CMS-1500 Box 23 / 837 REF*G1.</small></label>
1868
- <label [class.invalid]="bad('claim.claimNumber')">Claim number <b>*</b><input name="claimNumber" [(ngModel)]="bill.claim.claimNumber"></label>
1869
- <label [class.invalid]="bad('claim.employer')">Employer <b>*</b><input name="employer" [(ngModel)]="bill.claim.employer"></label>
1870
- <div class="wide lookup" [class.invalid]="bad('claim.claimsAdministrator')"><label>Claims administrator <b>*</b></label>
1871
- <input name="payerQuery" [(ngModel)]="payerQuery" (focus)="searchPayers()" (input)="searchPayers()" placeholder="Search the MindBill payer directory…" autocomplete="off">
1872
- @if (payerBusy) { <small>Searching directory…</small> }
1873
- @if (payerResults.length) { <div class="options">@for (payer of payerResults; track payer.id) { <button type="button" (click)="selectPayer(payer)"><strong>{{ payer.name }}</strong><span>{{ payer.hasElectronic ? 'Electronic routing' : 'Paper / fax' }}@if (payer.confidence) { · {{ payer.confidence }} match }</span></button> }</div> }
1874
- @if (bill.claim.claimsAdministrator.id) { <small class="selected">✓ Routed to {{ bill.claim.claimsAdministrator.name }}</small> }
1875
- </div>
1876
- <label class="wide">Injury description (optional)<input name="description" [(ngModel)]="bill.claim.description"></label>
1877
- <div class="wide diagnosis" [class.invalid]="bad('diagnoses')"><label>Diagnosis codes (ICD-10) <b>*</b></label>
1878
- <div class="quick">@for (item of quickDiagnoses; track item.code) { <button type="button" [class.on]="hasDiagnosis(item.code)" (click)="toggleDiagnosis(item)">{{ hasDiagnosis(item.code) ? '✓' : '+' }} {{ item.label }}</button> }</div>
1879
- @if (bill.diagnoses.length) { <div class="chips">@for (code of bill.diagnoses; track code) { <button type="button" (click)="removeDiagnosis(code)"><strong>{{ code }}</strong> ×</button> }</div> }
1880
- <input name="diagnosisQuery" [(ngModel)]="diagnosisQuery" (focus)="searchDiagnoses(true)" (input)="searchDiagnoses(true)" placeholder="Search ICD-10 codes…" autocomplete="off">
1881
- @if (diagnosisBusy && !diagnosisResults.length) { <small>Searching ICD-10 directory…</small> }
1882
- @if (diagnosisResults.length) { <div class="options diagnoses" (scroll)="diagnosisScrolled($event)">@for (item of diagnosisResults; track item.code) { <button type="button" (click)="addDiagnosis(item)"><strong>{{ item.code }}</strong><span>{{ item.description }}</span></button> }</div> }
1883
- </div>
1884
- </div></fieldset>
1885
-
1886
- <fieldset id="mbs-provider"><legend>Providers &amp; place of service</legend><div class="grid">
1887
- <h3 class="wide">Billing provider</h3>
1888
- <label [class.invalid]="bad('billingProvider.name')">Billing provider name <b>*</b><input name="billingProvider" [(ngModel)]="bill.billingProvider.name"></label>
1889
- <label [class.invalid]="bad('billingProvider.taxId')">Tax ID <b>*</b><input name="taxId" [(ngModel)]="bill.billingProvider.taxId"></label>
1890
- <label [class.invalid]="bad('billingProvider.npi')">Billing NPI <b>*</b><input name="billingNpi" [(ngModel)]="bill.billingProvider.npi" inputmode="numeric" maxlength="10"></label>
1891
- <label [class.invalid]="bad('billingProvider.phone')">Billing phone <b>*</b><input name="billingPhone" type="tel" [(ngModel)]="bill.billingProvider.phone"></label>
1892
- <label class="wide" [class.invalid]="bad('billingProvider.address.line1')">Billing address <b>*</b><input name="billingAddress" [(ngModel)]="bill.billingProvider.address.line1"></label>
1893
- <label [class.invalid]="bad('billingProvider.address.postalCode')">Billing ZIP <b>*</b><input name="billingZip" [(ngModel)]="bill.billingProvider.address.postalCode" inputmode="numeric"></label>
1894
- <label [class.invalid]="bad('billingProvider.address.city')">Billing city <b>*</b><input name="billingCity" [(ngModel)]="bill.billingProvider.address.city"></label>
1895
- <label [class.invalid]="bad('billingProvider.address.state')">Billing state <b>*</b><input name="billingState" [(ngModel)]="bill.billingProvider.address.state" maxlength="2"></label>
1896
- <h3 class="wide">Rendering provider</h3>
1897
- <label [class.invalid]="bad('renderingProvider.name')">Rendering provider name <b>*</b><input name="renderingProvider" [(ngModel)]="bill.renderingProvider.name"></label>
1898
- <label [class.invalid]="bad('renderingProvider.npi')">Rendering provider NPI <b>*</b><input name="renderingNpi" [(ngModel)]="bill.renderingProvider.npi" inputmode="numeric" maxlength="10"></label>
1899
- <div class="wide" [class.invalid]="bad('renderingProvider.taxonomy')"><label>Rendering taxonomy <b>*</b></label>
1900
- <mindbill-combo-box ariaLabel="Rendering taxonomy" [value]="bill.renderingProvider.taxonomy || ''" [invalid]="bad('renderingProvider.taxonomy')" [preserveValueOnOpen]="true" placeholder="Search specialty name or taxonomy code…" [options]="taxonomyComboOptions" [createOption]="customTaxonomyOption" (selected)="selectTaxonomy($event)"/>
1901
- <small>Search by specialty name or 10-character taxonomy code.</small>
1902
- </div>
1903
- <h3 class="wide">Place of service</h3>
1904
- <label [class.invalid]="bad('serviceLocation.placeOfServiceCode')">Place of service code <b>*</b><input name="pos" [(ngModel)]="bill.serviceLocation.placeOfServiceCode" placeholder="11" inputmode="numeric" maxlength="2"></label>
1905
- <label class="wide" [class.invalid]="bad('serviceLocation.address.line1')">Service address <b>*</b><input name="serviceAddress" [(ngModel)]="bill.serviceLocation.address.line1"></label>
1906
- <label [class.invalid]="bad('serviceLocation.address.postalCode')">Service ZIP <b>*</b><input name="serviceZip" [(ngModel)]="bill.serviceLocation.address.postalCode" inputmode="numeric"></label>
1907
- <label [class.invalid]="bad('serviceLocation.address.city')">Service city <b>*</b><input name="serviceCity" [(ngModel)]="bill.serviceLocation.address.city"></label>
1908
- <label [class.invalid]="bad('serviceLocation.address.state')">Service state <b>*</b><input name="serviceState" [(ngModel)]="bill.serviceLocation.address.state" maxlength="2"></label>
1909
- </div></fieldset>
1910
-
1911
- <fieldset id="mbs-lines" [class.invalid-set]="bad('serviceLines')"><legend>Service lines</legend>
1912
- <div class="line-head"><span>Procedure code *</span><span>Modifiers</span><span>Dx</span><span>Units *</span><span>Allowed</span><span></span></div>
1913
- @for (line of bill.serviceLines; track $index; let index = $index) { <div class="service-line">
1914
- <div class="cell"><span class="mobile-label">Procedure</span>
1915
- <mindbill-combo-box [ariaLabel]="'Procedure code ' + (index + 1)" [value]="line.code" placeholder="Search or enter code…" [options]="procedureComboOptions" [createOption]="customProcedureOption" (selected)="selectProcedure(index, $event)"/>
1916
- @if (line.code) { <small>{{ procedureDescription(line.code) || 'Custom CPT, HCPCS, or medical-legal code' }}</small> }
1917
- </div>
1918
- <div class="cell"><span class="mobile-label">Modifiers</span>
1919
- @if (line.modifiers?.length) { <div class="chips">@for (modifier of line.modifiers; track modifier) { <button type="button" (click)="removeModifier(index, modifier)" [attr.aria-label]="'Remove modifier ' + modifier">−{{ modifier }} ×</button> }</div> }
1920
- <mindbill-combo-box [ariaLabel]="'Modifiers ' + (index + 1)" value="" [placeholder]="line.modifiers?.length ? line.modifiers!.length + ' modifier' + (line.modifiers!.length === 1 ? '' : 's') : 'Add modifiers…'" [options]="modifierComboOptions(line)" (selected)="addModifier(index, $event)"/>
2223
+ args: [{ selector: "mindbill-bill-tasks-dashboard", standalone: true, imports: [CommonModule], template: `
2224
+ <div class="mbtk" [ngStyle]="themeStyle">
2225
+ @if (heading) { <header class="heading"><h2>{{ heading }}</h2></header> }
2226
+ @for (section of data?.sections ?? []; track section.id) {
2227
+ <section class="card" [style.borderColor]="tone(section)" [attr.aria-label]="section.label">
2228
+ <div class="grid head" [style.borderBottomColor]="tone(section)">
2229
+ <span class="title">
2230
+ <i class="dot" [style.background]="tone(section)"></i>
2231
+ <span class="title-text"><strong>{{ section.label }}</strong><span class="basis">by {{ section.agingBasisLabel }}</span></span>
2232
+ </span>
2233
+ @for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
2234
+ <span class="pill" [style.background]="pillBackground(bucketIndex)" [style.color]="pillColor(bucketIndex)">{{ bucket.label }}</span>
2235
+ }
2236
+ <span class="colhead">Task Total</span>
1921
2237
  </div>
1922
- <div class="cell"><span class="mobile-label">Dx pointers</span>
1923
- <div class="dx">
1924
- @if (bill.diagnoses.length) {
1925
- @for (code of bill.diagnoses; track code; let dxIndex = $index) {
1926
- <button type="button" [class.on]="hasDiagnosisPointer(line, dxIndex + 1)" [title]="dxLetter(dxIndex + 1) + ' — ' + code" [attr.aria-pressed]="hasDiagnosisPointer(line, dxIndex + 1)" [attr.aria-label]="'Point line ' + (index + 1) + ' at diagnosis ' + code" (click)="toggleDiagnosisPointer(index, dxIndex + 1)">{{ dxLetter(dxIndex + 1) }}</button>
2238
+ @if (section.empty) {
2239
+ <p class="empty">{{ emptyLabel }}</p>
2240
+ } @else {
2241
+ @for (row of section.rows; track row.id) {
2242
+ <div class="grid row">
2243
+ <span class="rowlabel">{{ row.label }}</span>
2244
+ @for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
2245
+ <span class="count">
2246
+ @if ((row.counts[bucketIndex] ?? 0) > 0) {
2247
+ <button type="button" class="link" (click)="select(section, row, bucketIndex)">{{ row.counts[bucketIndex] }}</button>
2248
+ } @else { <span class="zero">0</span> }
2249
+ </span>
1927
2250
  }
1928
- } @else { <small>Add diagnoses above</small> }
2251
+ <span class="total">
2252
+ @if (row.total > 0) {
2253
+ <button type="button" class="link" (click)="select(section, row, null)">{{ row.total }}</button>
2254
+ } @else { <span class="zero">0</span> }
2255
+ </span>
2256
+ </div>
2257
+ }
2258
+ <div class="grid row totals">
2259
+ <span class="rowlabel">Total</span>
2260
+ @for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
2261
+ <span class="count">{{ section.totals[bucketIndex] ?? 0 }}</span>
2262
+ }
2263
+ <span class="total">{{ section.total }}</span>
1929
2264
  </div>
1930
- </div>
1931
- <label><span class="mobile-label">Units</span><input [name]="'units'+index" type="number" min="1" [(ngModel)]="line.units"></label>
1932
- <strong>{{ allowed(line) == null ? '—' : (allowed(line) | currency) }}</strong>
1933
- <button class="remove" type="button" (click)="removeLine(index)" aria-label="Remove service line">×</button>
1934
- </div> }
1935
- <div class="total"><span>Total</span><strong>{{ totalAllowed | currency }}</strong></div>
1936
- </fieldset>
1937
-
1938
- <fieldset><legend>Attachments</legend>
1939
- @for (attachment of workingAttachments; track attachment.filename; let index = $index) { <div class="attachment" [class.locked]="attachment.locked"><div><strong>{{ attachment.filename }}</strong><span>{{ attachment.description || attachment.badge || 'Supporting document' }}</span></div><div class="attachment-actions"><button type="button" (click)="previewAttachment(attachment)">Preview</button>@if (!attachment.locked) { <button type="button" (click)="removeAttachment(index)" aria-label="Remove attachment">×</button> }</div></div> }
1940
- <label class="dropzone" [class.drag-active]="dragActive"><input type="file" accept="application/pdf,.pdf" multiple (change)="filesSelected($event)"><strong>Drop additional PDF files here, or click to choose</strong><span>Add supporting documents by dropping them anywhere on this screen.</span></label>
1941
- </fieldset>
1942
-
1943
- @if (errorMessage) { <div class="error" role="alert">{{ errorMessage }}</div> }
1944
- <div class="submit-row"><span>{{ invalidFields.size ? 'Complete the highlighted required fields.' : 'All required billing data is ready.' }}</span><button type="submit" [disabled]="submitting">{{ submitting ? 'Submitting…' : submitLabel }}</button></div>
1945
- @if (dragActive) { <div class="drop-overlay" aria-hidden="true"><div><strong>Drop PDFs to attach</strong><span>Supporting documents are added to this submission.</span></div></div> }
1946
- </form>
1947
- `, styles: [":host{display:block}.mbs{--danger:#c83c3c;font-family:var(--font);color:var(--t)}*{box-sizing:border-box}.intro{display:flex;justify-content:space-between;align-items:start;margin:0 0 24px}.intro h2{margin:0;font-size:24px;font-weight:600}.intro p{margin:6px 0 0;color:var(--m)}.intro>span{color:var(--m);font-size:13px}.mbs b{color:inherit}.intro b,label>b,.lookup>label>b,.diagnosis>label>b,.cell>label>b{color:var(--danger)}fieldset{border:1px solid var(--b);border-radius:var(--r);margin:0 0 22px;padding:24px 26px 26px;background:var(--s)}legend{padding:0 10px;font-size:18px;font-weight:700}.grid{display:grid;grid-template-columns:1fr 1fr;gap:20px 28px}.wide{grid-column:1/-1}h3{margin:4px 0 -4px;padding-top:12px;border-top:1px solid var(--b);font-size:16px}h3:first-child{border:0;padding-top:0}label,.lookup,.diagnosis{display:grid;gap:8px;font-size:14px;font-weight:700}.grid>label{display:flex;flex-wrap:wrap;align-content:start;align-items:center;column-gap:4px;row-gap:8px}.grid>label>input,.grid>label>select,.grid>label>small,.grid>label>mindbill-date-input{flex:0 0 100%}.grid>div.wide{display:grid;gap:8px;font-size:14px;font-weight:700}input,select{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus,select:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}.invalid input,.invalid select,.invalid-set{border-color:var(--danger)!important;background:color-mix(in srgb,var(--danger) 4%,var(--s))}.invalid>label{color:var(--danger)}small{color:var(--m);font-weight:450}.selected{color:#238655}.options{position:relative;z-index:4;max-height:300px;overflow:auto;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);box-shadow:0 14px 35px #172b3730}.options button{display:grid;width:100%;gap:3px;border:0;border-bottom:1px solid var(--b);background:var(--s);padding:12px 14px;text-align:left;color:var(--t);cursor:pointer}.options button:hover{background:color-mix(in srgb,var(--a) 7%,var(--s))}.options span{color:var(--m);font-size:12px}.quick,.chips{display:flex;flex-wrap:wrap;gap:8px}.quick button,.chips button{border:1px solid var(--b);border-radius:999px;background:var(--s);padding:7px 11px;color:var(--t);font:inherit;font-size:13px;cursor:pointer}.quick .on{border-color:var(--a);color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.chips button{border-color:color-mix(in srgb,var(--a) 40%,var(--b));background:color-mix(in srgb,var(--a) 7%,var(--s));font-weight:700}.dx{display:flex;flex-wrap:wrap;gap:5px;padding-top:6px}.dx button{width:30px;height:30px;border:1px solid var(--b);border-radius:8px;background:var(--s);color:var(--m);font:inherit;font-size:13px;font-weight:750;cursor:pointer;padding:0}.dx button.on{border-color:var(--a);background:color-mix(in srgb,var(--a) 10%,var(--s));color:var(--a)}.line-head,.service-line{display:grid;grid-template-columns:1.5fr 1.35fr .8fr .5fr .7fr 36px;gap:14px;align-items:start}.line-head{padding:0 0 9px;color:var(--m);font-size:12px;font-weight:700}.service-line{border-top:1px solid var(--b);padding:12px 0}.service-line label,.service-line .cell{display:grid;gap:6px;font-size:14px;font-weight:700}.service-line strong{text-align:right;padding-top:12px}.remove{border:0;background:transparent;color:var(--m);font-size:25px;cursor:pointer}.mobile-label{display:none}.total{display:flex;justify-content:flex-end;gap:36px;border-top:1px solid var(--b);padding-top:14px;font-size:17px}.attachment{display:flex;align-items:center;justify-content:space-between;gap:16px;border:1px solid var(--b);border-radius:var(--cr);margin-bottom:10px;padding:14px 16px}.attachment.locked{border-color:#9fd6b4;background:#f3fcf6}.attachment>div:first-child{display:grid;gap:4px}.attachment span{color:var(--m);font-size:13px}.attachment-actions{display:flex;gap:8px}.attachment button{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:8px 12px;color:var(--t);font-weight:700;cursor:pointer}.dropzone{position:relative;display:grid;min-height:170px;place-content:center;gap:8px;border:2px dashed color-mix(in srgb,var(--m) 60%,transparent);border-radius:var(--cr);text-align:center;cursor:pointer;transition:border-color .15s,background .15s}.dropzone.drag-active{border-color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.dropzone input{position:absolute;inset:0;opacity:0;cursor:pointer}.dropzone span{color:var(--m);font-weight:450}.drop-overlay{position:fixed;inset:0;z-index:60;display:grid;place-content:center;background:color-mix(in srgb,var(--a) 14%,#ffffffd9);pointer-events:none}.drop-overlay div{display:grid;gap:6px;border:2px dashed var(--a);border-radius:var(--r);background:var(--s);padding:34px 44px;text-align:center;box-shadow:0 24px 60px #172b3740}.drop-overlay strong{font-size:19px}.drop-overlay span{color:var(--m)}.error{margin:0 0 14px;border-left:4px solid var(--danger);border-radius:var(--cr);background:color-mix(in srgb,var(--danger) 10%,var(--s));padding:14px;color:var(--danger)}.submit-row{position:sticky;bottom:0;z-index:3;display:flex;align-items:center;justify-content:space-between;border:1px solid var(--b);border-radius:var(--r);background:color-mix(in srgb,var(--s) 96%,transparent);box-shadow:0 -8px 24px #172b3718;padding:14px 18px}.submit-row>span{color:#43835f}.submit-row button{min-width:190px;border:0;border-radius:var(--cr);background:var(--a);color:var(--ac);padding:12px 24px;font:inherit;font-weight:800;cursor:pointer}.submit-row button:disabled{opacity:.6;cursor:wait}@media(max-width:760px){fieldset{padding:18px 16px}.grid{grid-template-columns:1fr}.wide{grid-column:auto}.intro{display:grid;gap:10px}.line-head{display:none}.service-line{grid-template-columns:1fr 1fr}.service-line>*{grid-column:1/-1}.service-line label:nth-child(3){grid-column:1}.service-line>strong{grid-column:2;grid-row:3;text-align:left}.service-line>.remove{grid-column:1/-1}.mobile-label{display:block;color:var(--m);font-size:12px}.attachment{align-items:flex-start}.attachment-actions{align-items:center}.submit-row>span{display:none}.submit-row button{width:100%}}\n"] }]
1948
- }], propDecorators: { initialBill: [{
1949
- type: Input
1950
- }], attachments: [{
1951
- type: Input
1952
- }], appearance: [{
1953
- type: Input
1954
- }], sessionEndpoint: [{
1955
- type: Input
1956
- }], getSession: [{
2265
+ }
2266
+ </section>
2267
+ }
2268
+ <section class="card grand" [attr.aria-label]="grandTotalLabel">
2269
+ <div class="grid row totals">
2270
+ <span class="rowlabel grand-label">{{ grandTotalLabel }}</span>
2271
+ @for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
2272
+ <span class="count">{{ data?.grandTotals?.[bucketIndex] ?? 0 }}</span>
2273
+ }
2274
+ <span class="total">{{ data?.grandTotal ?? 0 }}</span>
2275
+ </div>
2276
+ </section>
2277
+ </div>
2278
+ `, styles: [":host{display:block}.mbtk{color:var(--t);font-family:var(--font);font-size:14px}.heading{margin-bottom:14px}.heading h2{margin:0;font-size:24px}.card{margin-bottom:16px;border:1.5px solid var(--b);border-radius:var(--r);background:var(--s);overflow:hidden}.grid{display:grid;grid-template-columns:var(--cols);gap:0 10px;align-items:center;padding:0 16px}.head{padding:12px 16px;border-bottom:1px solid var(--b)}.title{display:flex;align-items:center;gap:9px;min-width:0}.dot{flex:0 0 auto;width:11px;height:11px;border-radius:50%}.title-text{min-width:0}.title-text strong{display:block;font-size:15.5px;line-height:1.3}.basis{display:block;color:var(--m);font-size:12px;line-height:1.35}.pill{justify-self:stretch;padding:5px 6px;border-radius:999px;font-size:11px;font-weight:750;line-height:1.25;text-align:center;white-space:nowrap}.colhead{justify-self:stretch;color:var(--m);font-size:11px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;text-align:center;white-space:nowrap}.row{border-top:1px solid color-mix(in srgb,var(--b) 60%,transparent);min-height:44px}.row:first-of-type{border-top:0}.rowlabel{color:var(--m);font-size:13px;line-height:1.35;padding:9px 0;overflow-wrap:anywhere}.count,.total{text-align:center;font-variant-numeric:tabular-nums}.zero{color:var(--m)}.link{border:0;background:none;padding:6px 8px;color:var(--a);font:inherit;font-weight:750;font-variant-numeric:tabular-nums;cursor:pointer;border-radius:6px}.link:hover{text-decoration:underline;background:color-mix(in srgb,var(--a) 8%,transparent)}.totals{background:color-mix(in srgb,var(--bg) 78%,var(--s));border-top:1px solid var(--b);font-weight:760}.totals .rowlabel{color:var(--t);font-weight:760}.empty{margin:0;padding:22px 16px;color:var(--m);text-align:center}.grand{border-color:var(--b)}.grand .row{min-height:52px}.grand-label{font-size:14px;font-weight:800}@media(max-width:760px){.pill,.colhead,.count{display:none}.grid{grid-template-columns:minmax(0,1fr) auto}}\n"] }]
2279
+ }], propDecorators: { data: [{
1957
2280
  type: Input
1958
- }], apiBaseUrl: [{
2281
+ }], buckets: [{
1959
2282
  type: Input
1960
2283
  }], heading: [{
1961
2284
  type: Input
1962
- }], description: [{
1963
- type: Input
1964
- }], submitLabel: [{
1965
- type: Input
1966
- }], procedureOptions: [{
1967
- type: Input
1968
- }], modifierOptions: [{
2285
+ }], grandTotalLabel: [{
1969
2286
  type: Input
1970
- }], taxonomyOptions: [{
2287
+ }], emptyLabel: [{
1971
2288
  type: Input
1972
- }], submitter: [{
2289
+ }], appearance: [{
1973
2290
  type: Input
1974
- }], submitted: [{
1975
- type: Output
1976
- }], submissionError: [{
1977
- type: Output
1978
- }], billChange: [{
2291
+ }], cellSelected: [{
1979
2292
  type: Output
1980
- }], onWindowDragOver: [{
1981
- type: HostListener,
1982
- args: ["window:dragover", ["$event"]]
1983
- }], onWindowDragLeave: [{
1984
- type: HostListener,
1985
- args: ["window:dragleave", ["$event"]]
1986
- }], onWindowDrop: [{
1987
- type: HostListener,
1988
- args: ["window:drop", ["$event"]]
1989
2293
  }] } });
1990
2294
 
1991
2295
  const THEME$1 = {