@mindbill/angular 0.3.0 → 0.4.1
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 +107 -503
- package/fesm2022/mindbill-angular.mjs.map +1 -1
- package/package.json +2 -2
- package/types/mindbill-angular.d.ts +11 -77
|
@@ -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,533 +79,163 @@ 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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
payerResults = [];
|
|
188
|
-
documentType = "other";
|
|
189
|
-
pendingFile = null;
|
|
190
|
-
deliveryOptions = null;
|
|
191
|
-
submission = { route: "ebill" };
|
|
192
|
-
showSubmit = false;
|
|
193
|
-
deliveryBusy = false;
|
|
97
|
+
lifecycleStages = [
|
|
98
|
+
{ id: "submitted", label: "Submitted" }, { id: "accepted", label: "Accepted" },
|
|
99
|
+
{ id: "response", label: "Payer response" }, { id: "follow_up", label: "Follow-up" },
|
|
100
|
+
{ id: "closed", label: "Closed" },
|
|
101
|
+
];
|
|
194
102
|
panel = "";
|
|
195
103
|
notice = "";
|
|
196
104
|
closeReason = "";
|
|
197
105
|
payment = { amount: 0, method: "check", depositDate: new Date().toISOString().slice(0, 10) };
|
|
198
106
|
review = { reason: "", payerClaimControlNumber: "", disputedAmount: 0 };
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
effect(() => {
|
|
202
|
-
const data = this.store.data();
|
|
203
|
-
if (data && !this.dirty)
|
|
204
|
-
this.draft = this.makeDraft(data);
|
|
205
|
-
const error = this.store.error();
|
|
206
|
-
if (error)
|
|
207
|
-
this.billingError.emit(error);
|
|
208
|
-
});
|
|
209
|
-
}
|
|
107
|
+
constructor() { effect(() => { const error = this.store.error(); if (error)
|
|
108
|
+
this.billingError.emit(error); }); }
|
|
210
109
|
ngOnChanges(changes) {
|
|
211
|
-
if (changes["billId"] || changes["
|
|
212
|
-
if (!this.billId
|
|
110
|
+
if (changes["billId"] || changes["sessionEndpoint"] || changes["apiBaseUrl"] || changes["getSession"]) {
|
|
111
|
+
if (!this.billId.trim()) {
|
|
213
112
|
this.store.disconnect();
|
|
214
113
|
return;
|
|
215
114
|
}
|
|
216
|
-
this.store.connect({
|
|
217
|
-
...(this.billId ? { billId: this.billId } : {}),
|
|
218
|
-
sessionEndpoint: this.sessionEndpoint,
|
|
219
|
-
apiBaseUrl: this.apiBaseUrl,
|
|
220
|
-
...(this.getSession ? { getSession: this.getSession } : {}),
|
|
221
|
-
}, this.refreshInterval, this.create, (billId, data) => {
|
|
222
|
-
this.billIdChange.emit(billId);
|
|
223
|
-
this.billCreated.emit({ billId, data });
|
|
224
|
-
});
|
|
115
|
+
this.store.connect({ billId: this.billId, sessionEndpoint: this.sessionEndpoint, apiBaseUrl: this.apiBaseUrl, ...(this.getSession ? { getSession: this.getSession } : {}) }, this.refreshInterval);
|
|
225
116
|
}
|
|
226
117
|
}
|
|
227
|
-
ngOnDestroy() { this.store.disconnect();
|
|
228
|
-
clearTimeout(this.payerTimer); }
|
|
118
|
+
ngOnDestroy() { this.store.disconnect(); }
|
|
229
119
|
get themeStyle() {
|
|
230
120
|
const base = THEMES[this.appearance.preset ?? "mindbill"];
|
|
231
|
-
return {
|
|
232
|
-
"--a": this.appearance.accentColor ?? base.accentColor,
|
|
233
|
-
"--ac": this.appearance.accentTextColor ?? base.accentTextColor,
|
|
234
|
-
"--bg": this.appearance.backgroundColor ?? base.backgroundColor,
|
|
235
|
-
"--s": this.appearance.surfaceColor ?? base.surfaceColor,
|
|
236
|
-
"--t": this.appearance.textColor ?? base.textColor,
|
|
237
|
-
"--m": this.appearance.mutedColor ?? base.mutedColor,
|
|
238
|
-
"--b": this.appearance.borderColor ?? base.borderColor,
|
|
239
|
-
"--r": this.appearance.borderRadius ?? base.borderRadius,
|
|
240
|
-
"--cr": this.appearance.controlRadius ?? base.controlRadius,
|
|
241
|
-
"--font": this.appearance.fontFamily ?? base.fontFamily,
|
|
242
|
-
};
|
|
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 };
|
|
243
122
|
}
|
|
244
123
|
stateLabel(data) { return data.lifecycle.state.replace(/_/g, " ").replace(/\b\w/g, (value) => value.toUpperCase()); }
|
|
245
|
-
lifecycleDetail(data) { return data.lifecycle.submittedAt ? `Submitted ${new Date(data.lifecycle.submittedAt).toLocaleDateString()}${data.lifecycle.agingDays != null ? ` · ${data.lifecycle.agingDays} days old` : ""}` : "
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
selectPayer(payer) { if (!this.draft)
|
|
255
|
-
return; this.draft.injury.claimsAdminId = payer.id; this.draft.injury.claimsAdminName = payer.name; this.payerQuery = ""; this.payerResults = []; this.dirty = true; }
|
|
256
|
-
clearPayer() { if (!this.draft)
|
|
257
|
-
return; this.draft.injury.claimsAdminId = ""; this.draft.injury.claimsAdminName = ""; this.dirty = true; }
|
|
258
|
-
lineChanged() { this.normalizeLineItems(); this.dirty = true; }
|
|
259
|
-
removeLine(index) { this.draft?.bill.lineItems.splice(index, 1); this.normalizeLineItems(); this.dirty = true; }
|
|
260
|
-
setModifiers(line, value) { line.modifiers = value.split(",").map((item) => item.trim().replace(/^-/, "")).filter(Boolean); this.normalizeLineItems(); this.dirty = true; }
|
|
261
|
-
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; }
|
|
262
|
-
applyPreset(kind) {
|
|
263
|
-
if (!this.draft)
|
|
264
|
-
return;
|
|
265
|
-
const modifier = kind === "ame" ? "94" : kind === "psych" ? "96" : "95";
|
|
266
|
-
const entered = this.draft.bill.lineItems.filter((line) => line.code.trim());
|
|
267
|
-
if (entered.length === 0)
|
|
268
|
-
entered.push({ code: "ML201", modifiers: [modifier], units: 1, charge: 0 });
|
|
269
|
-
else
|
|
270
|
-
entered.forEach((line) => { if (!line.modifiers.length)
|
|
271
|
-
line.modifiers = [modifier]; });
|
|
272
|
-
this.draft.bill.lineItems = ensureTrailingProcedureLine(entered);
|
|
273
|
-
this.dirty = true;
|
|
274
|
-
}
|
|
275
|
-
fileSelected(event) { this.pendingFile = event.target.files?.[0] ?? null; }
|
|
276
|
-
async attach() { if (!this.pendingFile)
|
|
277
|
-
return; await this.store.addAttachment(this.pendingFile, this.documentType); this.pendingFile = null; this.notice = "Document attached."; }
|
|
278
|
-
async removeAttachment(id) { await this.store.removeAttachment(id); this.notice = "Document removed."; }
|
|
279
|
-
async openAttachment(id, filename) { this.openBlob(await this.store.getAttachment(id), filename); }
|
|
280
|
-
async openEor(id, filename) { this.openBlob(await this.store.getEor(id), filename); }
|
|
281
|
-
canSubmit() {
|
|
282
|
-
const input = this.buildInput();
|
|
283
|
-
if (!input || !input.claimsAdminId || !input.billingProvider?.name || !input.billingProvider.taxId || !input.billingProvider.npi || !input.renderingProvider?.name || !input.renderingProvider.npi || !input.lineItems.length)
|
|
284
|
-
return false;
|
|
285
|
-
return input.lineItems.every((line) => line.code && line.units > 0 && (this.draft?.bill.billingMode !== "professional" || Boolean(line.serviceDate && line.charge && line.charge > 0)));
|
|
286
|
-
}
|
|
287
|
-
async save() { const input = this.buildInput(); if (!input)
|
|
288
|
-
return; const data = await this.store.saveReview(input); this.dirty = false; this.draft = this.makeDraft(data); this.notice = "Bill saved."; }
|
|
289
|
-
async openSubmit() {
|
|
290
|
-
if (!this.canSubmit())
|
|
291
|
-
return;
|
|
292
|
-
this.deliveryBusy = true;
|
|
293
|
-
try {
|
|
294
|
-
this.deliveryOptions = await this.store.getDeliveryOptions();
|
|
295
|
-
this.chooseDelivery(this.deliveryOptions.recommended);
|
|
296
|
-
this.showSubmit = true;
|
|
297
|
-
}
|
|
298
|
-
finally {
|
|
299
|
-
this.deliveryBusy = false;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
closeSubmit() { this.showSubmit = false; }
|
|
303
|
-
selectedDelivery() { return this.deliveryOptions?.options.find((option) => option.route === this.submission.route); }
|
|
304
|
-
chooseDelivery(option) {
|
|
305
|
-
const destination = option.route === "fax" ? { faxNumber: option.target ?? this.deliveryOptions?.contacts.faxNumber ?? "" }
|
|
306
|
-
: option.route === "email" ? { email: option.target ?? this.deliveryOptions?.contacts.claimsEmail ?? "" }
|
|
307
|
-
: option.route === "mail" ? { mailingAddress: option.target ?? this.deliveryOptions?.contacts.mailingAddress ?? "" }
|
|
308
|
-
: undefined;
|
|
309
|
-
this.submission = { route: option.route, ...(destination ? { destination } : {}) };
|
|
310
|
-
}
|
|
311
|
-
setDestination(key, value) { this.submission = { ...this.submission, destination: { ...this.submission.destination, [key]: value } }; }
|
|
312
|
-
deliveryReady() {
|
|
313
|
-
if (this.submission.route === "fax")
|
|
314
|
-
return Boolean(this.submission.destination?.faxNumber?.trim());
|
|
315
|
-
if (this.submission.route === "email")
|
|
316
|
-
return Boolean(this.submission.destination?.email?.trim());
|
|
317
|
-
if (this.submission.route === "mail")
|
|
318
|
-
return Boolean(this.submission.destination?.mailingAddress?.trim());
|
|
319
|
-
return true;
|
|
320
|
-
}
|
|
321
|
-
async submit() { const input = this.buildInput(); if (!input || !this.canSubmit() || !this.deliveryReady())
|
|
322
|
-
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); }
|
|
323
|
-
beginAction(action) { if (action === "post_payment")
|
|
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")
|
|
324
133
|
this.panel = "payment";
|
|
325
134
|
else if (action === "second_review" || action === "independent_bill_review")
|
|
326
135
|
this.panel = "review";
|
|
327
136
|
else if (action === "close")
|
|
328
137
|
this.panel = "close";
|
|
329
|
-
else if (action === "view_eor" &&
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
async correct() { const data = await this.store.startCorrection(); this.billIdChange.emit(this.store.billId()); this.dirty = false; this.draft = this.makeDraft(data); this.notice = "Correction draft created."; }
|
|
336
|
-
async postPayment() { await this.store.postPayment({ ...this.payment }); this.panel = ""; this.notice = "Payment posted."; }
|
|
337
|
-
async submitReview() { const data = this.store.data(); if (!data)
|
|
338
|
-
return; await this.store.submitSecondReview({ ...this.review, disputedAmount: this.review.disputedAmount || undefined, route: "ebill", attachmentIds: data.bill.attachments.map((doc) => doc.id) }); this.panel = ""; this.notice = "Second Review submitted."; }
|
|
138
|
+
else if (action === "view_eor" && data.eors[0])
|
|
139
|
+
void this.openEor(data.eors[0].id, data.eors[0].filename); }
|
|
140
|
+
async openAttachment(id, filename) { this.openBlob(await this.store.getAttachment(id), filename); }
|
|
141
|
+
async openEor(id, filename) { this.openBlob(await this.store.getEor(id), filename); }
|
|
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."; }
|
|
339
144
|
async closeBill() { if (!this.closeReason.trim())
|
|
340
145
|
return; await this.store.closeBill({ reason: this.closeReason }); this.panel = ""; this.notice = "Bill closed."; }
|
|
341
|
-
buildInput() {
|
|
342
|
-
if (!this.draft)
|
|
343
|
-
return null;
|
|
344
|
-
return {
|
|
345
|
-
claimsAdminId: this.draft.injury.claimsAdminId ?? "",
|
|
346
|
-
patientOverrides: {
|
|
347
|
-
firstName: this.draft.patient.firstName,
|
|
348
|
-
lastName: this.draft.patient.lastName,
|
|
349
|
-
...(this.draft.patient.middleName ? { middleName: this.draft.patient.middleName } : {}),
|
|
350
|
-
...(this.draft.patient.dob ? { dob: this.draft.patient.dob } : {}),
|
|
351
|
-
},
|
|
352
|
-
injuryOverrides: {
|
|
353
|
-
...(this.draft.injury.claimNumber ? { claimNumber: this.draft.injury.claimNumber } : {}),
|
|
354
|
-
...(this.draft.injury.employer ? { employer: this.draft.injury.employer } : {}),
|
|
355
|
-
...(this.draft.injury.doi ? { doi: this.draft.injury.doi } : {}),
|
|
356
|
-
...(this.draft.injury.injuryEndDate ? { injuryEndDate: this.draft.injury.injuryEndDate } : {}),
|
|
357
|
-
...(typeof this.draft.injury.cumulativeTrauma === "boolean" ? { cumulativeTrauma: this.draft.injury.cumulativeTrauma } : {}),
|
|
358
|
-
...(this.draft.injury.adjNumber ? { adjNumber: this.draft.injury.adjNumber } : {}),
|
|
359
|
-
},
|
|
360
|
-
dos: this.draft.bill.dos,
|
|
361
|
-
billingProvider: this.draft.billingProvider,
|
|
362
|
-
renderingProvider: this.draft.clinician,
|
|
363
|
-
placeOfService: this.draft.location,
|
|
364
|
-
lineItems: this.draft.bill.lineItems.filter((line) => line.code.trim()).map((line) => ({
|
|
365
|
-
...(line.id ? { id: line.id } : {}),
|
|
366
|
-
code: line.code,
|
|
367
|
-
modifiers: line.modifiers,
|
|
368
|
-
units: line.units,
|
|
369
|
-
...(line.charge > 0 ? { charge: line.charge } : {}),
|
|
370
|
-
...(line.serviceDate ? { serviceDate: line.serviceDate } : {}),
|
|
371
|
-
...(line.serviceDateEnd ? { serviceDateEnd: line.serviceDateEnd } : {}),
|
|
372
|
-
...(line.diagnosisPointers?.length ? { diagnosisPointers: line.diagnosisPointers } : {}),
|
|
373
|
-
})),
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
makeDraft(data) {
|
|
377
|
-
const snapshot = data.bill.billingSnapshot ?? {};
|
|
378
|
-
const names = data.patient.name.trim().split(/\s+/);
|
|
379
|
-
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" } };
|
|
380
|
-
}
|
|
381
|
-
normalizeLineItems() { if (this.draft)
|
|
382
|
-
this.draft.bill.lineItems = ensureTrailingProcedureLine(this.draft.bill.lineItems); }
|
|
383
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); }
|
|
384
147
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillLifecycleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
385
|
-
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: `
|
|
386
149
|
<section class="mb" [ngStyle]="themeStyle">
|
|
387
|
-
@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()) {
|
|
388
153
|
<div class="state">Loading billing…</div>
|
|
389
154
|
} @else if (store.error() && !store.data()) {
|
|
390
|
-
<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>
|
|
391
156
|
} @else if (store.data(); as data) {
|
|
392
157
|
<header class="summary">
|
|
393
158
|
<div><span class="eyebrow">Bill {{ data.bill.billNumber }}</span><h2>{{ stateLabel(data) }}</h2><p>{{ lifecycleDetail(data) }}</p></div>
|
|
394
159
|
<div class="money"><span>{{ data.bill.balanceDue | currency }}</span><small>balance</small></div>
|
|
395
160
|
</header>
|
|
396
161
|
|
|
397
|
-
|
|
398
|
-
<
|
|
399
|
-
|
|
400
|
-
<div class="card-title"><div><h3>Patient and claim</h3><p>Prefilled from the case. Correct anything that should print differently on this bill.</p></div><span>Required</span></div>
|
|
401
|
-
<div class="grid four">
|
|
402
|
-
<label><span>First name</span><input required [(ngModel)]="draft.patient.firstName" name="firstName" (ngModelChange)="dirty=true"></label>
|
|
403
|
-
<label><span>Last name</span><input required [(ngModel)]="draft.patient.lastName" name="lastName" (ngModelChange)="dirty=true"></label>
|
|
404
|
-
<label><span>Date of birth</span><input type="date" [(ngModel)]="draft.patient.dob" name="dob" (ngModelChange)="dirty=true"></label>
|
|
405
|
-
<label><span>Date of injury</span><input type="date" [(ngModel)]="draft.injury.doi" name="doi" (ngModelChange)="dirty=true"></label>
|
|
406
|
-
<label><span>Claim number</span><input [(ngModel)]="draft.injury.claimNumber" name="claimNumber" (ngModelChange)="dirty=true"></label>
|
|
407
|
-
<label><span>Employer</span><input [(ngModel)]="draft.injury.employer" name="employer" (ngModelChange)="dirty=true"></label>
|
|
408
|
-
<label><span>WCAB / case number</span><input [(ngModel)]="draft.injury.adjNumber" name="adjNumber" (ngModelChange)="dirty=true"></label>
|
|
409
|
-
<label><span>Date of service</span><input required type="date" [(ngModel)]="draft.bill.dos" name="dos" (ngModelChange)="dirty=true"></label>
|
|
410
|
-
</div>
|
|
411
|
-
</section>
|
|
162
|
+
<section class="card progress" aria-label="Bill lifecycle">
|
|
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>
|
|
164
|
+
</section>
|
|
412
165
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
<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>
|
|
418
|
-
@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> }
|
|
419
|
-
}
|
|
420
|
-
</section>
|
|
166
|
+
<section class="card snapshot" aria-label="Bill snapshot">
|
|
167
|
+
<div class="card-title"><div><h3>Bill snapshot</h3><p>The immutable values submitted to the payer.</p></div></div>
|
|
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>
|
|
169
|
+
</section>
|
|
421
170
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
<label><span>Practice name</span><input required [(ngModel)]="draft.billingProvider.name" name="practiceName" (ngModelChange)="dirty=true"></label>
|
|
427
|
-
<label><span>Tax ID</span><input required [(ngModel)]="draft.billingProvider.taxId" name="taxId" (ngModelChange)="dirty=true"></label>
|
|
428
|
-
<label><span>Billing NPI</span><input required [(ngModel)]="draft.billingProvider.npi" name="billingNpi" (ngModelChange)="dirty=true"></label>
|
|
429
|
-
</div>
|
|
430
|
-
<div class="subhead">Rendering clinician</div>
|
|
431
|
-
<div class="grid three">
|
|
432
|
-
<label><span>Name</span><input required [(ngModel)]="draft.clinician.name" name="clinicianName" (ngModelChange)="dirty=true"></label>
|
|
433
|
-
<label><span>NPI</span><input required [(ngModel)]="draft.clinician.npi" name="clinicianNpi" (ngModelChange)="dirty=true"></label>
|
|
434
|
-
<label><span>Taxonomy</span><input [(ngModel)]="draft.clinician.taxonomy" name="taxonomy" (ngModelChange)="dirty=true"></label>
|
|
435
|
-
</div>
|
|
436
|
-
<div class="subhead">Service location</div>
|
|
437
|
-
<div class="grid three">
|
|
438
|
-
<label><span>Location</span><input required [(ngModel)]="draft.location.name" name="locationName" (ngModelChange)="dirty=true"></label>
|
|
439
|
-
<label><span>Street</span><input required [(ngModel)]="draft.location.street" name="street" (ngModelChange)="dirty=true"></label>
|
|
440
|
-
<label><span>City</span><input required [(ngModel)]="draft.location.city" name="city" (ngModelChange)="dirty=true"></label>
|
|
441
|
-
<label><span>State</span><input required list="mb-states" maxlength="2" [(ngModel)]="draft.location.state" name="state" (ngModelChange)="dirty=true"></label>
|
|
442
|
-
<label><span>ZIP</span><input required [(ngModel)]="draft.location.zip" name="zip" (ngModelChange)="dirty=true"></label>
|
|
443
|
-
<label><span>Place of service</span><input required [(ngModel)]="draft.location.posCode" name="pos" (ngModelChange)="dirty=true"></label>
|
|
444
|
-
</div>
|
|
445
|
-
</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>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></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>
|
|
446
175
|
|
|
447
|
-
|
|
448
|
-
<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>
|
|
449
|
-
@if (draft.bill.billingMode === 'med_legal') {
|
|
450
|
-
<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>
|
|
451
|
-
}
|
|
452
|
-
<div class="lines">@for (line of draft.bill.lineItems; track $index; let i=$index) {
|
|
453
|
-
<div class="line" [class.professional]="draft.bill.billingMode === 'professional'">
|
|
454
|
-
<label><span>Procedure</span>
|
|
455
|
-
@if (draft.bill.billingMode === 'med_legal') {
|
|
456
|
-
<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>
|
|
457
|
-
} @else { <input [(ngModel)]="line.code" [name]="'code'+i" (ngModelChange)="lineChanged()" placeholder="e.g. 99204"> }
|
|
458
|
-
</label>
|
|
459
|
-
<label><span>Modifiers</span><input [ngModel]="line.modifiers.join(', ')" (ngModelChange)="setModifiers(line,$any($event))" [name]="'modifiers'+i" placeholder="e.g. 25, 95"></label>
|
|
460
|
-
@if (draft.bill.billingMode === 'professional') {
|
|
461
|
-
<label><span>Service date</span><input type="date" [(ngModel)]="line.serviceDate" [name]="'serviceDate'+i" (ngModelChange)="lineChanged()"></label>
|
|
462
|
-
<label><span>End date</span><input type="date" [(ngModel)]="line.serviceDateEnd" [name]="'serviceDateEnd'+i" (ngModelChange)="lineChanged()"></label>
|
|
463
|
-
<label><span>Charge</span><input type="number" min="0" step="0.01" [(ngModel)]="line.charge" [name]="'charge'+i" (ngModelChange)="lineChanged()"></label>
|
|
464
|
-
<label><span>Diagnosis pointers</span><input [ngModel]="(line.diagnosisPointers || []).join(', ')" (ngModelChange)="setDiagnosisPointers(line,$any($event))" [name]="'pointers'+i" placeholder="1, 2"></label>
|
|
465
|
-
}
|
|
466
|
-
<label><span>Units</span><input type="number" min="1" [(ngModel)]="line.units" [name]="'units'+i" (ngModelChange)="lineChanged()"></label>
|
|
467
|
-
<div class="allowed"><span>{{ draft.bill.billingMode === 'professional' ? 'Charge' : 'Allowed' }}</span><strong>{{ line.charge | currency }}</strong></div>
|
|
468
|
-
<button type="button" class="remove" (click)="removeLine(i)" aria-label="Remove line">×</button>
|
|
469
|
-
</div>
|
|
470
|
-
}</div>
|
|
471
|
-
</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>
|
|
472
177
|
|
|
473
|
-
|
|
474
|
-
<div class="card-title"><div><h3>Payer packet</h3><p>Only these documents will be sent. Medical records are never attached unless selected intentionally.</p></div><span>{{ data.bill.attachments.length }} files</span></div>
|
|
475
|
-
<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>
|
|
476
|
-
<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>
|
|
477
|
-
</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>
|
|
478
179
|
|
|
479
|
-
|
|
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>
|
|
480
181
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
}</div>
|
|
488
|
-
@if (selectedDelivery(); as option) {
|
|
489
|
-
@if (option.route === 'fax') { <label><span>Fax number</span><input [ngModel]="submission.destination?.faxNumber || ''" (ngModelChange)="setDestination('faxNumber',$any($event))" name="submitFax"></label> }
|
|
490
|
-
@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> }
|
|
491
|
-
@if (option.route === 'mail') { <label><span>Mailing address</span><textarea [ngModel]="submission.destination?.mailingAddress || ''" (ngModelChange)="setDestination('mailingAddress',$any($event))" name="submitMail"></textarea></label> }
|
|
492
|
-
@if (option.route !== 'ebill') { <label><span>Attention (optional)</span><input [(ngModel)]="submission.attention" name="submitAttention"></label> }
|
|
493
|
-
}
|
|
494
|
-
<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>
|
|
495
|
-
</section>
|
|
496
|
-
</div>
|
|
497
|
-
}
|
|
498
|
-
</form>
|
|
499
|
-
} @else {
|
|
500
|
-
<section class="card lifecycle">
|
|
501
|
-
<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>
|
|
502
|
-
@if (data.eors.length) { <div class="eors"><h4>Explanation of Review</h4>@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 PDF</b></button> }</div> }
|
|
503
|
-
<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>
|
|
504
|
-
@if (panel === 'payment') { <div class="panel"><h4>Post payment</h4><div class="grid three"><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>Deposit date</span><input type="date" [(ngModel)]="payment.depositDate"></label></div><div class="actions"><button (click)="panel=''">Cancel</button><button class="primary" (click)="postPayment()">Post payment</button></div></div> }
|
|
505
|
-
@if (panel === 'review') { <div class="panel"><h4>Submit Second Review</h4><label><span>Reason</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><div class="actions"><button (click)="panel=''">Cancel</button><button class="primary" (click)="submitReview()">Submit review</button></div></div> }
|
|
506
|
-
@if (panel === 'close') { <div class="panel"><h4>Close bill</h4><label><span>Reason</span><textarea [(ngModel)]="closeReason"></textarea></label><div class="actions"><button (click)="panel=''">Cancel</button><button class="danger" (click)="closeBill()">Close bill</button></div></div> }
|
|
507
|
-
</section>
|
|
508
|
-
}
|
|
509
|
-
@if (notice) { <div class="notice">{{ notice }}</div> }
|
|
510
|
-
@if (store.error()) { <div class="notice error">{{ store.error()?.message }}</div> }
|
|
511
|
-
<footer class="powered">Powered by MindBill</footer>
|
|
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>
|
|
184
|
+
|
|
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> }
|
|
512
188
|
}
|
|
513
|
-
<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>
|
|
514
189
|
</section>
|
|
515
|
-
`, isInline: true, styles: [":host{display:block}.mb{--
|
|
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" }] });
|
|
516
191
|
}
|
|
517
192
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillLifecycleComponent, decorators: [{
|
|
518
193
|
type: Component,
|
|
519
194
|
args: [{ selector: "mindbill-bill-lifecycle", standalone: true, imports: [CommonModule, FormsModule], template: `
|
|
520
195
|
<section class="mb" [ngStyle]="themeStyle">
|
|
521
|
-
@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()) {
|
|
522
199
|
<div class="state">Loading billing…</div>
|
|
523
200
|
} @else if (store.error() && !store.data()) {
|
|
524
|
-
<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>
|
|
525
202
|
} @else if (store.data(); as data) {
|
|
526
203
|
<header class="summary">
|
|
527
204
|
<div><span class="eyebrow">Bill {{ data.bill.billNumber }}</span><h2>{{ stateLabel(data) }}</h2><p>{{ lifecycleDetail(data) }}</p></div>
|
|
528
205
|
<div class="money"><span>{{ data.bill.balanceDue | currency }}</span><small>balance</small></div>
|
|
529
206
|
</header>
|
|
530
207
|
|
|
531
|
-
|
|
532
|
-
<
|
|
533
|
-
|
|
534
|
-
<div class="card-title"><div><h3>Patient and claim</h3><p>Prefilled from the case. Correct anything that should print differently on this bill.</p></div><span>Required</span></div>
|
|
535
|
-
<div class="grid four">
|
|
536
|
-
<label><span>First name</span><input required [(ngModel)]="draft.patient.firstName" name="firstName" (ngModelChange)="dirty=true"></label>
|
|
537
|
-
<label><span>Last name</span><input required [(ngModel)]="draft.patient.lastName" name="lastName" (ngModelChange)="dirty=true"></label>
|
|
538
|
-
<label><span>Date of birth</span><input type="date" [(ngModel)]="draft.patient.dob" name="dob" (ngModelChange)="dirty=true"></label>
|
|
539
|
-
<label><span>Date of injury</span><input type="date" [(ngModel)]="draft.injury.doi" name="doi" (ngModelChange)="dirty=true"></label>
|
|
540
|
-
<label><span>Claim number</span><input [(ngModel)]="draft.injury.claimNumber" name="claimNumber" (ngModelChange)="dirty=true"></label>
|
|
541
|
-
<label><span>Employer</span><input [(ngModel)]="draft.injury.employer" name="employer" (ngModelChange)="dirty=true"></label>
|
|
542
|
-
<label><span>WCAB / case number</span><input [(ngModel)]="draft.injury.adjNumber" name="adjNumber" (ngModelChange)="dirty=true"></label>
|
|
543
|
-
<label><span>Date of service</span><input required type="date" [(ngModel)]="draft.bill.dos" name="dos" (ngModelChange)="dirty=true"></label>
|
|
544
|
-
</div>
|
|
545
|
-
</section>
|
|
208
|
+
<section class="card progress" aria-label="Bill lifecycle">
|
|
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>
|
|
210
|
+
</section>
|
|
546
211
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
<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>
|
|
552
|
-
@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> }
|
|
553
|
-
}
|
|
554
|
-
</section>
|
|
212
|
+
<section class="card snapshot" aria-label="Bill snapshot">
|
|
213
|
+
<div class="card-title"><div><h3>Bill snapshot</h3><p>The immutable values submitted to the payer.</p></div></div>
|
|
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>
|
|
215
|
+
</section>
|
|
555
216
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
<label><span>Practice name</span><input required [(ngModel)]="draft.billingProvider.name" name="practiceName" (ngModelChange)="dirty=true"></label>
|
|
561
|
-
<label><span>Tax ID</span><input required [(ngModel)]="draft.billingProvider.taxId" name="taxId" (ngModelChange)="dirty=true"></label>
|
|
562
|
-
<label><span>Billing NPI</span><input required [(ngModel)]="draft.billingProvider.npi" name="billingNpi" (ngModelChange)="dirty=true"></label>
|
|
563
|
-
</div>
|
|
564
|
-
<div class="subhead">Rendering clinician</div>
|
|
565
|
-
<div class="grid three">
|
|
566
|
-
<label><span>Name</span><input required [(ngModel)]="draft.clinician.name" name="clinicianName" (ngModelChange)="dirty=true"></label>
|
|
567
|
-
<label><span>NPI</span><input required [(ngModel)]="draft.clinician.npi" name="clinicianNpi" (ngModelChange)="dirty=true"></label>
|
|
568
|
-
<label><span>Taxonomy</span><input [(ngModel)]="draft.clinician.taxonomy" name="taxonomy" (ngModelChange)="dirty=true"></label>
|
|
569
|
-
</div>
|
|
570
|
-
<div class="subhead">Service location</div>
|
|
571
|
-
<div class="grid three">
|
|
572
|
-
<label><span>Location</span><input required [(ngModel)]="draft.location.name" name="locationName" (ngModelChange)="dirty=true"></label>
|
|
573
|
-
<label><span>Street</span><input required [(ngModel)]="draft.location.street" name="street" (ngModelChange)="dirty=true"></label>
|
|
574
|
-
<label><span>City</span><input required [(ngModel)]="draft.location.city" name="city" (ngModelChange)="dirty=true"></label>
|
|
575
|
-
<label><span>State</span><input required list="mb-states" maxlength="2" [(ngModel)]="draft.location.state" name="state" (ngModelChange)="dirty=true"></label>
|
|
576
|
-
<label><span>ZIP</span><input required [(ngModel)]="draft.location.zip" name="zip" (ngModelChange)="dirty=true"></label>
|
|
577
|
-
<label><span>Place of service</span><input required [(ngModel)]="draft.location.posCode" name="pos" (ngModelChange)="dirty=true"></label>
|
|
578
|
-
</div>
|
|
579
|
-
</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>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></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>
|
|
580
221
|
|
|
581
|
-
|
|
582
|
-
<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>
|
|
583
|
-
@if (draft.bill.billingMode === 'med_legal') {
|
|
584
|
-
<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>
|
|
585
|
-
}
|
|
586
|
-
<div class="lines">@for (line of draft.bill.lineItems; track $index; let i=$index) {
|
|
587
|
-
<div class="line" [class.professional]="draft.bill.billingMode === 'professional'">
|
|
588
|
-
<label><span>Procedure</span>
|
|
589
|
-
@if (draft.bill.billingMode === 'med_legal') {
|
|
590
|
-
<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>
|
|
591
|
-
} @else { <input [(ngModel)]="line.code" [name]="'code'+i" (ngModelChange)="lineChanged()" placeholder="e.g. 99204"> }
|
|
592
|
-
</label>
|
|
593
|
-
<label><span>Modifiers</span><input [ngModel]="line.modifiers.join(', ')" (ngModelChange)="setModifiers(line,$any($event))" [name]="'modifiers'+i" placeholder="e.g. 25, 95"></label>
|
|
594
|
-
@if (draft.bill.billingMode === 'professional') {
|
|
595
|
-
<label><span>Service date</span><input type="date" [(ngModel)]="line.serviceDate" [name]="'serviceDate'+i" (ngModelChange)="lineChanged()"></label>
|
|
596
|
-
<label><span>End date</span><input type="date" [(ngModel)]="line.serviceDateEnd" [name]="'serviceDateEnd'+i" (ngModelChange)="lineChanged()"></label>
|
|
597
|
-
<label><span>Charge</span><input type="number" min="0" step="0.01" [(ngModel)]="line.charge" [name]="'charge'+i" (ngModelChange)="lineChanged()"></label>
|
|
598
|
-
<label><span>Diagnosis pointers</span><input [ngModel]="(line.diagnosisPointers || []).join(', ')" (ngModelChange)="setDiagnosisPointers(line,$any($event))" [name]="'pointers'+i" placeholder="1, 2"></label>
|
|
599
|
-
}
|
|
600
|
-
<label><span>Units</span><input type="number" min="1" [(ngModel)]="line.units" [name]="'units'+i" (ngModelChange)="lineChanged()"></label>
|
|
601
|
-
<div class="allowed"><span>{{ draft.bill.billingMode === 'professional' ? 'Charge' : 'Allowed' }}</span><strong>{{ line.charge | currency }}</strong></div>
|
|
602
|
-
<button type="button" class="remove" (click)="removeLine(i)" aria-label="Remove line">×</button>
|
|
603
|
-
</div>
|
|
604
|
-
}</div>
|
|
605
|
-
</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>
|
|
606
223
|
|
|
607
|
-
|
|
608
|
-
<div class="card-title"><div><h3>Payer packet</h3><p>Only these documents will be sent. Medical records are never attached unless selected intentionally.</p></div><span>{{ data.bill.attachments.length }} files</span></div>
|
|
609
|
-
<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>
|
|
610
|
-
<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>
|
|
611
|
-
</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>
|
|
612
225
|
|
|
613
|
-
|
|
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>
|
|
614
227
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
}</div>
|
|
622
|
-
@if (selectedDelivery(); as option) {
|
|
623
|
-
@if (option.route === 'fax') { <label><span>Fax number</span><input [ngModel]="submission.destination?.faxNumber || ''" (ngModelChange)="setDestination('faxNumber',$any($event))" name="submitFax"></label> }
|
|
624
|
-
@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> }
|
|
625
|
-
@if (option.route === 'mail') { <label><span>Mailing address</span><textarea [ngModel]="submission.destination?.mailingAddress || ''" (ngModelChange)="setDestination('mailingAddress',$any($event))" name="submitMail"></textarea></label> }
|
|
626
|
-
@if (option.route !== 'ebill') { <label><span>Attention (optional)</span><input [(ngModel)]="submission.attention" name="submitAttention"></label> }
|
|
627
|
-
}
|
|
628
|
-
<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>
|
|
629
|
-
</section>
|
|
630
|
-
</div>
|
|
631
|
-
}
|
|
632
|
-
</form>
|
|
633
|
-
} @else {
|
|
634
|
-
<section class="card lifecycle">
|
|
635
|
-
<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>
|
|
636
|
-
@if (data.eors.length) { <div class="eors"><h4>Explanation of Review</h4>@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 PDF</b></button> }</div> }
|
|
637
|
-
<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>
|
|
638
|
-
@if (panel === 'payment') { <div class="panel"><h4>Post payment</h4><div class="grid three"><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>Deposit date</span><input type="date" [(ngModel)]="payment.depositDate"></label></div><div class="actions"><button (click)="panel=''">Cancel</button><button class="primary" (click)="postPayment()">Post payment</button></div></div> }
|
|
639
|
-
@if (panel === 'review') { <div class="panel"><h4>Submit Second Review</h4><label><span>Reason</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><div class="actions"><button (click)="panel=''">Cancel</button><button class="primary" (click)="submitReview()">Submit review</button></div></div> }
|
|
640
|
-
@if (panel === 'close') { <div class="panel"><h4>Close bill</h4><label><span>Reason</span><textarea [(ngModel)]="closeReason"></textarea></label><div class="actions"><button (click)="panel=''">Cancel</button><button class="danger" (click)="closeBill()">Close bill</button></div></div> }
|
|
641
|
-
</section>
|
|
642
|
-
}
|
|
643
|
-
@if (notice) { <div class="notice">{{ notice }}</div> }
|
|
644
|
-
@if (store.error()) { <div class="notice error">{{ store.error()?.message }}</div> }
|
|
645
|
-
<footer class="powered">Powered by MindBill</footer>
|
|
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>
|
|
230
|
+
|
|
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> }
|
|
646
234
|
}
|
|
647
|
-
<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>
|
|
648
235
|
</section>
|
|
649
|
-
`, 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"] }]
|
|
650
237
|
}], ctorParameters: () => [], propDecorators: { billId: [{
|
|
651
238
|
type: Input
|
|
652
|
-
}], create: [{
|
|
653
|
-
type: Input
|
|
654
239
|
}], sessionEndpoint: [{
|
|
655
240
|
type: Input
|
|
656
241
|
}], apiBaseUrl: [{
|
|
@@ -661,16 +246,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
661
246
|
type: Input
|
|
662
247
|
}], appearance: [{
|
|
663
248
|
type: Input
|
|
664
|
-
}], billIdChange: [{
|
|
665
|
-
type: Output
|
|
666
|
-
}], billCreated: [{
|
|
667
|
-
type: Output
|
|
668
|
-
}], submitted: [{
|
|
669
|
-
type: Output
|
|
670
249
|
}], billingError: [{
|
|
671
250
|
type: Output
|
|
672
251
|
}] } });
|
|
673
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
|
+
|
|
674
278
|
/**
|
|
675
279
|
* Generated bundle index. Do not edit.
|
|
676
280
|
*/
|