@mindbill/angular 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -7
- package/fesm2022/mindbill-angular.mjs +96 -598
- package/fesm2022/mindbill-angular.mjs.map +1 -1
- package/package.json +2 -2
- package/types/mindbill-angular.d.ts +7 -85
|
@@ -14,14 +14,10 @@ class MindBillLifecycleStore {
|
|
|
14
14
|
mutating = signal(false, ...(ngDevMode ? [{ debugName: "mutating" }] : /* istanbul ignore next */ []));
|
|
15
15
|
ready = computed(() => this.data() !== null && !this.loading(), ...(ngDevMode ? [{ debugName: "ready" }] : /* istanbul ignore next */ []));
|
|
16
16
|
client = null;
|
|
17
|
-
createInput;
|
|
18
|
-
onBillCreated;
|
|
19
17
|
refreshTimer = null;
|
|
20
|
-
connect(options, refreshInterval = 60_000
|
|
18
|
+
connect(options, refreshInterval = 60_000) {
|
|
21
19
|
this.disconnect();
|
|
22
|
-
this.billId.set(options.billId
|
|
23
|
-
this.createInput = createInput;
|
|
24
|
-
this.onBillCreated = onBillCreated;
|
|
20
|
+
this.billId.set(options.billId);
|
|
25
21
|
this.client = createBillLifecycleClient(options);
|
|
26
22
|
void this.refresh();
|
|
27
23
|
if (refreshInterval > 0) {
|
|
@@ -34,29 +30,13 @@ class MindBillLifecycleStore {
|
|
|
34
30
|
this.refreshTimer = null;
|
|
35
31
|
this.client?.clearSession();
|
|
36
32
|
this.client = null;
|
|
37
|
-
this.createInput = undefined;
|
|
38
|
-
this.onBillCreated = undefined;
|
|
39
33
|
}
|
|
40
34
|
async refresh() {
|
|
41
35
|
if (!this.client)
|
|
42
36
|
return null;
|
|
43
37
|
this.loading.set(this.data() === null);
|
|
44
38
|
try {
|
|
45
|
-
|
|
46
|
-
if (!this.client.getBillId()) {
|
|
47
|
-
if (!this.createInput) {
|
|
48
|
-
throw new Error("Pass billId to open a bill or create to start a new one.");
|
|
49
|
-
}
|
|
50
|
-
const created = await this.client.createBill(this.createInput);
|
|
51
|
-
const isNew = !this.billId();
|
|
52
|
-
this.billId.set(created.billId);
|
|
53
|
-
if (isNew)
|
|
54
|
-
this.onBillCreated?.(created.billId, created.data);
|
|
55
|
-
data = created.data;
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
data = await this.client.getLifecycle();
|
|
59
|
-
}
|
|
39
|
+
const data = await this.client.getLifecycle();
|
|
60
40
|
this.data.set(data);
|
|
61
41
|
this.error.set(null);
|
|
62
42
|
return data;
|
|
@@ -69,37 +49,12 @@ class MindBillLifecycleStore {
|
|
|
69
49
|
this.loading.set(false);
|
|
70
50
|
}
|
|
71
51
|
}
|
|
72
|
-
searchClaimsAdministrators(query, claimNumber) {
|
|
73
|
-
return this.requireClient().searchClaimsAdministrators(query, claimNumber);
|
|
74
|
-
}
|
|
75
52
|
getDeliveryOptions() { return this.requireClient().getDeliveryOptions(); }
|
|
76
|
-
saveReview(input) { return this.mutate(() => this.requireClient().saveReview(input)); }
|
|
77
|
-
submitBill(input, submission) { return this.mutate(() => this.requireClient().submitBill(input, submission)); }
|
|
78
|
-
addAttachment(file, type, description) { return this.mutate(() => this.requireClient().addAttachment(file, type, description)); }
|
|
79
|
-
removeAttachment(id) { return this.mutate(() => this.requireClient().removeAttachment(id)); }
|
|
80
53
|
getAttachment(id) { return this.requireClient().getAttachment(id); }
|
|
81
54
|
getEor(id) { return this.requireClient().getEor(id); }
|
|
82
55
|
closeBill(input) { return this.mutate(() => this.requireClient().closeBill(input)); }
|
|
83
56
|
postPayment(input) { return this.mutate(() => this.requireClient().postPayment(input)); }
|
|
84
57
|
submitSecondReview(input) { return this.mutate(() => this.requireClient().submitSecondReview(input)); }
|
|
85
|
-
async startCorrection() {
|
|
86
|
-
this.mutating.set(true);
|
|
87
|
-
try {
|
|
88
|
-
const result = await this.requireClient().startCorrection();
|
|
89
|
-
this.billId.set(result.replacementBillId);
|
|
90
|
-
this.data.set(result.data);
|
|
91
|
-
this.error.set(null);
|
|
92
|
-
return result.data;
|
|
93
|
-
}
|
|
94
|
-
catch (cause) {
|
|
95
|
-
const error = cause instanceof Error ? cause : new Error("Correction draft could not be created.");
|
|
96
|
-
this.error.set(error);
|
|
97
|
-
throw error;
|
|
98
|
-
}
|
|
99
|
-
finally {
|
|
100
|
-
this.mutating.set(false);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
58
|
requireClient() {
|
|
104
59
|
if (!this.client)
|
|
105
60
|
throw new Error("Connect the lifecycle store before using it.");
|
|
@@ -124,326 +79,80 @@ class MindBillLifecycleStore {
|
|
|
124
79
|
}
|
|
125
80
|
}
|
|
126
81
|
|
|
127
|
-
const EMPTY_PROCEDURE_LINE = {
|
|
128
|
-
code: "",
|
|
129
|
-
modifiers: [],
|
|
130
|
-
units: 1,
|
|
131
|
-
charge: 0,
|
|
132
|
-
};
|
|
133
|
-
function isEmptyProcedureLine(line) {
|
|
134
|
-
return !line.code.trim()
|
|
135
|
-
&& line.modifiers.length === 0
|
|
136
|
-
&& line.units === 1
|
|
137
|
-
&& !line.serviceDate
|
|
138
|
-
&& !line.serviceDateEnd
|
|
139
|
-
&& !(line.diagnosisPointers?.length)
|
|
140
|
-
&& !line.charge;
|
|
141
|
-
}
|
|
142
|
-
/** Keeps completed/partial lines plus one keyboard-ready empty row. */
|
|
143
|
-
function ensureTrailingProcedureLine(lines) {
|
|
144
|
-
const entered = lines
|
|
145
|
-
.filter((line) => !isEmptyProcedureLine(line))
|
|
146
|
-
.map((line) => ({ ...line, modifiers: [...line.modifiers] }));
|
|
147
|
-
return entered.length < 50
|
|
148
|
-
? [...entered, { ...EMPTY_PROCEDURE_LINE, modifiers: [] }]
|
|
149
|
-
: entered;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
82
|
const THEMES = {
|
|
153
83
|
mindbill: { preset: "mindbill", accentColor: "#238dbd", accentTextColor: "#fff", backgroundColor: "#f3f8fa", surfaceColor: "#fff", textColor: "#203743", mutedColor: "#657982", borderColor: "#dbe6ea", borderRadius: "14px", controlRadius: "8px", fontFamily: "Inter,system-ui,sans-serif" },
|
|
154
84
|
"qme-companion": { preset: "qme-companion", accentColor: "#53b5dc", accentTextColor: "#173542", backgroundColor: "#f2f8fb", surfaceColor: "#fff", textColor: "#1d3440", mutedColor: "#617783", borderColor: "#d7e5eb", borderRadius: "12px", controlRadius: "8px", fontFamily: "Inter,system-ui,sans-serif" },
|
|
155
|
-
"orange-bright": { preset: "orange-bright", accentColor: "#
|
|
85
|
+
"orange-bright": { preset: "orange-bright", accentColor: "#f4510b", accentTextColor: "#fff", backgroundColor: "#fffaf6", surfaceColor: "#fffefd", textColor: "#090f1f", mutedColor: "#626a73", borderColor: "#e7e1da", borderRadius: "16px", controlRadius: "10px", fontFamily: "Inter,system-ui,sans-serif" },
|
|
156
86
|
"clinical-blue": { preset: "clinical-blue", accentColor: "#1677ff", accentTextColor: "#fff", backgroundColor: "#f5f7fa", surfaceColor: "#fff", textColor: "#1f2d3d", mutedColor: "#66788a", borderColor: "#d9e2ec", borderRadius: "8px", controlRadius: "6px", fontFamily: "Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif" },
|
|
157
87
|
};
|
|
158
|
-
const DOCUMENT_TYPES = [
|
|
159
|
-
{ value: "final_report", label: "Final report" },
|
|
160
|
-
{ value: "proof_of_service", label: "Proof of service" },
|
|
161
|
-
{ value: "letter_of_attestation", label: "Letter of attestation" },
|
|
162
|
-
{ value: "form_122", label: "Required form" },
|
|
163
|
-
{ value: "return_to_work_voucher", label: "Return-to-work voucher" },
|
|
164
|
-
{ value: "w9", label: "W-9" },
|
|
165
|
-
{ value: "appeal", label: "Appeal support" },
|
|
166
|
-
{ value: "medical_records", label: "Medical records (intentional)" },
|
|
167
|
-
{ value: "other", label: "Other supporting document" },
|
|
168
|
-
];
|
|
169
88
|
class MindBillBillLifecycleComponent {
|
|
170
89
|
billId = "";
|
|
171
|
-
create;
|
|
172
90
|
sessionEndpoint = "/api/mindbill/session";
|
|
173
91
|
apiBaseUrl = "https://app.mindbill.org";
|
|
174
92
|
getSession;
|
|
175
93
|
refreshInterval = 60_000;
|
|
176
94
|
appearance = { preset: "mindbill" };
|
|
177
|
-
billIdChange = new EventEmitter();
|
|
178
|
-
billCreated = new EventEmitter();
|
|
179
|
-
submitted = new EventEmitter();
|
|
180
95
|
billingError = new EventEmitter();
|
|
181
96
|
store = new MindBillLifecycleStore();
|
|
182
|
-
documentTypes = DOCUMENT_TYPES;
|
|
183
|
-
medLegalCodes = ["ML200", "ML201", "ML202", "ML203", "ML204", "ML205", "ML206", "MLPRR"];
|
|
184
97
|
lifecycleStages = [
|
|
185
|
-
{ id: "
|
|
186
|
-
{ id: "
|
|
187
|
-
{ id: "accepted", label: "Accepted" },
|
|
188
|
-
{ id: "response", label: "Payer response" },
|
|
189
|
-
{ id: "follow_up", label: "Follow-up" },
|
|
98
|
+
{ id: "submitted", label: "Submitted" }, { id: "accepted", label: "Accepted" },
|
|
99
|
+
{ id: "response", label: "Payer response" }, { id: "follow_up", label: "Follow-up" },
|
|
190
100
|
{ id: "closed", label: "Closed" },
|
|
191
101
|
];
|
|
192
|
-
draft = null;
|
|
193
|
-
dirty = false;
|
|
194
|
-
payerQuery = "";
|
|
195
|
-
payerResults = [];
|
|
196
|
-
documentType = "other";
|
|
197
|
-
pendingFile = null;
|
|
198
|
-
deliveryOptions = null;
|
|
199
|
-
submission = { route: "ebill" };
|
|
200
|
-
showSubmit = false;
|
|
201
|
-
deliveryBusy = false;
|
|
202
102
|
panel = "";
|
|
203
103
|
notice = "";
|
|
204
104
|
closeReason = "";
|
|
205
|
-
payment = { amount: 0, method: "check",
|
|
105
|
+
payment = { amount: 0, method: "check", depositDate: new Date().toISOString().slice(0, 10) };
|
|
206
106
|
review = { reason: "", payerClaimControlNumber: "", disputedAmount: 0 };
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
constructor() {
|
|
210
|
-
effect(() => {
|
|
211
|
-
const data = this.store.data();
|
|
212
|
-
if (data && !this.dirty)
|
|
213
|
-
this.draft = this.makeDraft(data);
|
|
214
|
-
const error = this.store.error();
|
|
215
|
-
if (error)
|
|
216
|
-
this.billingError.emit(error);
|
|
217
|
-
});
|
|
218
|
-
}
|
|
107
|
+
constructor() { effect(() => { const error = this.store.error(); if (error)
|
|
108
|
+
this.billingError.emit(error); }); }
|
|
219
109
|
ngOnChanges(changes) {
|
|
220
|
-
if (changes["billId"] || changes["
|
|
221
|
-
if (!this.billId
|
|
110
|
+
if (changes["billId"] || changes["sessionEndpoint"] || changes["apiBaseUrl"] || changes["getSession"]) {
|
|
111
|
+
if (!this.billId.trim()) {
|
|
222
112
|
this.store.disconnect();
|
|
223
113
|
return;
|
|
224
114
|
}
|
|
225
|
-
this.store.connect({
|
|
226
|
-
...(this.billId ? { billId: this.billId } : {}),
|
|
227
|
-
sessionEndpoint: this.sessionEndpoint,
|
|
228
|
-
apiBaseUrl: this.apiBaseUrl,
|
|
229
|
-
...(this.getSession ? { getSession: this.getSession } : {}),
|
|
230
|
-
}, this.refreshInterval, this.create, (billId, data) => {
|
|
231
|
-
this.billIdChange.emit(billId);
|
|
232
|
-
this.billCreated.emit({ billId, data });
|
|
233
|
-
});
|
|
115
|
+
this.store.connect({ billId: this.billId, sessionEndpoint: this.sessionEndpoint, apiBaseUrl: this.apiBaseUrl, ...(this.getSession ? { getSession: this.getSession } : {}) }, this.refreshInterval);
|
|
234
116
|
}
|
|
235
117
|
}
|
|
236
|
-
ngOnDestroy() { this.store.disconnect();
|
|
237
|
-
clearTimeout(this.payerTimer); }
|
|
118
|
+
ngOnDestroy() { this.store.disconnect(); }
|
|
238
119
|
get themeStyle() {
|
|
239
120
|
const base = THEMES[this.appearance.preset ?? "mindbill"];
|
|
240
|
-
return {
|
|
241
|
-
"--a": this.appearance.accentColor ?? base.accentColor,
|
|
242
|
-
"--ac": this.appearance.accentTextColor ?? base.accentTextColor,
|
|
243
|
-
"--bg": this.appearance.backgroundColor ?? base.backgroundColor,
|
|
244
|
-
"--s": this.appearance.surfaceColor ?? base.surfaceColor,
|
|
245
|
-
"--t": this.appearance.textColor ?? base.textColor,
|
|
246
|
-
"--m": this.appearance.mutedColor ?? base.mutedColor,
|
|
247
|
-
"--b": this.appearance.borderColor ?? base.borderColor,
|
|
248
|
-
"--r": this.appearance.borderRadius ?? base.borderRadius,
|
|
249
|
-
"--cr": this.appearance.controlRadius ?? base.controlRadius,
|
|
250
|
-
"--font": this.appearance.fontFamily ?? base.fontFamily,
|
|
251
|
-
};
|
|
121
|
+
return { "--a": this.appearance.accentColor ?? base.accentColor, "--ac": this.appearance.accentTextColor ?? base.accentTextColor, "--bg": this.appearance.backgroundColor ?? base.backgroundColor, "--s": this.appearance.surfaceColor ?? base.surfaceColor, "--t": this.appearance.textColor ?? base.textColor, "--m": this.appearance.mutedColor ?? base.mutedColor, "--b": this.appearance.borderColor ?? base.borderColor, "--r": this.appearance.borderRadius ?? base.borderRadius, "--cr": this.appearance.controlRadius ?? base.controlRadius, "--font": this.appearance.fontFamily ?? base.fontFamily };
|
|
252
122
|
}
|
|
253
123
|
stateLabel(data) { return data.lifecycle.state.replace(/_/g, " ").replace(/\b\w/g, (value) => value.toUpperCase()); }
|
|
254
|
-
lifecycleDetail(data) { return data.lifecycle.submittedAt ? `Submitted ${new Date(data.lifecycle.submittedAt).toLocaleDateString()}${data.lifecycle.agingDays != null ? ` · ${data.lifecycle.agingDays} days old` : ""}` : "
|
|
255
|
-
currentStageIndex(data) {
|
|
256
|
-
|
|
257
|
-
if (value.includes("
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const labels = { "bill.created": "Bill created", "bill.submitted": "Bill submitted", "bill.accepted": "Bill accepted", "bill.rejected": "Bill rejected", "bill.denied": "Bill denied", "bill.partially_paid": "Partial payment received", "bill.paid": "Bill paid", "bill.second_review": "Second Bill Review submitted", "bill.ibr": "Independent Bill Review submitted", "bill.lien": "Bill moved to lien", "bill.closed": "Bill closed", "payment.posted": "Payment posted", "eor.received": "EOR received" };
|
|
271
|
-
return labels[type] ?? type.replace(/[._-]+/g, " ").replace(/\b\w/g, (value) => value.toUpperCase());
|
|
272
|
-
}
|
|
273
|
-
isEditable(data) { return data.lifecycle.actions.some((action) => action.id === "edit_and_submit" && action.enabled) || ["incomplete", "draft", "not_submitted"].includes(data.lifecycle.state.toLowerCase()); }
|
|
274
|
-
payerExplanation(payer) { return payer.signals?.map((signal) => signal.label).join(" · ") || (payer.hasElectronic ? "Electronic billing available" : "Available in payer directory"); }
|
|
275
|
-
queuePayerSearch() { if (this.payerTimer)
|
|
276
|
-
clearTimeout(this.payerTimer); this.payerTimer = setTimeout(() => void this.searchPayers(), 250); }
|
|
277
|
-
async searchPayers() { if (!this.payerQuery.trim()) {
|
|
278
|
-
this.payerResults = [];
|
|
279
|
-
return;
|
|
280
|
-
} this.payerResults = await this.store.searchClaimsAdministrators(this.payerQuery, this.draft?.injury.claimNumber); }
|
|
281
|
-
selectPayer(payer) { if (!this.draft)
|
|
282
|
-
return; this.draft.injury.claimsAdminId = payer.id; this.draft.injury.claimsAdminName = payer.name; this.payerQuery = ""; this.payerResults = []; this.dirty = true; }
|
|
283
|
-
clearPayer() { if (!this.draft)
|
|
284
|
-
return; this.draft.injury.claimsAdminId = ""; this.draft.injury.claimsAdminName = ""; this.dirty = true; }
|
|
285
|
-
lineChanged() { this.normalizeLineItems(); this.dirty = true; }
|
|
286
|
-
removeLine(index) { this.draft?.bill.lineItems.splice(index, 1); this.normalizeLineItems(); this.dirty = true; }
|
|
287
|
-
setModifiers(line, value) { line.modifiers = value.split(",").map((item) => item.trim().replace(/^-/, "")).filter(Boolean); this.normalizeLineItems(); this.dirty = true; }
|
|
288
|
-
setDiagnosisPointers(line, value) { line.diagnosisPointers = value.split(",").map((item) => Number(item.trim())).filter((item) => Number.isInteger(item) && item >= 1 && item <= 12); this.normalizeLineItems(); this.dirty = true; }
|
|
289
|
-
applyPreset(kind) {
|
|
290
|
-
if (!this.draft)
|
|
291
|
-
return;
|
|
292
|
-
const modifier = kind === "ame" ? "94" : kind === "psych" ? "96" : "95";
|
|
293
|
-
const entered = this.draft.bill.lineItems.filter((line) => line.code.trim());
|
|
294
|
-
if (entered.length === 0)
|
|
295
|
-
entered.push({ code: "ML201", modifiers: [modifier], units: 1, charge: 0 });
|
|
296
|
-
else
|
|
297
|
-
entered.forEach((line) => { if (!line.modifiers.length)
|
|
298
|
-
line.modifiers = [modifier]; });
|
|
299
|
-
this.draft.bill.lineItems = ensureTrailingProcedureLine(entered);
|
|
300
|
-
this.dirty = true;
|
|
301
|
-
}
|
|
302
|
-
fileSelected(event) { this.pendingFile = event.target.files?.[0] ?? null; }
|
|
303
|
-
async attach() { if (!this.pendingFile)
|
|
304
|
-
return; await this.store.addAttachment(this.pendingFile, this.documentType); this.pendingFile = null; this.notice = "Document attached."; }
|
|
305
|
-
async removeAttachment(id) { await this.store.removeAttachment(id); this.notice = "Document removed."; }
|
|
124
|
+
lifecycleDetail(data) { return data.lifecycle.submittedAt ? `Submitted ${new Date(data.lifecycle.submittedAt).toLocaleDateString()}${data.lifecycle.agingDays != null ? ` · ${data.lifecycle.agingDays} days old` : ""}` : "Submitted bill"; }
|
|
125
|
+
currentStageIndex(data) { const value = data.lifecycle.state.toLowerCase(); if (value.includes("closed") || value.includes("written_off"))
|
|
126
|
+
return 4; if (value.includes("second_review") || value.includes("ibr") || value.includes("lien") || value.includes("appeal"))
|
|
127
|
+
return 3; if (value.includes("processed") || value.includes("paid") || value.includes("denied") || value.includes("rejected") || value.includes("response"))
|
|
128
|
+
return 2; if (value.includes("accepted"))
|
|
129
|
+
return 1; return 0; }
|
|
130
|
+
activityLabel(type) { return type.replace(/[._-]+/g, " ").replace(/\b\w/g, (value) => value.toUpperCase()); }
|
|
131
|
+
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."; }
|
|
132
|
+
beginAction(action, data) { if (action === "post_payment")
|
|
133
|
+
this.panel = "payment";
|
|
134
|
+
else if (action === "second_review" || action === "independent_bill_review")
|
|
135
|
+
this.panel = "review";
|
|
136
|
+
else if (action === "close")
|
|
137
|
+
this.panel = "close";
|
|
138
|
+
else if (action === "view_eor" && data.eors[0])
|
|
139
|
+
void this.openEor(data.eors[0].id, data.eors[0].filename); }
|
|
306
140
|
async openAttachment(id, filename) { this.openBlob(await this.store.getAttachment(id), filename); }
|
|
307
141
|
async openEor(id, filename) { this.openBlob(await this.store.getEor(id), filename); }
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (!input || !input.claimsAdminId || !input.billingProvider?.name || !input.billingProvider.taxId || !input.billingProvider.npi || !input.renderingProvider?.name || !input.renderingProvider.npi || !input.lineItems.length)
|
|
311
|
-
return false;
|
|
312
|
-
return input.lineItems.every((line) => line.code && line.units > 0 && (this.draft?.bill.billingMode !== "professional" || Boolean(line.serviceDate && line.charge && line.charge > 0)));
|
|
313
|
-
}
|
|
314
|
-
async save() { const input = this.buildInput(); if (!input)
|
|
315
|
-
return; const data = await this.store.saveReview(input); this.dirty = false; this.draft = this.makeDraft(data); this.notice = "Bill saved."; }
|
|
316
|
-
async openSubmit() {
|
|
317
|
-
if (!this.canSubmit())
|
|
318
|
-
return;
|
|
319
|
-
this.deliveryBusy = true;
|
|
320
|
-
try {
|
|
321
|
-
this.deliveryOptions = await this.store.getDeliveryOptions();
|
|
322
|
-
this.chooseDelivery(this.deliveryOptions.recommended);
|
|
323
|
-
this.showSubmit = true;
|
|
324
|
-
}
|
|
325
|
-
finally {
|
|
326
|
-
this.deliveryBusy = false;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
closeSubmit() { this.showSubmit = false; }
|
|
330
|
-
selectedDelivery() { return this.deliveryOptions?.options.find((option) => option.route === this.submission.route); }
|
|
331
|
-
chooseDelivery(option) {
|
|
332
|
-
const destination = option.route === "fax" ? { faxNumber: option.target ?? this.deliveryOptions?.contacts.faxNumber ?? "" }
|
|
333
|
-
: option.route === "email" ? { email: option.target ?? this.deliveryOptions?.contacts.claimsEmail ?? "" }
|
|
334
|
-
: option.route === "mail" ? { mailingAddress: option.target ?? this.deliveryOptions?.contacts.mailingAddress ?? "" }
|
|
335
|
-
: undefined;
|
|
336
|
-
this.submission = { route: option.route, ...(destination ? { destination } : {}) };
|
|
337
|
-
}
|
|
338
|
-
setDestination(key, value) { this.submission = { ...this.submission, destination: { ...this.submission.destination, [key]: value } }; }
|
|
339
|
-
deliveryReady() {
|
|
340
|
-
if (this.submission.route === "fax")
|
|
341
|
-
return Boolean(this.submission.destination?.faxNumber?.trim());
|
|
342
|
-
if (this.submission.route === "email")
|
|
343
|
-
return Boolean(this.submission.destination?.email?.trim());
|
|
344
|
-
if (this.submission.route === "mail")
|
|
345
|
-
return Boolean(this.submission.destination?.mailingAddress?.trim());
|
|
346
|
-
return true;
|
|
347
|
-
}
|
|
348
|
-
async submit() { const input = this.buildInput(); if (!input || !this.canSubmit() || !this.deliveryReady())
|
|
349
|
-
return; const data = await this.store.submitBill(input, this.submission); this.showSubmit = false; this.dirty = false; this.draft = this.makeDraft(data); this.notice = "Bill submitted."; this.submitted.emit(data); }
|
|
350
|
-
beginAction(action) {
|
|
351
|
-
const data = this.store.data();
|
|
352
|
-
if (action === "post_payment")
|
|
353
|
-
this.panel = "payment";
|
|
354
|
-
else if (action === "second_review" || action === "independent_bill_review") {
|
|
355
|
-
this.reviewAttachmentIds = data?.bill.attachments.map((doc) => doc.id) ?? [];
|
|
356
|
-
this.panel = "review";
|
|
357
|
-
}
|
|
358
|
-
else if (action === "close")
|
|
359
|
-
this.panel = "close";
|
|
360
|
-
else if (action === "view_eor" && data?.eors[0]) {
|
|
361
|
-
const eor = data.eors[0];
|
|
362
|
-
void this.openEor(eor.id, eor.filename);
|
|
363
|
-
}
|
|
364
|
-
else if (action === "correct_and_resubmit")
|
|
365
|
-
this.panel = "correction";
|
|
366
|
-
}
|
|
367
|
-
actionTitle() { return this.panel === "payment" ? "Post payment" : this.panel === "review" ? "Submit Second Review" : this.panel === "correction" ? "Correct and resubmit" : "Close bill"; }
|
|
368
|
-
toggleReviewAttachment(id) { this.reviewAttachmentIds = this.reviewAttachmentIds.includes(id) ? this.reviewAttachmentIds.filter((item) => item !== id) : [...this.reviewAttachmentIds, id]; }
|
|
369
|
-
async attachForReview() { if (!this.pendingFile)
|
|
370
|
-
return; const filename = this.pendingFile.name; await this.attach(); const attachment = this.store.data()?.bill.attachments.find((doc) => doc.filename === filename); if (attachment && !this.reviewAttachmentIds.includes(attachment.id))
|
|
371
|
-
this.reviewAttachmentIds = [...this.reviewAttachmentIds, attachment.id]; }
|
|
372
|
-
async correct() { const data = await this.store.startCorrection(); this.billIdChange.emit(this.store.billId()); this.dirty = false; this.draft = this.makeDraft(data); this.panel = ""; this.notice = "Correction ready for review."; }
|
|
373
|
-
async postPayment() {
|
|
374
|
-
await this.store.postPayment({
|
|
375
|
-
amount: this.payment.amount,
|
|
376
|
-
method: this.payment.method,
|
|
377
|
-
depositDate: this.payment.depositDate,
|
|
378
|
-
...(this.payment.checkNumber ? { checkNumber: this.payment.checkNumber } : {}),
|
|
379
|
-
...(this.payment.note ? { note: this.payment.note } : {}),
|
|
380
|
-
});
|
|
381
|
-
this.panel = "";
|
|
382
|
-
this.notice = "Payment posted.";
|
|
383
|
-
}
|
|
384
|
-
async submitReview() {
|
|
385
|
-
await this.store.submitSecondReview({
|
|
386
|
-
reason: this.review.reason,
|
|
387
|
-
payerClaimControlNumber: this.review.payerClaimControlNumber,
|
|
388
|
-
disputedAmount: this.review.disputedAmount > 0 ? this.review.disputedAmount : undefined,
|
|
389
|
-
route: "ebill",
|
|
390
|
-
attachmentIds: this.reviewAttachmentIds,
|
|
391
|
-
});
|
|
392
|
-
this.panel = "";
|
|
393
|
-
this.notice = "Second Review submitted.";
|
|
394
|
-
}
|
|
142
|
+
async postPayment() { await this.store.postPayment(this.payment); this.panel = ""; this.notice = "Payment posted."; }
|
|
143
|
+
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."; }
|
|
395
144
|
async closeBill() { if (!this.closeReason.trim())
|
|
396
145
|
return; await this.store.closeBill({ reason: this.closeReason }); this.panel = ""; this.notice = "Bill closed."; }
|
|
397
|
-
buildInput() {
|
|
398
|
-
if (!this.draft)
|
|
399
|
-
return null;
|
|
400
|
-
return {
|
|
401
|
-
claimsAdminId: this.draft.injury.claimsAdminId ?? "",
|
|
402
|
-
patientOverrides: {
|
|
403
|
-
firstName: this.draft.patient.firstName,
|
|
404
|
-
lastName: this.draft.patient.lastName,
|
|
405
|
-
...(this.draft.patient.middleName ? { middleName: this.draft.patient.middleName } : {}),
|
|
406
|
-
...(this.draft.patient.dob ? { dob: this.draft.patient.dob } : {}),
|
|
407
|
-
},
|
|
408
|
-
injuryOverrides: {
|
|
409
|
-
...(this.draft.injury.claimNumber ? { claimNumber: this.draft.injury.claimNumber } : {}),
|
|
410
|
-
...(this.draft.injury.employer ? { employer: this.draft.injury.employer } : {}),
|
|
411
|
-
...(this.draft.injury.doi ? { doi: this.draft.injury.doi } : {}),
|
|
412
|
-
...(this.draft.injury.injuryEndDate ? { injuryEndDate: this.draft.injury.injuryEndDate } : {}),
|
|
413
|
-
...(typeof this.draft.injury.cumulativeTrauma === "boolean" ? { cumulativeTrauma: this.draft.injury.cumulativeTrauma } : {}),
|
|
414
|
-
...(this.draft.injury.adjNumber ? { adjNumber: this.draft.injury.adjNumber } : {}),
|
|
415
|
-
},
|
|
416
|
-
dos: this.draft.bill.dos,
|
|
417
|
-
billingProvider: this.draft.billingProvider,
|
|
418
|
-
renderingProvider: this.draft.clinician,
|
|
419
|
-
placeOfService: this.draft.location,
|
|
420
|
-
lineItems: this.draft.bill.lineItems.filter((line) => line.code.trim()).map((line) => ({
|
|
421
|
-
...(line.id ? { id: line.id } : {}),
|
|
422
|
-
code: line.code,
|
|
423
|
-
modifiers: line.modifiers,
|
|
424
|
-
units: line.units,
|
|
425
|
-
...(line.charge > 0 ? { charge: line.charge } : {}),
|
|
426
|
-
...(line.serviceDate ? { serviceDate: line.serviceDate } : {}),
|
|
427
|
-
...(line.serviceDateEnd ? { serviceDateEnd: line.serviceDateEnd } : {}),
|
|
428
|
-
...(line.diagnosisPointers?.length ? { diagnosisPointers: line.diagnosisPointers } : {}),
|
|
429
|
-
})),
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
makeDraft(data) {
|
|
433
|
-
const snapshot = data.bill.billingSnapshot ?? {};
|
|
434
|
-
const names = data.patient.name.trim().split(/\s+/);
|
|
435
|
-
return { patient: { ...data.patient, firstName: data.patient.firstName ?? names[0] ?? "", lastName: data.patient.lastName ?? names.slice(1).join(" ") }, injury: { ...data.injury }, bill: { ...data.bill, lineItems: ensureTrailingProcedureLine(data.bill.lineItems) }, billingProvider: snapshot.billingProvider ?? { name: "", taxId: "", npi: "", billType: "Professional" }, clinician: snapshot.renderingProvider ?? { name: "", specialty: "", npi: "" }, location: snapshot.placeOfService ?? { name: "", street: "", city: "", state: "", zip: "", posCode: "11" } };
|
|
436
|
-
}
|
|
437
|
-
normalizeLineItems() { if (this.draft)
|
|
438
|
-
this.draft.bill.lineItems = ensureTrailingProcedureLine(this.draft.bill.lineItems); }
|
|
439
146
|
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); }
|
|
440
147
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillLifecycleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
441
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillLifecycleComponent, isStandalone: true, selector: "mindbill-bill-lifecycle", inputs: { billId: "billId",
|
|
148
|
+
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: `
|
|
442
149
|
<section class="mb" [ngStyle]="themeStyle">
|
|
443
|
-
@if (
|
|
150
|
+
@if (!billId) {
|
|
151
|
+
<div class="state error"><strong>A submitted bill ID is required.</strong></div>
|
|
152
|
+
} @else if (store.loading() && !store.data()) {
|
|
444
153
|
<div class="state">Loading billing…</div>
|
|
445
154
|
} @else if (store.error() && !store.data()) {
|
|
446
|
-
<div class="state error"><strong>Billing is temporarily unavailable</strong><span>{{ store.error()?.message }}</span><button (click)="store.refresh()">Try again</button></div>
|
|
155
|
+
<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>
|
|
447
156
|
} @else if (store.data(); as data) {
|
|
448
157
|
<header class="summary">
|
|
449
158
|
<div><span class="eyebrow">Bill {{ data.bill.billNumber }}</span><h2>{{ stateLabel(data) }}</h2><p>{{ lifecycleDetail(data) }}</p></div>
|
|
@@ -451,158 +160,45 @@ class MindBillBillLifecycleComponent {
|
|
|
451
160
|
</header>
|
|
452
161
|
|
|
453
162
|
<section class="card progress" aria-label="Bill lifecycle">
|
|
454
|
-
<
|
|
455
|
-
<ol>@for (stage of lifecycleStages; track stage.id; let i=$index) { <li [class.complete]="i < currentStageIndex(data)" [class.current]="i === currentStageIndex(data)" [attr.aria-current]="i === currentStageIndex(data) ? 'step' : null"><b>{{ i < currentStageIndex(data) ? '✓' : i + 1 }}</b><span>{{ stage.label }}</span></li> }</ol>
|
|
163
|
+
<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>
|
|
456
164
|
</section>
|
|
457
165
|
|
|
458
166
|
<section class="card snapshot" aria-label="Bill snapshot">
|
|
459
|
-
<div class="card-title"><div><h3>Bill snapshot</h3><p>The
|
|
167
|
+
<div class="card-title"><div><h3>Bill snapshot</h3><p>The immutable values submitted to the payer.</p></div></div>
|
|
460
168
|
<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>
|
|
461
169
|
</section>
|
|
462
170
|
|
|
463
|
-
|
|
464
|
-
<
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
<div class="grid four">
|
|
468
|
-
<label><span>First name</span><input required [(ngModel)]="draft.patient.firstName" name="firstName" (ngModelChange)="dirty=true"></label>
|
|
469
|
-
<label><span>Last name</span><input required [(ngModel)]="draft.patient.lastName" name="lastName" (ngModelChange)="dirty=true"></label>
|
|
470
|
-
<label><span>Date of birth</span><input type="date" [(ngModel)]="draft.patient.dob" name="dob" (ngModelChange)="dirty=true"></label>
|
|
471
|
-
<label><span>Date of injury</span><input type="date" [(ngModel)]="draft.injury.doi" name="doi" (ngModelChange)="dirty=true"></label>
|
|
472
|
-
<label><span>Claim number</span><input [(ngModel)]="draft.injury.claimNumber" name="claimNumber" (ngModelChange)="dirty=true"></label>
|
|
473
|
-
<label><span>Employer</span><input [(ngModel)]="draft.injury.employer" name="employer" (ngModelChange)="dirty=true"></label>
|
|
474
|
-
<label><span>WCAB / case number</span><input [(ngModel)]="draft.injury.adjNumber" name="adjNumber" (ngModelChange)="dirty=true"></label>
|
|
475
|
-
<label><span>Date of service</span><input required type="date" [(ngModel)]="draft.bill.dos" name="dos" (ngModelChange)="dirty=true"></label>
|
|
476
|
-
</div>
|
|
477
|
-
</section>
|
|
171
|
+
<div class="columns">
|
|
172
|
+
<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 & 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>
|
|
173
|
+
<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>
|
|
174
|
+
</div>
|
|
478
175
|
|
|
479
|
-
|
|
480
|
-
<div class="card-title"><div><h3>Claims administrator</h3><p>Select who should receive this bill. Search uses payer names and claim-number patterns.</p></div><span>Required</span></div>
|
|
481
|
-
@if (draft.injury.claimsAdminId) { <div class="selected"><div><strong>{{ draft.injury.claimsAdminName }}</strong><small>Selected recipient</small></div><button type="button" (click)="clearPayer()">Change</button></div> }
|
|
482
|
-
@else {
|
|
483
|
-
<label><span>Insurance company or claims administrator</span><input name="payerSearch" [(ngModel)]="payerQuery" (ngModelChange)="queuePayerSearch()" placeholder="Search by payer or administrator name" autocomplete="off"></label>
|
|
484
|
-
@if (payerResults.length) { <div class="results">@for (payer of payerResults; track payer.id) { <button type="button" (click)="selectPayer(payer)"><strong>{{ payer.name }}</strong><span>{{ payerExplanation(payer) }}</span></button> }</div> }
|
|
485
|
-
}
|
|
486
|
-
</section>
|
|
176
|
+
<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>
|
|
487
177
|
|
|
488
|
-
|
|
489
|
-
<div class="card-title"><div><h3>Billing identity</h3><p>The payee, rendering clinician, and place of service are bill snapshots—not synchronization records.</p></div><span>Prefilled</span></div>
|
|
490
|
-
<div class="subhead">Billing provider</div>
|
|
491
|
-
<div class="grid three">
|
|
492
|
-
<label><span>Practice name</span><input required [(ngModel)]="draft.billingProvider.name" name="practiceName" (ngModelChange)="dirty=true"></label>
|
|
493
|
-
<label><span>Tax ID</span><input required [(ngModel)]="draft.billingProvider.taxId" name="taxId" (ngModelChange)="dirty=true"></label>
|
|
494
|
-
<label><span>Billing NPI</span><input required [(ngModel)]="draft.billingProvider.npi" name="billingNpi" (ngModelChange)="dirty=true"></label>
|
|
495
|
-
</div>
|
|
496
|
-
<div class="subhead">Rendering clinician</div>
|
|
497
|
-
<div class="grid three">
|
|
498
|
-
<label><span>Name</span><input required [(ngModel)]="draft.clinician.name" name="clinicianName" (ngModelChange)="dirty=true"></label>
|
|
499
|
-
<label><span>NPI</span><input required [(ngModel)]="draft.clinician.npi" name="clinicianNpi" (ngModelChange)="dirty=true"></label>
|
|
500
|
-
<label><span>Taxonomy</span><input [(ngModel)]="draft.clinician.taxonomy" name="taxonomy" (ngModelChange)="dirty=true"></label>
|
|
501
|
-
</div>
|
|
502
|
-
<div class="subhead">Service location</div>
|
|
503
|
-
<div class="grid three">
|
|
504
|
-
<label><span>Location</span><input required [(ngModel)]="draft.location.name" name="locationName" (ngModelChange)="dirty=true"></label>
|
|
505
|
-
<label><span>Street</span><input required [(ngModel)]="draft.location.street" name="street" (ngModelChange)="dirty=true"></label>
|
|
506
|
-
<label><span>City</span><input required [(ngModel)]="draft.location.city" name="city" (ngModelChange)="dirty=true"></label>
|
|
507
|
-
<label><span>State</span><input required list="mb-states" maxlength="2" [(ngModel)]="draft.location.state" name="state" (ngModelChange)="dirty=true"></label>
|
|
508
|
-
<label><span>ZIP</span><input required [(ngModel)]="draft.location.zip" name="zip" (ngModelChange)="dirty=true"></label>
|
|
509
|
-
<label><span>Place of service</span><input required [(ngModel)]="draft.location.posCode" name="pos" (ngModelChange)="dirty=true"></label>
|
|
510
|
-
</div>
|
|
511
|
-
</section>
|
|
178
|
+
<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>
|
|
512
179
|
|
|
513
|
-
|
|
514
|
-
<div class="card-title"><div><h3>Charges</h3><p>{{ draft.bill.billingMode === 'professional' ? 'Enter each treatment or professional service.' : 'Choose medical-legal codes, modifiers, units, and allowed amounts.' }} A keyboard-ready row appears automatically.</p></div></div>
|
|
515
|
-
@if (draft.bill.billingMode === 'med_legal') {
|
|
516
|
-
<div class="presets"><button type="button" (click)="applyPreset('qme')">QME</button><button type="button" (click)="applyPreset('ame')">AME</button><button type="button" (click)="applyPreset('psych')">Psych QME</button></div>
|
|
517
|
-
}
|
|
518
|
-
<div class="lines">@for (line of draft.bill.lineItems; track $index; let i=$index) {
|
|
519
|
-
<div class="line" [class.professional]="draft.bill.billingMode === 'professional'">
|
|
520
|
-
<label><span>Procedure</span>
|
|
521
|
-
@if (draft.bill.billingMode === 'med_legal') {
|
|
522
|
-
<select [(ngModel)]="line.code" [name]="'code'+i" (ngModelChange)="lineChanged()"><option value="">Choose code…</option>@for (code of medLegalCodes; track code) { <option [value]="code">{{ code }}</option> }</select>
|
|
523
|
-
} @else { <input [(ngModel)]="line.code" [name]="'code'+i" (ngModelChange)="lineChanged()" placeholder="e.g. 99204"> }
|
|
524
|
-
</label>
|
|
525
|
-
<label><span>Modifiers</span><input [ngModel]="line.modifiers.join(', ')" (ngModelChange)="setModifiers(line,$any($event))" [name]="'modifiers'+i" placeholder="e.g. 25, 95"></label>
|
|
526
|
-
@if (draft.bill.billingMode === 'professional') {
|
|
527
|
-
<label><span>Service date</span><input type="date" [(ngModel)]="line.serviceDate" [name]="'serviceDate'+i" (ngModelChange)="lineChanged()"></label>
|
|
528
|
-
<label><span>End date</span><input type="date" [(ngModel)]="line.serviceDateEnd" [name]="'serviceDateEnd'+i" (ngModelChange)="lineChanged()"></label>
|
|
529
|
-
<label><span>Charge</span><input type="number" min="0" step="0.01" [(ngModel)]="line.charge" [name]="'charge'+i" (ngModelChange)="lineChanged()"></label>
|
|
530
|
-
<label><span>Diagnosis pointers</span><input [ngModel]="(line.diagnosisPointers || []).join(', ')" (ngModelChange)="setDiagnosisPointers(line,$any($event))" [name]="'pointers'+i" placeholder="1, 2"></label>
|
|
531
|
-
}
|
|
532
|
-
<label><span>Units</span><input type="number" min="1" [(ngModel)]="line.units" [name]="'units'+i" (ngModelChange)="lineChanged()"></label>
|
|
533
|
-
<div class="allowed"><span>{{ draft.bill.billingMode === 'professional' ? 'Charge' : 'Allowed' }}</span><strong>{{ line.charge | currency }}</strong></div>
|
|
534
|
-
<button type="button" class="remove" (click)="removeLine(i)" aria-label="Remove line">×</button>
|
|
535
|
-
</div>
|
|
536
|
-
}</div>
|
|
537
|
-
</section>
|
|
180
|
+
<section class="card"><div class="card-title"><div><h3>Bill history</h3><p>Submission, payer responses, follow-up, and payments.</p></div><span>{{ data.activity.length }}</span></div><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></section>
|
|
538
181
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
<ul class="docs">@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><button type="button" (click)="removeAttachment(doc.id)">Remove</button></li> }</ul>
|
|
542
|
-
<div class="upload"><select [(ngModel)]="documentType" name="documentType">@for (type of documentTypes; track type.value) { <option [value]="type.value">{{ type.label }}</option> }</select><input #fileInput type="file" accept="application/pdf" (change)="fileSelected($event)"><button type="button" [disabled]="!pendingFile || store.mutating()" (click)="attach()">Attach document</button></div>
|
|
543
|
-
</section>
|
|
182
|
+
@if (notice) { <p class="notice">{{ notice }}</p> }
|
|
183
|
+
<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>
|
|
544
184
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
<div class="modal-backdrop" (click)="closeSubmit()">
|
|
549
|
-
<section class="submit-modal" role="dialog" aria-modal="true" aria-labelledby="mb-submit-title" (click)="$event.stopPropagation()">
|
|
550
|
-
<header><div><span class="eyebrow">Send bill</span><h3 id="mb-submit-title">Choose a delivery route</h3><p>{{ deliveryOptions.payerName }}</p></div><button type="button" class="close" (click)="closeSubmit()" aria-label="Close">×</button></header>
|
|
551
|
-
<div class="route-list">@for (option of deliveryOptions.options; track option.route) {
|
|
552
|
-
<button type="button" class="route-option" [class.selected]="submission.route === option.route" (click)="chooseDelivery(option)"><span class="radio"></span><span><strong>{{ option.label }}</strong><small>{{ option.detail }}</small></span>@if (option.route === deliveryOptions.recommended.route) { <b>Recommended</b> }</button>
|
|
553
|
-
}</div>
|
|
554
|
-
@if (selectedDelivery(); as option) {
|
|
555
|
-
@if (option.route === 'fax') { <label><span>Fax number</span><input [ngModel]="submission.destination?.faxNumber || ''" (ngModelChange)="setDestination('faxNumber',$any($event))" name="submitFax"></label> }
|
|
556
|
-
@if (option.route === 'email') { <label><span>Email address</span><input type="email" [ngModel]="submission.destination?.email || ''" (ngModelChange)="setDestination('email',$any($event))" name="submitEmail"></label><label><span>Subject</span><input [(ngModel)]="submission.subject" name="submitSubject"></label> }
|
|
557
|
-
@if (option.route === 'mail') { <label><span>Mailing address</span><textarea [ngModel]="submission.destination?.mailingAddress || ''" (ngModelChange)="setDestination('mailingAddress',$any($event))" name="submitMail"></textarea></label> }
|
|
558
|
-
@if (option.route !== 'ebill') { <label><span>Attention (optional)</span><input [(ngModel)]="submission.attention" name="submitAttention"></label> }
|
|
559
|
-
}
|
|
560
|
-
<footer><button type="button" (click)="closeSubmit()">Cancel</button><button type="button" class="primary" (click)="submit()" [disabled]="store.mutating() || !deliveryReady()">{{ store.mutating() ? 'Submitting…' : 'Submit bill' }}</button></footer>
|
|
561
|
-
</section>
|
|
562
|
-
</div>
|
|
563
|
-
}
|
|
564
|
-
</form>
|
|
565
|
-
} @else {
|
|
566
|
-
<section class="card lifecycle">
|
|
567
|
-
<div class="card-title"><div><h3>Billing activity</h3><p>Available actions change automatically with bill status.</p></div><button (click)="store.refresh()">Refresh</button></div>
|
|
568
|
-
<div class="lifecycle-grid">
|
|
569
|
-
<section><h4>Remittance</h4><dl class="facts"><div><dt>Payer reported</dt><dd>{{ data.remittance.payerReportedPaid == null ? '—' : (data.remittance.payerReportedPaid | currency) }}</dd></div><div><dt>Posted</dt><dd>{{ data.remittance.totalPaid | currency }}</dd></div><div><dt>Balance</dt><dd>{{ data.remittance.balanceDue | currency }}</dd></div></dl>@if (data.remittance.denialReason) { <div class="denial"><strong>Denial reason</strong><p>{{ data.remittance.denialReason }}</p></div> }</section>
|
|
570
|
-
<section><h4>{{ data.delivery.payerName || 'Claims administrator' }}</h4><dl class="contacts">@if (data.delivery.contacts.adjusterName) { <div><dt>Adjuster</dt><dd>{{ data.delivery.contacts.adjusterName }}</dd></div> }@if (data.delivery.contacts.adjusterPhone) { <div><dt>Phone</dt><dd><a [href]="'tel:'+data.delivery.contacts.adjusterPhone">{{ data.delivery.contacts.adjusterPhone }}</a></dd></div> }@if (data.delivery.contacts.adjusterEmail) { <div><dt>Adjuster email</dt><dd><a [href]="'mailto:'+data.delivery.contacts.adjusterEmail">{{ data.delivery.contacts.adjusterEmail }}</a></dd></div> }@if (data.delivery.contacts.faxNumber) { <div><dt>Fax</dt><dd><a [href]="'tel:'+data.delivery.contacts.faxNumber">{{ data.delivery.contacts.faxNumber }}</a></dd></div> }@if (data.delivery.contacts.claimsEmail) { <div><dt>Claims email</dt><dd><a [href]="'mailto:'+data.delivery.contacts.claimsEmail">{{ data.delivery.contacts.claimsEmail }}</a></dd></div> }@if (data.delivery.contacts.mailingAddress) { <div><dt>Mail</dt><dd>{{ data.delivery.contacts.mailingAddress }}</dd></div> }@if (data.delivery.contacts.portalUrl) { <div><dt>Portal</dt><dd><a [href]="data.delivery.contacts.portalUrl" target="_blank" rel="noopener">Open payer portal</a></dd></div> }</dl></section>
|
|
571
|
-
</div>
|
|
572
|
-
@if (data.eors.length) { <div class="eors"><h4>Explanation of Review</h4><p>Payer response details and the original returned document.</p>@for (eor of data.eors; track eor.id) { <button (click)="openEor(eor.id,eor.filename)"><span><strong>{{ eor.filename }}</strong><small>{{ eor.description || 'Payer response' }}</small></span><b>View original PDF</b></button> }</div> }
|
|
573
|
-
<section class="payments"><h4>Payments</h4>@if (!data.payments.length) { <p>No payments posted.</p> } @else { @for (paymentItem of data.payments; track paymentItem.id) { <article><div><strong>{{ paymentItem.amount | currency }}</strong><span>{{ paymentItem.method | uppercase }} · {{ paymentItem.status }}</span></div><div><span>{{ paymentItem.checkNumber ? '#'+paymentItem.checkNumber : paymentItem.source }}</span><span>{{ paymentItem.depositDate || (paymentItem.postedAt | date:'mediumDate') }}</span></div>@if (paymentItem.note) { <p>{{ paymentItem.note }}</p> }</article> } }</section>
|
|
574
|
-
<section class="history"><h4>Bill history</h4>@if (!data.activity.length) { <p>No bill activity yet.</p> } @else { <ol>@for (event of data.activity; track event.id) { <li><span></span><div><strong>{{ activityLabel(event.type) }}</strong><small>{{ event.createdAt | date:'medium' }}{{ event.actor ? ' · '+event.actor : '' }}</small>@if (event.description) { <p>{{ event.description }}</p> }</div></li> }</ol> }</section>
|
|
575
|
-
<div class="actionbar">@for (action of data.lifecycle.actions; track action.id) { @if (action.enabled) { <button [class.primary]="action.primary" (click)="beginAction(action.id)">{{ action.label }}</button> } }</div>
|
|
576
|
-
</section>
|
|
577
|
-
@if (panel) {
|
|
578
|
-
<div class="modal-backdrop" (click)="panel=''">
|
|
579
|
-
<section class="submit-modal action-modal" role="dialog" aria-modal="true" (click)="$event.stopPropagation()">
|
|
580
|
-
<header><div><span class="eyebrow">Bill {{ data.bill.billNumber }}</span><h3>{{ actionTitle() }}</h3></div><button class="close" type="button" (click)="panel=''" aria-label="Close">×</button></header>
|
|
581
|
-
@if (panel === 'payment') { <p>Record funds received for this bill. This updates the payment ledger and remaining balance.</p><div class="grid two"><label><span>Amount</span><input type="number" min="0.01" [(ngModel)]="payment.amount"></label><label><span>Method</span><select [(ngModel)]="payment.method"><option value="check">Check</option><option value="eft">EFT</option></select></label><label><span>Check / EFT number</span><input [(ngModel)]="payment.checkNumber"></label><label><span>Deposit date</span><input type="date" [(ngModel)]="payment.depositDate"></label></div><label><span>Note (optional)</span><input [(ngModel)]="payment.note"></label> }
|
|
582
|
-
@if (panel === 'review') { <p>Explain the dispute and choose the exact payer documents to include with the Second Review.</p><label><span>Reason for Second Review</span><textarea [(ngModel)]="review.reason"></textarea></label><div class="grid two"><label><span>Payer control number</span><input [(ngModel)]="review.payerClaimControlNumber"></label><label><span>Disputed amount</span><input type="number" [(ngModel)]="review.disputedAmount"></label></div><fieldset class="review-docs"><legend>Supporting documents</legend>@for (doc of data.bill.attachments; track doc.id) { <label><input type="checkbox" [checked]="reviewAttachmentIds.includes(doc.id)" (change)="toggleReviewAttachment(doc.id)"><span><strong>{{ doc.filename }}</strong><small>{{ doc.description || doc.documentType }}</small></span></label> }</fieldset><div class="upload"><select [(ngModel)]="documentType">@for (type of documentTypes; track type.value) { <option [value]="type.value">{{ type.label }}</option> }</select><input type="file" accept="application/pdf" (change)="fileSelected($event)"><button type="button" [disabled]="!pendingFile || store.mutating()" (click)="attachForReview()">Add document</button></div> }
|
|
583
|
-
@if (panel === 'correction') { <p>Create a replacement bill prefilled from this rejected submission. You can correct the payer, claim, provider, service lines, and packet before resubmitting.</p> }
|
|
584
|
-
@if (panel === 'close') { <p>Closing stops active follow-up. The bill history and documents remain available.</p><label><span>Reason</span><textarea [(ngModel)]="closeReason"></textarea></label> }
|
|
585
|
-
<footer><button type="button" (click)="panel=''">Cancel</button>@if (panel === 'payment') { <button class="primary" type="button" (click)="postPayment()" [disabled]="store.mutating() || payment.amount <= 0">Post payment</button> }@if (panel === 'review') { <button class="primary" type="button" (click)="submitReview()" [disabled]="store.mutating() || !review.reason.trim() || !review.payerClaimControlNumber.trim()">Submit Second Review</button> }@if (panel === 'correction') { <button class="primary" type="button" (click)="correct()" [disabled]="store.mutating()">Create correction</button> }@if (panel === 'close') { <button class="danger" type="button" (click)="closeBill()" [disabled]="store.mutating() || !closeReason.trim()">Close bill</button> }</footer>
|
|
586
|
-
</section>
|
|
587
|
-
</div>
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
@if (notice) { <div class="notice">{{ notice }}</div> }
|
|
591
|
-
@if (store.error()) { <div class="notice error">{{ store.error()?.message }}</div> }
|
|
592
|
-
<footer class="powered">Powered by MindBill</footer>
|
|
185
|
+
@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> }
|
|
186
|
+
@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> }
|
|
187
|
+
@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> }
|
|
593
188
|
}
|
|
594
|
-
<datalist id="mb-states"><option value="CA"></option><option value="AZ"></option><option value="NV"></option><option value="OR"></option><option value="WA"></option><option value="TX"></option><option value="NY"></option></datalist>
|
|
595
189
|
</section>
|
|
596
|
-
`, isInline: true, styles: [":host{display:block}.mb{--a:#238dbd;--ac:#fff;--bg:#f3f8fa;--s:#fff;--t:#203743;--m:#657982;--b:#dbe6ea;--r:12px;--cr:8px;display:grid;gap:16px;color:var(--t);font:14px/1.45 var(--font,Inter,system-ui,sans-serif)}*{box-sizing:border-box}h2,h3,h4,p{margin:0}a{color:var(--a)}.summary,.card,.delivery,.state{border:1px solid var(--b);border-radius:var(--r);background:var(--s)}.summary{display:flex;justify-content:space-between;gap:20px;padding:20px}.summary h2{font-size:24px}.summary p,.card p,.delivery p,small{color:var(--m)}.eyebrow{color:var(--m);font-size:11px;font-weight:800;letter-spacing:.13em;text-transform:uppercase}.money{text-align:right}.money span{display:block;font-size:28px;font-weight:800}.money small{font-size:12px}.review{display:grid;gap:16px}.card{padding:20px}.card-title{display:flex;align-items:start;justify-content:space-between;gap:16px;margin-bottom:18px}.card-title h3,.delivery h3{font-size:18px}.card-title>span{border-radius:999px;background:var(--bg);padding:5px 9px;color:var(--m);font-size:11px;font-weight:800;text-transform:uppercase}.grid{display:grid;gap:14px}.grid.four{grid-template-columns:repeat(4,minmax(0,1fr))}.grid.three{grid-template-columns:repeat(3,minmax(0,1fr))}.grid.two{grid-template-columns:repeat(2,minmax(0,1fr))}label{display:grid;gap:6px;color:var(--t);font-size:12px;font-weight:700}input,select,textarea,button{font:inherit}input,select,textarea{width:100%;min-height:42px;border:1px solid var(--b);border-radius:var(--cr);background:#fff;color:var(--t);padding:9px 11px}textarea{min-height:90px;resize:vertical}button{min-height:38px;border:1px solid var(--b);border-radius:var(--cr);background:#fff;color:var(--t);padding:8px 13px;cursor:pointer;font-weight:700}button.primary{border-color:var(--a);background:var(--a);color:var(--ac)}button.danger{border-color:#d4380d;background:#d4380d;color:#fff}button:disabled{cursor:not-allowed;opacity:.5}.subhead{margin:18px 0 9px;border-top:1px solid var(--b);padding-top:15px;font-size:12px;font-weight:800}.selected{display:flex;align-items:center;justify-content:space-between;border:1px solid var(--b);border-radius:var(--cr);padding:12px}.selected div{display:grid}.results{position:relative;z-index:3;display:grid;margin-top:5px;border:1px solid var(--b);border-radius:var(--cr);background:#fff;box-shadow:0 12px 30px #1f2d3d1f;overflow:hidden}.results button{display:grid;gap:2px;text-align:left;border:0;border-bottom:1px solid var(--b);border-radius:0;padding:12px}.results span{color:var(--m);font-weight:400}.presets{display:flex;gap:8px;margin:-4px 0 12px}.lines{display:grid;gap:10px}.line{display:grid;grid-template-columns:1.2fr 1.2fr 110px 120px 40px;align-items:end;gap:10px;border-radius:var(--cr);background:var(--bg);padding:12px}.line.professional{grid-template-columns:1.1fr 1fr 1fr 1fr 110px 1fr 90px 110px 40px}.allowed{display:grid;gap:7px;text-align:right}.allowed span{color:var(--m);font-size:11px;font-weight:800;text-transform:uppercase}.allowed strong{font-size:17px}.remove{border:0;background:transparent;font-size:20px}.docs{list-style:none;margin:0;padding:0}.docs li{display:flex;align-items:center;gap:8px;border-top:1px solid var(--b);padding:11px 0}.docs li>div{display:grid;flex:1}.upload{display:grid;grid-template-columns:240px 1fr auto;gap:10px;margin-top:12px;border-radius:var(--cr);background:var(--bg);padding:12px}.delivery{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:20px;background:color-mix(in srgb,var(--a) 5%,white)}.actions,.actionbar{display:flex;justify-content:flex-end;gap:8px}.progress ol{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));list-style:none;margin:0;padding:0}.progress li{position:relative;display:grid;justify-items:center;gap:7px;color:var(--m);font-size:12px;text-align:center}.progress li:before{content:\"\";position:absolute;top:14px;right:50%;left:-50%;height:2px;background:var(--b)}.progress li:first-child:before{display:none}.progress li b{position:relative;z-index:1;display:grid;place-items:center;width:28px;height:28px;border:2px solid var(--b);border-radius:50%;background:var(--s)}.progress li.complete,.progress li.current{color:var(--t)}.progress li.complete:before,.progress li.current:before{background:var(--a)}.progress li.complete b,.progress li.current b{border-color:var(--a);background:var(--a);color:var(--ac)}.snapshot dl,.facts{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));margin:0}.snapshot dl>div,.facts>div{display:grid;gap:3px;border-right:1px solid var(--b);padding:4px 14px}.snapshot dl>div:nth-child(3n),.facts>div:last-child{border-right:0}.snapshot dt,.facts dt,.contacts dt{color:var(--m);font-size:11px;font-weight:800;text-transform:uppercase}.snapshot dd,.facts dd,.contacts dd{margin:0;font-weight:700}.lifecycle{display:grid;gap:20px}.actionbar{flex-wrap:wrap;border-top:1px solid var(--b);padding-top:18px}.lifecycle-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}.lifecycle-grid>section,.payments,.history,.eors{display:grid;gap:11px;border:1px solid var(--b);border-radius:var(--cr);padding:15px}.contacts{display:grid;gap:9px;margin:0}.contacts>div{display:grid;grid-template-columns:70px 1fr;gap:10px}.denial{border-radius:var(--cr);background:#fff4ed;padding:11px}.eors>button{display:flex;align-items:center;justify-content:space-between;text-align:left}.eors span{display:grid}.payments article{display:grid;grid-template-columns:1fr auto;gap:4px;border-top:1px solid var(--b);padding-top:10px}.payments article>div{display:grid}.payments article>div:last-of-type{text-align:right}.payments article p{grid-column:1/-1}.history ol{display:grid;gap:0;list-style:none;margin:0;padding:0}.history li{display:grid;grid-template-columns:18px 1fr;gap:9px}.history li>span{position:relative;margin-top:5px;width:10px;height:10px;border-radius:50%;background:var(--a)}.history li>span:after{content:\"\";position:absolute;top:10px;bottom:-30px;left:4px;width:2px;background:var(--b)}.history li:last-child>span:after{display:none}.history li>div{display:grid;gap:2px;padding-bottom:15px}.notice,.state{padding:13px 15px}.notice{border-radius:var(--cr);background:#edf8f2;color:#23734c}.notice.error,.state.error{background:#fff2f0;color:#b42318}.state{display:grid;gap:10px}.state button{justify-self:start}.powered{text-align:right;color:var(--m);font-size:11px}.modal-backdrop{position:fixed;z-index:1000;inset:0;display:grid;place-items:center;background:#0f20288f;padding:20px}.submit-modal{display:grid;gap:18px;width:min(680px,100%);max-height:calc(100vh - 40px);overflow:auto;border:1px solid var(--b);border-radius:var(--r);background:var(--s);padding:22px;box-shadow:0 24px 80px #0f202847}.submit-modal>header,.submit-modal>footer{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.submit-modal>footer{justify-content:flex-end}.submit-modal .close{border:0;background:transparent;font-size:26px}.route-list{display:grid;gap:8px}.route-option{display:grid;grid-template-columns:20px 1fr auto;align-items:center;gap:12px;text-align:left;padding:13px}.route-option>span:nth-child(2){display:grid}.route-option.selected{border-color:var(--a);box-shadow:0 0 0 2px color-mix(in srgb,var(--a) 18%,transparent)}.route-option .radio{width:16px;height:16px;border:2px solid var(--b);border-radius:50%}.route-option.selected .radio{border:5px solid var(--a)}.route-option b{color:var(--a);font-size:11px;text-transform:uppercase}.action-modal{width:min(720px,100%)}.review-docs{display:grid;gap:8px;border:1px solid var(--b);border-radius:var(--cr);padding:12px}.review-docs label{display:grid;grid-template-columns:20px 1fr;align-items:start}.review-docs input{min-height:auto}.review-docs label>span{display:grid}@media(max-width:1100px){.line.professional{grid-template-columns:repeat(3,minmax(0,1fr))}.line.professional .allowed{grid-column:auto}}@media(max-width:900px){.grid.four,.grid.three{grid-template-columns:repeat(2,minmax(0,1fr))}.line{grid-template-columns:1fr 1fr 90px}.allowed{grid-column:1/-2;text-align:left}.upload{grid-template-columns:1fr}.delivery{align-items:stretch;flex-direction:column}.actions{justify-content:flex-start}.lifecycle-grid{grid-template-columns:1fr}}@media(max-width:600px){.summary,.card-title{align-items:stretch;flex-direction:column}.money{text-align:left}.grid.four,.grid.three,.grid.two,.line,.line.professional{grid-template-columns:1fr}.allowed{grid-column:auto}.docs li{align-items:flex-start;flex-wrap:wrap}.progress{overflow-x:auto}.progress ol{min-width:560px}.snapshot dl,.facts{grid-template-columns:1fr}.snapshot dl>div,.facts>div{border-right:0;border-bottom:1px solid var(--b);padding:10px 0}.snapshot dl>div:last-child,.facts>div:last-child{border-bottom:0}.modal-backdrop{align-items:end;padding:0}.submit-modal{max-height:92vh;border-radius:var(--r) var(--r) 0 0;padding:16px}.route-option{grid-template-columns:20px 1fr}.route-option b{grid-column:2}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "pipe", type: i1.UpperCasePipe, name: "uppercase" }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }, { kind: "pipe", type: i1.DatePipe, name: "date" }] });
|
|
190
|
+
`, isInline: true, styles: [":host{display:block}.mb{font-family:var(--font);color:var(--t);background:var(--bg);padding:20px}.summary,.card,.columns{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}}\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: "pipe", type: i1.UpperCasePipe, name: "uppercase" }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }, { kind: "pipe", type: i1.DatePipe, name: "date" }] });
|
|
597
191
|
}
|
|
598
192
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillLifecycleComponent, decorators: [{
|
|
599
193
|
type: Component,
|
|
600
194
|
args: [{ selector: "mindbill-bill-lifecycle", standalone: true, imports: [CommonModule, FormsModule], template: `
|
|
601
195
|
<section class="mb" [ngStyle]="themeStyle">
|
|
602
|
-
@if (
|
|
196
|
+
@if (!billId) {
|
|
197
|
+
<div class="state error"><strong>A submitted bill ID is required.</strong></div>
|
|
198
|
+
} @else if (store.loading() && !store.data()) {
|
|
603
199
|
<div class="state">Loading billing…</div>
|
|
604
200
|
} @else if (store.error() && !store.data()) {
|
|
605
|
-
<div class="state error"><strong>Billing is temporarily unavailable</strong><span>{{ store.error()?.message }}</span><button (click)="store.refresh()">Try again</button></div>
|
|
201
|
+
<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>
|
|
606
202
|
} @else if (store.data(); as data) {
|
|
607
203
|
<header class="summary">
|
|
608
204
|
<div><span class="eyebrow">Bill {{ data.bill.billNumber }}</span><h2>{{ stateLabel(data) }}</h2><p>{{ lifecycleDetail(data) }}</p></div>
|
|
@@ -610,153 +206,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
610
206
|
</header>
|
|
611
207
|
|
|
612
208
|
<section class="card progress" aria-label="Bill lifecycle">
|
|
613
|
-
<
|
|
614
|
-
<ol>@for (stage of lifecycleStages; track stage.id; let i=$index) { <li [class.complete]="i < currentStageIndex(data)" [class.current]="i === currentStageIndex(data)" [attr.aria-current]="i === currentStageIndex(data) ? 'step' : null"><b>{{ i < currentStageIndex(data) ? '✓' : i + 1 }}</b><span>{{ stage.label }}</span></li> }</ol>
|
|
209
|
+
<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>
|
|
615
210
|
</section>
|
|
616
211
|
|
|
617
212
|
<section class="card snapshot" aria-label="Bill snapshot">
|
|
618
|
-
<div class="card-title"><div><h3>Bill snapshot</h3><p>The
|
|
213
|
+
<div class="card-title"><div><h3>Bill snapshot</h3><p>The immutable values submitted to the payer.</p></div></div>
|
|
619
214
|
<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>
|
|
620
215
|
</section>
|
|
621
216
|
|
|
622
|
-
|
|
623
|
-
<
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
<div class="grid four">
|
|
627
|
-
<label><span>First name</span><input required [(ngModel)]="draft.patient.firstName" name="firstName" (ngModelChange)="dirty=true"></label>
|
|
628
|
-
<label><span>Last name</span><input required [(ngModel)]="draft.patient.lastName" name="lastName" (ngModelChange)="dirty=true"></label>
|
|
629
|
-
<label><span>Date of birth</span><input type="date" [(ngModel)]="draft.patient.dob" name="dob" (ngModelChange)="dirty=true"></label>
|
|
630
|
-
<label><span>Date of injury</span><input type="date" [(ngModel)]="draft.injury.doi" name="doi" (ngModelChange)="dirty=true"></label>
|
|
631
|
-
<label><span>Claim number</span><input [(ngModel)]="draft.injury.claimNumber" name="claimNumber" (ngModelChange)="dirty=true"></label>
|
|
632
|
-
<label><span>Employer</span><input [(ngModel)]="draft.injury.employer" name="employer" (ngModelChange)="dirty=true"></label>
|
|
633
|
-
<label><span>WCAB / case number</span><input [(ngModel)]="draft.injury.adjNumber" name="adjNumber" (ngModelChange)="dirty=true"></label>
|
|
634
|
-
<label><span>Date of service</span><input required type="date" [(ngModel)]="draft.bill.dos" name="dos" (ngModelChange)="dirty=true"></label>
|
|
635
|
-
</div>
|
|
636
|
-
</section>
|
|
217
|
+
<div class="columns">
|
|
218
|
+
<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 & 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>
|
|
219
|
+
<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>
|
|
220
|
+
</div>
|
|
637
221
|
|
|
638
|
-
|
|
639
|
-
<div class="card-title"><div><h3>Claims administrator</h3><p>Select who should receive this bill. Search uses payer names and claim-number patterns.</p></div><span>Required</span></div>
|
|
640
|
-
@if (draft.injury.claimsAdminId) { <div class="selected"><div><strong>{{ draft.injury.claimsAdminName }}</strong><small>Selected recipient</small></div><button type="button" (click)="clearPayer()">Change</button></div> }
|
|
641
|
-
@else {
|
|
642
|
-
<label><span>Insurance company or claims administrator</span><input name="payerSearch" [(ngModel)]="payerQuery" (ngModelChange)="queuePayerSearch()" placeholder="Search by payer or administrator name" autocomplete="off"></label>
|
|
643
|
-
@if (payerResults.length) { <div class="results">@for (payer of payerResults; track payer.id) { <button type="button" (click)="selectPayer(payer)"><strong>{{ payer.name }}</strong><span>{{ payerExplanation(payer) }}</span></button> }</div> }
|
|
644
|
-
}
|
|
645
|
-
</section>
|
|
222
|
+
<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>
|
|
646
223
|
|
|
647
|
-
|
|
648
|
-
<div class="card-title"><div><h3>Billing identity</h3><p>The payee, rendering clinician, and place of service are bill snapshots—not synchronization records.</p></div><span>Prefilled</span></div>
|
|
649
|
-
<div class="subhead">Billing provider</div>
|
|
650
|
-
<div class="grid three">
|
|
651
|
-
<label><span>Practice name</span><input required [(ngModel)]="draft.billingProvider.name" name="practiceName" (ngModelChange)="dirty=true"></label>
|
|
652
|
-
<label><span>Tax ID</span><input required [(ngModel)]="draft.billingProvider.taxId" name="taxId" (ngModelChange)="dirty=true"></label>
|
|
653
|
-
<label><span>Billing NPI</span><input required [(ngModel)]="draft.billingProvider.npi" name="billingNpi" (ngModelChange)="dirty=true"></label>
|
|
654
|
-
</div>
|
|
655
|
-
<div class="subhead">Rendering clinician</div>
|
|
656
|
-
<div class="grid three">
|
|
657
|
-
<label><span>Name</span><input required [(ngModel)]="draft.clinician.name" name="clinicianName" (ngModelChange)="dirty=true"></label>
|
|
658
|
-
<label><span>NPI</span><input required [(ngModel)]="draft.clinician.npi" name="clinicianNpi" (ngModelChange)="dirty=true"></label>
|
|
659
|
-
<label><span>Taxonomy</span><input [(ngModel)]="draft.clinician.taxonomy" name="taxonomy" (ngModelChange)="dirty=true"></label>
|
|
660
|
-
</div>
|
|
661
|
-
<div class="subhead">Service location</div>
|
|
662
|
-
<div class="grid three">
|
|
663
|
-
<label><span>Location</span><input required [(ngModel)]="draft.location.name" name="locationName" (ngModelChange)="dirty=true"></label>
|
|
664
|
-
<label><span>Street</span><input required [(ngModel)]="draft.location.street" name="street" (ngModelChange)="dirty=true"></label>
|
|
665
|
-
<label><span>City</span><input required [(ngModel)]="draft.location.city" name="city" (ngModelChange)="dirty=true"></label>
|
|
666
|
-
<label><span>State</span><input required list="mb-states" maxlength="2" [(ngModel)]="draft.location.state" name="state" (ngModelChange)="dirty=true"></label>
|
|
667
|
-
<label><span>ZIP</span><input required [(ngModel)]="draft.location.zip" name="zip" (ngModelChange)="dirty=true"></label>
|
|
668
|
-
<label><span>Place of service</span><input required [(ngModel)]="draft.location.posCode" name="pos" (ngModelChange)="dirty=true"></label>
|
|
669
|
-
</div>
|
|
670
|
-
</section>
|
|
224
|
+
<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>
|
|
671
225
|
|
|
672
|
-
|
|
673
|
-
<div class="card-title"><div><h3>Charges</h3><p>{{ draft.bill.billingMode === 'professional' ? 'Enter each treatment or professional service.' : 'Choose medical-legal codes, modifiers, units, and allowed amounts.' }} A keyboard-ready row appears automatically.</p></div></div>
|
|
674
|
-
@if (draft.bill.billingMode === 'med_legal') {
|
|
675
|
-
<div class="presets"><button type="button" (click)="applyPreset('qme')">QME</button><button type="button" (click)="applyPreset('ame')">AME</button><button type="button" (click)="applyPreset('psych')">Psych QME</button></div>
|
|
676
|
-
}
|
|
677
|
-
<div class="lines">@for (line of draft.bill.lineItems; track $index; let i=$index) {
|
|
678
|
-
<div class="line" [class.professional]="draft.bill.billingMode === 'professional'">
|
|
679
|
-
<label><span>Procedure</span>
|
|
680
|
-
@if (draft.bill.billingMode === 'med_legal') {
|
|
681
|
-
<select [(ngModel)]="line.code" [name]="'code'+i" (ngModelChange)="lineChanged()"><option value="">Choose code…</option>@for (code of medLegalCodes; track code) { <option [value]="code">{{ code }}</option> }</select>
|
|
682
|
-
} @else { <input [(ngModel)]="line.code" [name]="'code'+i" (ngModelChange)="lineChanged()" placeholder="e.g. 99204"> }
|
|
683
|
-
</label>
|
|
684
|
-
<label><span>Modifiers</span><input [ngModel]="line.modifiers.join(', ')" (ngModelChange)="setModifiers(line,$any($event))" [name]="'modifiers'+i" placeholder="e.g. 25, 95"></label>
|
|
685
|
-
@if (draft.bill.billingMode === 'professional') {
|
|
686
|
-
<label><span>Service date</span><input type="date" [(ngModel)]="line.serviceDate" [name]="'serviceDate'+i" (ngModelChange)="lineChanged()"></label>
|
|
687
|
-
<label><span>End date</span><input type="date" [(ngModel)]="line.serviceDateEnd" [name]="'serviceDateEnd'+i" (ngModelChange)="lineChanged()"></label>
|
|
688
|
-
<label><span>Charge</span><input type="number" min="0" step="0.01" [(ngModel)]="line.charge" [name]="'charge'+i" (ngModelChange)="lineChanged()"></label>
|
|
689
|
-
<label><span>Diagnosis pointers</span><input [ngModel]="(line.diagnosisPointers || []).join(', ')" (ngModelChange)="setDiagnosisPointers(line,$any($event))" [name]="'pointers'+i" placeholder="1, 2"></label>
|
|
690
|
-
}
|
|
691
|
-
<label><span>Units</span><input type="number" min="1" [(ngModel)]="line.units" [name]="'units'+i" (ngModelChange)="lineChanged()"></label>
|
|
692
|
-
<div class="allowed"><span>{{ draft.bill.billingMode === 'professional' ? 'Charge' : 'Allowed' }}</span><strong>{{ line.charge | currency }}</strong></div>
|
|
693
|
-
<button type="button" class="remove" (click)="removeLine(i)" aria-label="Remove line">×</button>
|
|
694
|
-
</div>
|
|
695
|
-
}</div>
|
|
696
|
-
</section>
|
|
226
|
+
<section class="card"><div class="card-title"><div><h3>Bill history</h3><p>Submission, payer responses, follow-up, and payments.</p></div><span>{{ data.activity.length }}</span></div><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></section>
|
|
697
227
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
<ul class="docs">@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><button type="button" (click)="removeAttachment(doc.id)">Remove</button></li> }</ul>
|
|
701
|
-
<div class="upload"><select [(ngModel)]="documentType" name="documentType">@for (type of documentTypes; track type.value) { <option [value]="type.value">{{ type.label }}</option> }</select><input #fileInput type="file" accept="application/pdf" (change)="fileSelected($event)"><button type="button" [disabled]="!pendingFile || store.mutating()" (click)="attach()">Attach document</button></div>
|
|
702
|
-
</section>
|
|
228
|
+
@if (notice) { <p class="notice">{{ notice }}</p> }
|
|
229
|
+
<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>
|
|
703
230
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
<div class="modal-backdrop" (click)="closeSubmit()">
|
|
708
|
-
<section class="submit-modal" role="dialog" aria-modal="true" aria-labelledby="mb-submit-title" (click)="$event.stopPropagation()">
|
|
709
|
-
<header><div><span class="eyebrow">Send bill</span><h3 id="mb-submit-title">Choose a delivery route</h3><p>{{ deliveryOptions.payerName }}</p></div><button type="button" class="close" (click)="closeSubmit()" aria-label="Close">×</button></header>
|
|
710
|
-
<div class="route-list">@for (option of deliveryOptions.options; track option.route) {
|
|
711
|
-
<button type="button" class="route-option" [class.selected]="submission.route === option.route" (click)="chooseDelivery(option)"><span class="radio"></span><span><strong>{{ option.label }}</strong><small>{{ option.detail }}</small></span>@if (option.route === deliveryOptions.recommended.route) { <b>Recommended</b> }</button>
|
|
712
|
-
}</div>
|
|
713
|
-
@if (selectedDelivery(); as option) {
|
|
714
|
-
@if (option.route === 'fax') { <label><span>Fax number</span><input [ngModel]="submission.destination?.faxNumber || ''" (ngModelChange)="setDestination('faxNumber',$any($event))" name="submitFax"></label> }
|
|
715
|
-
@if (option.route === 'email') { <label><span>Email address</span><input type="email" [ngModel]="submission.destination?.email || ''" (ngModelChange)="setDestination('email',$any($event))" name="submitEmail"></label><label><span>Subject</span><input [(ngModel)]="submission.subject" name="submitSubject"></label> }
|
|
716
|
-
@if (option.route === 'mail') { <label><span>Mailing address</span><textarea [ngModel]="submission.destination?.mailingAddress || ''" (ngModelChange)="setDestination('mailingAddress',$any($event))" name="submitMail"></textarea></label> }
|
|
717
|
-
@if (option.route !== 'ebill') { <label><span>Attention (optional)</span><input [(ngModel)]="submission.attention" name="submitAttention"></label> }
|
|
718
|
-
}
|
|
719
|
-
<footer><button type="button" (click)="closeSubmit()">Cancel</button><button type="button" class="primary" (click)="submit()" [disabled]="store.mutating() || !deliveryReady()">{{ store.mutating() ? 'Submitting…' : 'Submit bill' }}</button></footer>
|
|
720
|
-
</section>
|
|
721
|
-
</div>
|
|
722
|
-
}
|
|
723
|
-
</form>
|
|
724
|
-
} @else {
|
|
725
|
-
<section class="card lifecycle">
|
|
726
|
-
<div class="card-title"><div><h3>Billing activity</h3><p>Available actions change automatically with bill status.</p></div><button (click)="store.refresh()">Refresh</button></div>
|
|
727
|
-
<div class="lifecycle-grid">
|
|
728
|
-
<section><h4>Remittance</h4><dl class="facts"><div><dt>Payer reported</dt><dd>{{ data.remittance.payerReportedPaid == null ? '—' : (data.remittance.payerReportedPaid | currency) }}</dd></div><div><dt>Posted</dt><dd>{{ data.remittance.totalPaid | currency }}</dd></div><div><dt>Balance</dt><dd>{{ data.remittance.balanceDue | currency }}</dd></div></dl>@if (data.remittance.denialReason) { <div class="denial"><strong>Denial reason</strong><p>{{ data.remittance.denialReason }}</p></div> }</section>
|
|
729
|
-
<section><h4>{{ data.delivery.payerName || 'Claims administrator' }}</h4><dl class="contacts">@if (data.delivery.contacts.adjusterName) { <div><dt>Adjuster</dt><dd>{{ data.delivery.contacts.adjusterName }}</dd></div> }@if (data.delivery.contacts.adjusterPhone) { <div><dt>Phone</dt><dd><a [href]="'tel:'+data.delivery.contacts.adjusterPhone">{{ data.delivery.contacts.adjusterPhone }}</a></dd></div> }@if (data.delivery.contacts.adjusterEmail) { <div><dt>Adjuster email</dt><dd><a [href]="'mailto:'+data.delivery.contacts.adjusterEmail">{{ data.delivery.contacts.adjusterEmail }}</a></dd></div> }@if (data.delivery.contacts.faxNumber) { <div><dt>Fax</dt><dd><a [href]="'tel:'+data.delivery.contacts.faxNumber">{{ data.delivery.contacts.faxNumber }}</a></dd></div> }@if (data.delivery.contacts.claimsEmail) { <div><dt>Claims email</dt><dd><a [href]="'mailto:'+data.delivery.contacts.claimsEmail">{{ data.delivery.contacts.claimsEmail }}</a></dd></div> }@if (data.delivery.contacts.mailingAddress) { <div><dt>Mail</dt><dd>{{ data.delivery.contacts.mailingAddress }}</dd></div> }@if (data.delivery.contacts.portalUrl) { <div><dt>Portal</dt><dd><a [href]="data.delivery.contacts.portalUrl" target="_blank" rel="noopener">Open payer portal</a></dd></div> }</dl></section>
|
|
730
|
-
</div>
|
|
731
|
-
@if (data.eors.length) { <div class="eors"><h4>Explanation of Review</h4><p>Payer response details and the original returned document.</p>@for (eor of data.eors; track eor.id) { <button (click)="openEor(eor.id,eor.filename)"><span><strong>{{ eor.filename }}</strong><small>{{ eor.description || 'Payer response' }}</small></span><b>View original PDF</b></button> }</div> }
|
|
732
|
-
<section class="payments"><h4>Payments</h4>@if (!data.payments.length) { <p>No payments posted.</p> } @else { @for (paymentItem of data.payments; track paymentItem.id) { <article><div><strong>{{ paymentItem.amount | currency }}</strong><span>{{ paymentItem.method | uppercase }} · {{ paymentItem.status }}</span></div><div><span>{{ paymentItem.checkNumber ? '#'+paymentItem.checkNumber : paymentItem.source }}</span><span>{{ paymentItem.depositDate || (paymentItem.postedAt | date:'mediumDate') }}</span></div>@if (paymentItem.note) { <p>{{ paymentItem.note }}</p> }</article> } }</section>
|
|
733
|
-
<section class="history"><h4>Bill history</h4>@if (!data.activity.length) { <p>No bill activity yet.</p> } @else { <ol>@for (event of data.activity; track event.id) { <li><span></span><div><strong>{{ activityLabel(event.type) }}</strong><small>{{ event.createdAt | date:'medium' }}{{ event.actor ? ' · '+event.actor : '' }}</small>@if (event.description) { <p>{{ event.description }}</p> }</div></li> }</ol> }</section>
|
|
734
|
-
<div class="actionbar">@for (action of data.lifecycle.actions; track action.id) { @if (action.enabled) { <button [class.primary]="action.primary" (click)="beginAction(action.id)">{{ action.label }}</button> } }</div>
|
|
735
|
-
</section>
|
|
736
|
-
@if (panel) {
|
|
737
|
-
<div class="modal-backdrop" (click)="panel=''">
|
|
738
|
-
<section class="submit-modal action-modal" role="dialog" aria-modal="true" (click)="$event.stopPropagation()">
|
|
739
|
-
<header><div><span class="eyebrow">Bill {{ data.bill.billNumber }}</span><h3>{{ actionTitle() }}</h3></div><button class="close" type="button" (click)="panel=''" aria-label="Close">×</button></header>
|
|
740
|
-
@if (panel === 'payment') { <p>Record funds received for this bill. This updates the payment ledger and remaining balance.</p><div class="grid two"><label><span>Amount</span><input type="number" min="0.01" [(ngModel)]="payment.amount"></label><label><span>Method</span><select [(ngModel)]="payment.method"><option value="check">Check</option><option value="eft">EFT</option></select></label><label><span>Check / EFT number</span><input [(ngModel)]="payment.checkNumber"></label><label><span>Deposit date</span><input type="date" [(ngModel)]="payment.depositDate"></label></div><label><span>Note (optional)</span><input [(ngModel)]="payment.note"></label> }
|
|
741
|
-
@if (panel === 'review') { <p>Explain the dispute and choose the exact payer documents to include with the Second Review.</p><label><span>Reason for Second Review</span><textarea [(ngModel)]="review.reason"></textarea></label><div class="grid two"><label><span>Payer control number</span><input [(ngModel)]="review.payerClaimControlNumber"></label><label><span>Disputed amount</span><input type="number" [(ngModel)]="review.disputedAmount"></label></div><fieldset class="review-docs"><legend>Supporting documents</legend>@for (doc of data.bill.attachments; track doc.id) { <label><input type="checkbox" [checked]="reviewAttachmentIds.includes(doc.id)" (change)="toggleReviewAttachment(doc.id)"><span><strong>{{ doc.filename }}</strong><small>{{ doc.description || doc.documentType }}</small></span></label> }</fieldset><div class="upload"><select [(ngModel)]="documentType">@for (type of documentTypes; track type.value) { <option [value]="type.value">{{ type.label }}</option> }</select><input type="file" accept="application/pdf" (change)="fileSelected($event)"><button type="button" [disabled]="!pendingFile || store.mutating()" (click)="attachForReview()">Add document</button></div> }
|
|
742
|
-
@if (panel === 'correction') { <p>Create a replacement bill prefilled from this rejected submission. You can correct the payer, claim, provider, service lines, and packet before resubmitting.</p> }
|
|
743
|
-
@if (panel === 'close') { <p>Closing stops active follow-up. The bill history and documents remain available.</p><label><span>Reason</span><textarea [(ngModel)]="closeReason"></textarea></label> }
|
|
744
|
-
<footer><button type="button" (click)="panel=''">Cancel</button>@if (panel === 'payment') { <button class="primary" type="button" (click)="postPayment()" [disabled]="store.mutating() || payment.amount <= 0">Post payment</button> }@if (panel === 'review') { <button class="primary" type="button" (click)="submitReview()" [disabled]="store.mutating() || !review.reason.trim() || !review.payerClaimControlNumber.trim()">Submit Second Review</button> }@if (panel === 'correction') { <button class="primary" type="button" (click)="correct()" [disabled]="store.mutating()">Create correction</button> }@if (panel === 'close') { <button class="danger" type="button" (click)="closeBill()" [disabled]="store.mutating() || !closeReason.trim()">Close bill</button> }</footer>
|
|
745
|
-
</section>
|
|
746
|
-
</div>
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
@if (notice) { <div class="notice">{{ notice }}</div> }
|
|
750
|
-
@if (store.error()) { <div class="notice error">{{ store.error()?.message }}</div> }
|
|
751
|
-
<footer class="powered">Powered by MindBill</footer>
|
|
231
|
+
@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> }
|
|
232
|
+
@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> }
|
|
233
|
+
@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> }
|
|
752
234
|
}
|
|
753
|
-
<datalist id="mb-states"><option value="CA"></option><option value="AZ"></option><option value="NV"></option><option value="OR"></option><option value="WA"></option><option value="TX"></option><option value="NY"></option></datalist>
|
|
754
235
|
</section>
|
|
755
|
-
`, styles: [":host{display:block}.mb{--
|
|
236
|
+
`, styles: [":host{display:block}.mb{font-family:var(--font);color:var(--t);background:var(--bg);padding:20px}.summary,.card,.columns{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}}\n"] }]
|
|
756
237
|
}], ctorParameters: () => [], propDecorators: { billId: [{
|
|
757
238
|
type: Input
|
|
758
|
-
}], create: [{
|
|
759
|
-
type: Input
|
|
760
239
|
}], sessionEndpoint: [{
|
|
761
240
|
type: Input
|
|
762
241
|
}], apiBaseUrl: [{
|
|
@@ -767,16 +246,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
767
246
|
type: Input
|
|
768
247
|
}], appearance: [{
|
|
769
248
|
type: Input
|
|
770
|
-
}], billIdChange: [{
|
|
771
|
-
type: Output
|
|
772
|
-
}], billCreated: [{
|
|
773
|
-
type: Output
|
|
774
|
-
}], submitted: [{
|
|
775
|
-
type: Output
|
|
776
249
|
}], billingError: [{
|
|
777
250
|
type: Output
|
|
778
251
|
}] } });
|
|
779
252
|
|
|
253
|
+
const EMPTY_PROCEDURE_LINE = {
|
|
254
|
+
code: "",
|
|
255
|
+
modifiers: [],
|
|
256
|
+
units: 1,
|
|
257
|
+
charge: 0,
|
|
258
|
+
};
|
|
259
|
+
function isEmptyProcedureLine(line) {
|
|
260
|
+
return !line.code.trim()
|
|
261
|
+
&& line.modifiers.length === 0
|
|
262
|
+
&& line.units === 1
|
|
263
|
+
&& !line.serviceDate
|
|
264
|
+
&& !line.serviceDateEnd
|
|
265
|
+
&& !(line.diagnosisPointers?.length)
|
|
266
|
+
&& !line.charge;
|
|
267
|
+
}
|
|
268
|
+
/** Keeps completed/partial lines plus one keyboard-ready empty row. */
|
|
269
|
+
function ensureTrailingProcedureLine(lines) {
|
|
270
|
+
const entered = lines
|
|
271
|
+
.filter((line) => !isEmptyProcedureLine(line))
|
|
272
|
+
.map((line) => ({ ...line, modifiers: [...line.modifiers] }));
|
|
273
|
+
return entered.length < 50
|
|
274
|
+
? [...entered, { ...EMPTY_PROCEDURE_LINE, modifiers: [] }]
|
|
275
|
+
: entered;
|
|
276
|
+
}
|
|
277
|
+
|
|
780
278
|
/**
|
|
781
279
|
* Generated bundle index. Do not edit.
|
|
782
280
|
*/
|