heor-agent-mcp 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/formatters/xlsx.d.ts +5 -3
- package/dist/formatters/xlsx.d.ts.map +1 -1
- package/dist/formatters/xlsx.js +408 -87
- package/dist/formatters/xlsx.js.map +1 -1
- package/dist/jca/countryRegistry.d.ts.map +1 -1
- package/dist/jca/countryRegistry.js +157 -4
- package/dist/jca/countryRegistry.js.map +1 -1
- package/dist/jca/types.d.ts +2 -2
- package/dist/jca/types.d.ts.map +1 -1
- package/dist/providers/types.d.ts +2 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/server.js +42 -0
- package/dist/server.js.map +1 -1
- package/dist/tools/evidenceClinicalScale.d.ts +145 -0
- package/dist/tools/evidenceClinicalScale.d.ts.map +1 -0
- package/dist/tools/evidenceClinicalScale.js +512 -0
- package/dist/tools/evidenceClinicalScale.js.map +1 -0
- package/dist/tools/evidenceUnmetNeed.d.ts +519 -0
- package/dist/tools/evidenceUnmetNeed.d.ts.map +1 -0
- package/dist/tools/evidenceUnmetNeed.js +602 -0
- package/dist/tools/evidenceUnmetNeed.js.map +1 -0
- package/dist/tools/htaDossier/gvdEvidencePack.d.ts +41 -0
- package/dist/tools/htaDossier/gvdEvidencePack.d.ts.map +1 -0
- package/dist/tools/htaDossier/gvdEvidencePack.js +68 -0
- package/dist/tools/htaDossier/gvdEvidencePack.js.map +1 -0
- package/dist/tools/htaDossier/gvdMarketBranches.d.ts +18 -0
- package/dist/tools/htaDossier/gvdMarketBranches.d.ts.map +1 -0
- package/dist/tools/htaDossier/gvdMarketBranches.js +56 -0
- package/dist/tools/htaDossier/gvdMarketBranches.js.map +1 -0
- package/dist/tools/htaDossier/gvdSectionRouter.d.ts +20 -0
- package/dist/tools/htaDossier/gvdSectionRouter.d.ts.map +1 -0
- package/dist/tools/htaDossier/gvdSectionRouter.js +75 -0
- package/dist/tools/htaDossier/gvdSectionRouter.js.map +1 -0
- package/dist/tools/htaDossier/gvdSections.d.ts +36 -0
- package/dist/tools/htaDossier/gvdSections.d.ts.map +1 -0
- package/dist/tools/htaDossier/gvdSections.js +267 -0
- package/dist/tools/htaDossier/gvdSections.js.map +1 -0
- package/dist/tools/htaDossierPrep.d.ts +4 -0
- package/dist/tools/htaDossierPrep.d.ts.map +1 -1
- package/dist/tools/htaDossierPrep.js +46 -0
- package/dist/tools/htaDossierPrep.js.map +1 -1
- package/package.json +1 -1
|
@@ -6,14 +6,16 @@
|
|
|
6
6
|
* 2. Modify assumptions
|
|
7
7
|
* 3. Submit to HTA bodies
|
|
8
8
|
*
|
|
9
|
-
* Workbooks use
|
|
10
|
-
* results recalculate. Multi-tab structure:
|
|
9
|
+
* Workbooks use live Excel formulas so inputs can be changed and
|
|
10
|
+
* results recalculate. Multi-tab structure: Summary, Inputs, Transition Matrix,
|
|
11
|
+
* Markov Trace, PSA, CEAC, Audit.
|
|
11
12
|
*/
|
|
12
13
|
import type { CEModelParams, CEModelResult } from "../providers/types.js";
|
|
13
14
|
import type { AuditRecord } from "../audit/types.js";
|
|
14
15
|
/**
|
|
15
16
|
* Build a cost-effectiveness model Excel workbook.
|
|
16
|
-
* Inputs are editable (yellow)
|
|
17
|
+
* Inputs are editable (yellow); formula cells (blue) update automatically
|
|
18
|
+
* when Inputs values are changed and the workbook is saved/recalculated.
|
|
17
19
|
*/
|
|
18
20
|
export declare function ceModelToXlsx(params: CEModelParams, result: CEModelResult, audit: AuditRecord): Promise<Buffer>;
|
|
19
21
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xlsx.d.ts","sourceRoot":"","sources":["../../src/formatters/xlsx.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"xlsx.d.ts","sourceRoot":"","sources":["../../src/formatters/xlsx.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AA6CrD;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,MAAM,CAAC,CAipBjB;AAED;;GAEG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,KAAK,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB,EAAE,MAAM,CAAC;IAC5B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC,EACF,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,MAAM,CAAC,CAqNjB"}
|
package/dist/formatters/xlsx.js
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* 2. Modify assumptions
|
|
7
7
|
* 3. Submit to HTA bodies
|
|
8
8
|
*
|
|
9
|
-
* Workbooks use
|
|
10
|
-
* results recalculate. Multi-tab structure:
|
|
9
|
+
* Workbooks use live Excel formulas so inputs can be changed and
|
|
10
|
+
* results recalculate. Multi-tab structure: Summary, Inputs, Transition Matrix,
|
|
11
|
+
* Markov Trace, PSA, CEAC, Audit.
|
|
11
12
|
*/
|
|
12
13
|
import ExcelJS from "exceljs";
|
|
13
14
|
const HEADER_FILL = {
|
|
@@ -41,9 +42,19 @@ function styleHeaderCell(cell) {
|
|
|
41
42
|
right: { style: "thin" },
|
|
42
43
|
};
|
|
43
44
|
}
|
|
45
|
+
function getNCycles(time_horizon) {
|
|
46
|
+
if (time_horizon === "lifetime")
|
|
47
|
+
return 40;
|
|
48
|
+
if (time_horizon === "5yr")
|
|
49
|
+
return 5;
|
|
50
|
+
if (time_horizon === "10yr")
|
|
51
|
+
return 10;
|
|
52
|
+
return Number(time_horizon);
|
|
53
|
+
}
|
|
44
54
|
/**
|
|
45
55
|
* Build a cost-effectiveness model Excel workbook.
|
|
46
|
-
* Inputs are editable (yellow)
|
|
56
|
+
* Inputs are editable (yellow); formula cells (blue) update automatically
|
|
57
|
+
* when Inputs values are changed and the workbook is saved/recalculated.
|
|
47
58
|
*/
|
|
48
59
|
export async function ceModelToXlsx(params, result, audit) {
|
|
49
60
|
const wb = new ExcelJS.Workbook();
|
|
@@ -52,6 +63,15 @@ export async function ceModelToXlsx(params, result, audit) {
|
|
|
52
63
|
const perspective = params.perspective;
|
|
53
64
|
const currency = perspective === "nhs" ? "GBP" : "USD";
|
|
54
65
|
const symbol = perspective === "nhs" ? "£" : "$";
|
|
66
|
+
const n_cycles = getNCycles(params.time_horizon);
|
|
67
|
+
// Precompute transition probabilities (needed for result caching in formula cells)
|
|
68
|
+
const efficacyDelta = Math.max(0, Math.min(0.999, params.clinical_inputs.efficacy_delta));
|
|
69
|
+
const mortalityReduction = params.clinical_inputs.mortality_reduction ?? 0;
|
|
70
|
+
const baseMortality = 0.02;
|
|
71
|
+
const interventionMortality = Math.max(0.005, baseMortality * (1 - mortalityReduction));
|
|
72
|
+
const comparatorMortality = baseMortality;
|
|
73
|
+
const probStayOnIntervention = Math.max(0.05, Math.min(0.93, 0.5 + efficacyDelta * 0.5));
|
|
74
|
+
const baselineProbStayOn = Math.max(0.05, Math.min(0.88, probStayOnIntervention * 0.7));
|
|
55
75
|
// --- Tab 1: Summary ---
|
|
56
76
|
const summary = wb.addWorksheet("Summary");
|
|
57
77
|
summary.columns = [
|
|
@@ -60,7 +80,7 @@ export async function ceModelToXlsx(params, result, audit) {
|
|
|
60
80
|
];
|
|
61
81
|
styleHeaderCell(summary.getCell("A1"));
|
|
62
82
|
styleHeaderCell(summary.getCell("B1"));
|
|
63
|
-
const
|
|
83
|
+
const staticRows = [
|
|
64
84
|
["Intervention", params.intervention],
|
|
65
85
|
["Comparator", params.comparator],
|
|
66
86
|
["Indication", params.indication],
|
|
@@ -70,34 +90,79 @@ export async function ceModelToXlsx(params, result, audit) {
|
|
|
70
90
|
["Model Type", params.model_type ?? "markov"],
|
|
71
91
|
["Discount Rate", 0.035],
|
|
72
92
|
["", ""],
|
|
73
|
-
[
|
|
74
|
-
"ICER",
|
|
75
|
-
isFinite(result.base_case.icer) ? result.base_case.icer : "Dominated",
|
|
76
|
-
],
|
|
77
|
-
["Delta Cost", result.base_case.delta_cost],
|
|
78
|
-
["Delta QALY", result.base_case.delta_qaly],
|
|
79
|
-
["Incremental Life Years", result.base_case.incremental_lys],
|
|
80
|
-
["Total Cost Intervention", result.base_case.total_cost_intervention],
|
|
81
|
-
["Total Cost Comparator", result.base_case.total_cost_comparator],
|
|
82
|
-
["Total QALYs Intervention", result.base_case.total_qaly_intervention],
|
|
83
|
-
["Total QALYs Comparator", result.base_case.total_qaly_comparator],
|
|
84
93
|
];
|
|
85
|
-
|
|
94
|
+
staticRows.forEach(([metric, value], i) => {
|
|
86
95
|
const row = summary.getRow(i + 2);
|
|
87
96
|
row.getCell(1).value = metric;
|
|
88
97
|
row.getCell(2).value = value;
|
|
89
|
-
if (
|
|
90
|
-
metric === "ICER") {
|
|
91
|
-
row.getCell(2).numFmt = `"${symbol}"#,##0`;
|
|
92
|
-
}
|
|
93
|
-
else if (typeof value === "number" &&
|
|
94
|
-
(metric.includes("QALY") || metric.includes("Life Years"))) {
|
|
95
|
-
row.getCell(2).numFmt = "0.000";
|
|
96
|
-
}
|
|
97
|
-
else if (metric === "Discount Rate") {
|
|
98
|
+
if (metric === "Discount Rate") {
|
|
98
99
|
row.getCell(2).numFmt = "0.0%";
|
|
99
100
|
}
|
|
100
101
|
});
|
|
102
|
+
// Rows 11-18: formula cells referencing Markov Trace
|
|
103
|
+
const lastTraceRow = 2 + n_cycles; // row 2 = cycle 0, row 2+n_cycles = last cycle
|
|
104
|
+
// Row 11: ICER
|
|
105
|
+
summary.getRow(11).getCell(1).value = "ICER";
|
|
106
|
+
summary.getRow(11).getCell(2).value = {
|
|
107
|
+
formula: `=IF(B13=0,"Dominated",B12/B13)`,
|
|
108
|
+
result: isFinite(result.base_case.icer)
|
|
109
|
+
? result.base_case.icer
|
|
110
|
+
: "Dominated",
|
|
111
|
+
};
|
|
112
|
+
summary.getRow(11).getCell(2).numFmt = `"${symbol}"#,##0`;
|
|
113
|
+
summary.getRow(11).getCell(2).fill = FORMULA_FILL;
|
|
114
|
+
// Row 12: Delta Cost
|
|
115
|
+
summary.getRow(12).getCell(1).value = "Delta Cost";
|
|
116
|
+
summary.getRow(12).getCell(2).value = {
|
|
117
|
+
formula: `=B15-B16`,
|
|
118
|
+
result: result.base_case.delta_cost,
|
|
119
|
+
};
|
|
120
|
+
summary.getRow(12).getCell(2).numFmt = `"${symbol}"#,##0`;
|
|
121
|
+
summary.getRow(12).getCell(2).fill = FORMULA_FILL;
|
|
122
|
+
// Row 13: Delta QALY
|
|
123
|
+
summary.getRow(13).getCell(1).value = "Delta QALY";
|
|
124
|
+
summary.getRow(13).getCell(2).value = {
|
|
125
|
+
formula: `=B17-B18`,
|
|
126
|
+
result: result.base_case.delta_qaly,
|
|
127
|
+
};
|
|
128
|
+
summary.getRow(13).getCell(2).numFmt = "0.000";
|
|
129
|
+
summary.getRow(13).getCell(2).fill = FORMULA_FILL;
|
|
130
|
+
// Row 14: Incremental Life Years — keep static (complex lifecycle, minor value)
|
|
131
|
+
summary.getRow(14).getCell(1).value = "Incremental Life Years";
|
|
132
|
+
summary.getRow(14).getCell(2).value = result.base_case.incremental_lys;
|
|
133
|
+
summary.getRow(14).getCell(2).numFmt = "0.000";
|
|
134
|
+
// Row 15: Total Cost Intervention
|
|
135
|
+
summary.getRow(15).getCell(1).value = "Total Cost Intervention";
|
|
136
|
+
summary.getRow(15).getCell(2).value = {
|
|
137
|
+
formula: `=SUM('Markov Trace'!$I$2:$I$${lastTraceRow})`,
|
|
138
|
+
result: result.base_case.total_cost_intervention,
|
|
139
|
+
};
|
|
140
|
+
summary.getRow(15).getCell(2).numFmt = `"${symbol}"#,##0`;
|
|
141
|
+
summary.getRow(15).getCell(2).fill = FORMULA_FILL;
|
|
142
|
+
// Row 16: Total Cost Comparator
|
|
143
|
+
summary.getRow(16).getCell(1).value = "Total Cost Comparator";
|
|
144
|
+
summary.getRow(16).getCell(2).value = {
|
|
145
|
+
formula: `=SUM('Markov Trace'!$J$2:$J$${lastTraceRow})`,
|
|
146
|
+
result: result.base_case.total_cost_comparator,
|
|
147
|
+
};
|
|
148
|
+
summary.getRow(16).getCell(2).numFmt = `"${symbol}"#,##0`;
|
|
149
|
+
summary.getRow(16).getCell(2).fill = FORMULA_FILL;
|
|
150
|
+
// Row 17: Total QALYs Intervention
|
|
151
|
+
summary.getRow(17).getCell(1).value = "Total QALYs Intervention";
|
|
152
|
+
summary.getRow(17).getCell(2).value = {
|
|
153
|
+
formula: `=SUM('Markov Trace'!$L$2:$L$${lastTraceRow})`,
|
|
154
|
+
result: result.base_case.total_qaly_intervention,
|
|
155
|
+
};
|
|
156
|
+
summary.getRow(17).getCell(2).numFmt = "0.000";
|
|
157
|
+
summary.getRow(17).getCell(2).fill = FORMULA_FILL;
|
|
158
|
+
// Row 18: Total QALYs Comparator
|
|
159
|
+
summary.getRow(18).getCell(1).value = "Total QALYs Comparator";
|
|
160
|
+
summary.getRow(18).getCell(2).value = {
|
|
161
|
+
formula: `=SUM('Markov Trace'!$M$2:$M$${lastTraceRow})`,
|
|
162
|
+
result: result.base_case.total_qaly_comparator,
|
|
163
|
+
};
|
|
164
|
+
summary.getRow(18).getCell(2).numFmt = "0.000";
|
|
165
|
+
summary.getRow(18).getCell(2).fill = FORMULA_FILL;
|
|
101
166
|
// --- Tab 2: Inputs (editable) ---
|
|
102
167
|
const inputs = wb.addWorksheet("Inputs");
|
|
103
168
|
inputs.columns = [
|
|
@@ -159,6 +224,11 @@ export async function ceModelToXlsx(params, result, audit) {
|
|
|
159
224
|
["Discount rate (costs)", 0.035, "annual %", "NICE reference case"],
|
|
160
225
|
["Discount rate (outcomes)", 0.035, "annual %", "NICE reference case"],
|
|
161
226
|
];
|
|
227
|
+
// Inputs row mapping (1-based after header):
|
|
228
|
+
// B2 = drug_cost_annual, B3 = comparator_cost_annual, B4 = admin_cost, B5 = ae_cost
|
|
229
|
+
// B6 = efficacy_delta, B7 = mortality_reduction
|
|
230
|
+
// B8 = qaly_on_treatment, B9 = qaly_comparator
|
|
231
|
+
// B10 = discount_rate_costs, B11 = discount_rate_outcomes
|
|
162
232
|
inputRows.forEach(([param, value, unit, notes], i) => {
|
|
163
233
|
const row = inputs.getRow(i + 2);
|
|
164
234
|
row.getCell(1).value = param;
|
|
@@ -174,21 +244,12 @@ export async function ceModelToXlsx(params, result, audit) {
|
|
|
174
244
|
row.getCell(2).numFmt = "0.000";
|
|
175
245
|
});
|
|
176
246
|
inputs.getRow(inputRows.length + 3).getCell(1).value =
|
|
177
|
-
"Inputs
|
|
247
|
+
"Inputs are linked to the Markov Trace, Transition Matrix, Results, and CEAC sheets — edit any value and save to trigger Excel recalculation.";
|
|
178
248
|
inputs.getRow(inputRows.length + 3).getCell(1).font = {
|
|
179
249
|
italic: true,
|
|
180
250
|
color: { argb: "FF666666" },
|
|
181
251
|
};
|
|
182
|
-
// --- Tab 3: Transition Matrix ---
|
|
183
|
-
// Derive actual transition probabilities from model params (same logic as
|
|
184
|
-
// src/models/modelUtils.ts buildMarkovParamsFromCE).
|
|
185
|
-
const efficacyDelta = Math.max(0, Math.min(0.999, params.clinical_inputs.efficacy_delta));
|
|
186
|
-
const mortalityReduction = params.clinical_inputs.mortality_reduction ?? 0;
|
|
187
|
-
const baseMortality = 0.02;
|
|
188
|
-
const interventionMortality = Math.max(0.005, baseMortality * (1 - mortalityReduction));
|
|
189
|
-
const comparatorMortality = baseMortality;
|
|
190
|
-
const probStayOnIntervention = Math.max(0.05, Math.min(0.93, 0.5 + efficacyDelta * 0.5));
|
|
191
|
-
const baselineProbStayOn = Math.max(0.05, Math.min(0.88, probStayOnIntervention * 0.7));
|
|
252
|
+
// --- Tab 3: Transition Matrix (formula cells referencing Inputs) ---
|
|
192
253
|
const trans = wb.addWorksheet("Transition Matrix");
|
|
193
254
|
trans.columns = [
|
|
194
255
|
{ header: "From \\ To", key: "state", width: 20 },
|
|
@@ -197,54 +258,284 @@ export async function ceModelToXlsx(params, result, audit) {
|
|
|
197
258
|
{ header: "Dead", key: "dead", width: 16 },
|
|
198
259
|
];
|
|
199
260
|
["A1", "B1", "C1", "D1"].forEach((c) => styleHeaderCell(trans.getCell(c)));
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
trans.getRow(
|
|
207
|
-
|
|
208
|
-
0.
|
|
209
|
-
Math.max(0,
|
|
210
|
-
|
|
211
|
-
|
|
261
|
+
// Row 2: On-Treatment (Intervention)
|
|
262
|
+
trans.getRow(2).getCell(1).value = "On-Treatment (Intervention)";
|
|
263
|
+
trans.getRow(2).getCell(2).value = {
|
|
264
|
+
formula: `=MIN(0.93,MAX(0.05,0.5+Inputs!$B$6*0.5))`,
|
|
265
|
+
result: probStayOnIntervention,
|
|
266
|
+
};
|
|
267
|
+
trans.getRow(2).getCell(2).fill = FORMULA_FILL;
|
|
268
|
+
trans.getRow(2).getCell(3).value = {
|
|
269
|
+
formula: `=MAX(0,1-B2-MAX(0.005,0.02*(1-Inputs!$B$7)))`,
|
|
270
|
+
result: Math.max(0, 1 - probStayOnIntervention - interventionMortality),
|
|
271
|
+
};
|
|
272
|
+
trans.getRow(2).getCell(3).fill = FORMULA_FILL;
|
|
273
|
+
trans.getRow(2).getCell(4).value = {
|
|
274
|
+
formula: `=MAX(0.005,0.02*(1-Inputs!$B$7))`,
|
|
275
|
+
result: interventionMortality,
|
|
276
|
+
};
|
|
277
|
+
trans.getRow(2).getCell(4).fill = FORMULA_FILL;
|
|
278
|
+
// Row 3: Off-Treatment (Intervention)
|
|
279
|
+
trans.getRow(3).getCell(1).value = "Off-Treatment (Intervention)";
|
|
280
|
+
trans.getRow(3).getCell(2).value = {
|
|
281
|
+
formula: `=0.05`,
|
|
282
|
+
result: 0.05,
|
|
283
|
+
};
|
|
284
|
+
trans.getRow(3).getCell(2).fill = FORMULA_FILL;
|
|
285
|
+
trans.getRow(3).getCell(3).value = {
|
|
286
|
+
formula: `=MAX(0,0.95-MAX(0.005,0.02*(1-Inputs!$B$7)))`,
|
|
287
|
+
result: Math.max(0, 0.95 - interventionMortality),
|
|
288
|
+
};
|
|
289
|
+
trans.getRow(3).getCell(3).fill = FORMULA_FILL;
|
|
290
|
+
trans.getRow(3).getCell(4).value = {
|
|
291
|
+
formula: `=MAX(0.005,0.02*(1-Inputs!$B$7))`,
|
|
292
|
+
result: interventionMortality,
|
|
293
|
+
};
|
|
294
|
+
trans.getRow(3).getCell(4).fill = FORMULA_FILL;
|
|
295
|
+
// Row 4: Dead (Intervention) — static
|
|
212
296
|
trans.getRow(4).values = ["Dead", 0, 0, 1];
|
|
297
|
+
// Row 5: empty separator
|
|
213
298
|
trans.getRow(5).values = [""];
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
trans.getRow(
|
|
221
|
-
|
|
222
|
-
0.
|
|
223
|
-
Math.max(0,
|
|
224
|
-
|
|
225
|
-
|
|
299
|
+
// Row 6: On-Treatment (Comparator)
|
|
300
|
+
trans.getRow(6).getCell(1).value = "On-Treatment (Comparator)";
|
|
301
|
+
trans.getRow(6).getCell(2).value = {
|
|
302
|
+
formula: `=MIN(0.88,MAX(0.05,MIN(0.93,MAX(0.05,0.5+Inputs!$B$6*0.5))*0.7))`,
|
|
303
|
+
result: baselineProbStayOn,
|
|
304
|
+
};
|
|
305
|
+
trans.getRow(6).getCell(2).fill = FORMULA_FILL;
|
|
306
|
+
trans.getRow(6).getCell(3).value = {
|
|
307
|
+
formula: `=MAX(0,1-B6-0.02)`,
|
|
308
|
+
result: Math.max(0, 1 - baselineProbStayOn - comparatorMortality),
|
|
309
|
+
};
|
|
310
|
+
trans.getRow(6).getCell(3).fill = FORMULA_FILL;
|
|
311
|
+
trans.getRow(6).getCell(4).value = 0.02;
|
|
312
|
+
// Row 7: Off-Treatment (Comparator)
|
|
313
|
+
trans.getRow(7).getCell(1).value = "Off-Treatment (Comparator)";
|
|
314
|
+
trans.getRow(7).getCell(2).value = {
|
|
315
|
+
formula: `=0.05`,
|
|
316
|
+
result: 0.05,
|
|
317
|
+
};
|
|
318
|
+
trans.getRow(7).getCell(2).fill = FORMULA_FILL;
|
|
319
|
+
trans.getRow(7).getCell(3).value = {
|
|
320
|
+
formula: `=MAX(0,0.95-0.02)`,
|
|
321
|
+
result: 0.93,
|
|
322
|
+
};
|
|
323
|
+
trans.getRow(7).getCell(3).fill = FORMULA_FILL;
|
|
324
|
+
trans.getRow(7).getCell(4).value = 0.02;
|
|
325
|
+
// Row 8: Dead (Comparator) — static
|
|
226
326
|
trans.getRow(8).values = ["Dead", 0, 0, 1];
|
|
327
|
+
// Apply number format to all data cells
|
|
227
328
|
for (let r = 2; r <= 8; r++) {
|
|
228
329
|
for (let c = 2; c <= 4; c++) {
|
|
229
330
|
const cell = trans.getRow(r).getCell(c);
|
|
230
|
-
if (
|
|
331
|
+
if (cell.value !== undefined && cell.value !== "") {
|
|
231
332
|
cell.numFmt = "0.0000";
|
|
232
333
|
}
|
|
233
334
|
}
|
|
234
335
|
}
|
|
235
336
|
trans.getRow(10).getCell(1).value =
|
|
236
|
-
"
|
|
337
|
+
"Rows must sum to 1.0. Formulas reference Inputs tab — edit efficacy_delta or mortality_reduction to update.";
|
|
237
338
|
trans.getRow(10).getCell(1).font = {
|
|
238
339
|
italic: true,
|
|
239
340
|
color: { argb: "FF666666" },
|
|
240
341
|
};
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
342
|
+
// --- Tab 4: Markov Trace (NEW — live formulas for state populations and discounted costs/QALYs) ---
|
|
343
|
+
const traceSheet = wb.addWorksheet("Markov Trace");
|
|
344
|
+
traceSheet.columns = [
|
|
345
|
+
{ header: "Cycle", key: "cycle", width: 8 },
|
|
346
|
+
{ header: "Int-On", key: "i_on", width: 12 },
|
|
347
|
+
{ header: "Int-Off", key: "i_off", width: 12 },
|
|
348
|
+
{ header: "Int-Dead", key: "i_dead", width: 12 },
|
|
349
|
+
{ header: "Comp-On", key: "c_on", width: 12 },
|
|
350
|
+
{ header: "Comp-Off", key: "c_off", width: 12 },
|
|
351
|
+
{ header: "Comp-Dead", key: "c_dead", width: 12 },
|
|
352
|
+
{ header: "Cost Disc Factor", key: "hdf", width: 18 },
|
|
353
|
+
{ header: "Int Disc Cost", key: "idc", width: 18 },
|
|
354
|
+
{ header: "Comp Disc Cost", key: "cdc", width: 18 },
|
|
355
|
+
{ header: "QALY Disc Factor", key: "kdf", width: 18 },
|
|
356
|
+
{ header: "Int Disc QALY", key: "idq", width: 18 },
|
|
357
|
+
{ header: "Comp Disc QALY", key: "cdq", width: 18 },
|
|
358
|
+
];
|
|
359
|
+
for (let c = 1; c <= 13; c++) {
|
|
360
|
+
styleHeaderCell(traceSheet.getRow(1).getCell(c));
|
|
361
|
+
}
|
|
362
|
+
// Row 2: Cycle 0 (initial state)
|
|
363
|
+
// Precompute initial discounted cost/QALY for result caching
|
|
364
|
+
const drugCost = params.cost_inputs.drug_cost_annual;
|
|
365
|
+
const comparatorCost = params.cost_inputs.comparator_cost_annual;
|
|
366
|
+
const adminCost = params.cost_inputs.admin_cost ?? 0;
|
|
367
|
+
const qalyOnTx = params.utility_inputs?.qaly_on_treatment ?? 0.75;
|
|
368
|
+
const qalyComparator = params.utility_inputs?.qaly_comparator ?? 0.7;
|
|
369
|
+
const initIntDiscCost = drugCost + adminCost; // cycle 0: 100% On-Treatment
|
|
370
|
+
const initCompDiscCost = comparatorCost + adminCost;
|
|
371
|
+
const initIntDiscQaly = qalyOnTx; // cycle 0: 100% On-Treatment
|
|
372
|
+
const initCompDiscQaly = qalyOnTx; // cycle 0: comparator also starts On-Treatment
|
|
373
|
+
traceSheet.getRow(2).getCell(1).value = 0; // cycle
|
|
374
|
+
traceSheet.getRow(2).getCell(2).value = 1; // Int-On = 1
|
|
375
|
+
traceSheet.getRow(2).getCell(3).value = 0; // Int-Off = 0
|
|
376
|
+
traceSheet.getRow(2).getCell(4).value = 0; // Int-Dead = 0
|
|
377
|
+
traceSheet.getRow(2).getCell(5).value = 1; // Comp-On = 1
|
|
378
|
+
traceSheet.getRow(2).getCell(6).value = 0; // Comp-Off = 0
|
|
379
|
+
traceSheet.getRow(2).getCell(7).value = 0; // Comp-Dead = 0
|
|
380
|
+
// H2: cost discount factor at cycle 0 = 1
|
|
381
|
+
traceSheet.getRow(2).getCell(8).value = { formula: `=1`, result: 1 };
|
|
382
|
+
traceSheet.getRow(2).getCell(8).fill = FORMULA_FILL;
|
|
383
|
+
// I2: Int discounted cost at cycle 0
|
|
384
|
+
traceSheet.getRow(2).getCell(9).value = {
|
|
385
|
+
formula: `=H2*(B2*(Inputs!$B$2+Inputs!$B$4))`,
|
|
386
|
+
result: initIntDiscCost,
|
|
387
|
+
};
|
|
388
|
+
traceSheet.getRow(2).getCell(9).fill = FORMULA_FILL;
|
|
389
|
+
// J2: Comp discounted cost at cycle 0
|
|
390
|
+
traceSheet.getRow(2).getCell(10).value = {
|
|
391
|
+
formula: `=H2*(E2*(Inputs!$B$3+Inputs!$B$4))`,
|
|
392
|
+
result: initCompDiscCost,
|
|
393
|
+
};
|
|
394
|
+
traceSheet.getRow(2).getCell(10).fill = FORMULA_FILL;
|
|
395
|
+
// K2: QALY discount factor at cycle 0 = 1
|
|
396
|
+
traceSheet.getRow(2).getCell(11).value = { formula: `=1`, result: 1 };
|
|
397
|
+
traceSheet.getRow(2).getCell(11).fill = FORMULA_FILL;
|
|
398
|
+
// L2: Int discounted QALY at cycle 0
|
|
399
|
+
traceSheet.getRow(2).getCell(12).value = {
|
|
400
|
+
formula: `=K2*(B2*Inputs!$B$8+C2*Inputs!$B$9)`,
|
|
401
|
+
result: initIntDiscQaly,
|
|
246
402
|
};
|
|
247
|
-
|
|
403
|
+
traceSheet.getRow(2).getCell(12).fill = FORMULA_FILL;
|
|
404
|
+
// M2: Comp discounted QALY at cycle 0
|
|
405
|
+
traceSheet.getRow(2).getCell(13).value = {
|
|
406
|
+
formula: `=K2*(E2*Inputs!$B$8+F2*Inputs!$B$9)`,
|
|
407
|
+
result: initCompDiscQaly,
|
|
408
|
+
};
|
|
409
|
+
traceSheet.getRow(2).getCell(13).fill = FORMULA_FILL;
|
|
410
|
+
// Apply number formats to row 2
|
|
411
|
+
traceSheet.getRow(2).getCell(9).numFmt = `"${symbol}"#,##0`;
|
|
412
|
+
traceSheet.getRow(2).getCell(10).numFmt = `"${symbol}"#,##0`;
|
|
413
|
+
traceSheet.getRow(2).getCell(12).numFmt = "0.000";
|
|
414
|
+
traceSheet.getRow(2).getCell(13).numFmt = "0.000";
|
|
415
|
+
// Rows 3..n_cycles+2: one row per cycle 1..n_cycles
|
|
416
|
+
// Precompute Markov trace for result caching
|
|
417
|
+
let intOn = 1, intOff = 0, intDead = 0;
|
|
418
|
+
let compOn = 1, compOff = 0, compDead = 0;
|
|
419
|
+
let costDiscFactor = 1;
|
|
420
|
+
let qalyDiscFactor = 1;
|
|
421
|
+
const discRateCosts = 0.035;
|
|
422
|
+
const discRateOutcomes = 0.035;
|
|
423
|
+
// Transition probabilities (precomputed)
|
|
424
|
+
const pIntOnOn = probStayOnIntervention;
|
|
425
|
+
const pIntOffOn = 0.05;
|
|
426
|
+
const pIntOnOff = Math.max(0, 1 - probStayOnIntervention - interventionMortality);
|
|
427
|
+
const pIntOffOff = Math.max(0, 0.95 - interventionMortality);
|
|
428
|
+
const pIntOnDead = interventionMortality;
|
|
429
|
+
const pIntOffDead = interventionMortality;
|
|
430
|
+
const pCompOnOn = baselineProbStayOn;
|
|
431
|
+
const pCompOffOn = 0.05;
|
|
432
|
+
const pCompOnOff = Math.max(0, 1 - baselineProbStayOn - comparatorMortality);
|
|
433
|
+
const pCompOffOff = Math.max(0, 0.95 - comparatorMortality);
|
|
434
|
+
const pCompOnDead = comparatorMortality;
|
|
435
|
+
const pCompOffDead = comparatorMortality;
|
|
436
|
+
for (let cycle = 1; cycle <= n_cycles; cycle++) {
|
|
437
|
+
const r = cycle + 2; // Excel row number
|
|
438
|
+
const prev = r - 1;
|
|
439
|
+
// Apply transitions for caching
|
|
440
|
+
costDiscFactor = costDiscFactor * (1 / (1 + discRateCosts));
|
|
441
|
+
qalyDiscFactor = qalyDiscFactor * (1 / (1 + discRateOutcomes));
|
|
442
|
+
const newIntOn = pIntOnOn * intOn + pIntOffOn * intOff; // Dead stays dead → 0 contribution
|
|
443
|
+
const newIntOff = pIntOnOff * intOn + pIntOffOff * intOff;
|
|
444
|
+
const newIntDead = pIntOnDead * intOn + pIntOffDead * intOff + intDead;
|
|
445
|
+
intOn = newIntOn;
|
|
446
|
+
intOff = newIntOff;
|
|
447
|
+
intDead = newIntDead;
|
|
448
|
+
const newCompOn = pCompOnOn * compOn + pCompOffOn * compOff;
|
|
449
|
+
const newCompOff = pCompOnOff * compOn + pCompOffOff * compOff;
|
|
450
|
+
const newCompDead = pCompOnDead * compOn + pCompOffDead * compOff + compDead;
|
|
451
|
+
compOn = newCompOn;
|
|
452
|
+
compOff = newCompOff;
|
|
453
|
+
compDead = newCompDead;
|
|
454
|
+
const intDiscCost = costDiscFactor * (intOn * (drugCost + adminCost));
|
|
455
|
+
const compDiscCost = costDiscFactor * (compOn * (comparatorCost + adminCost));
|
|
456
|
+
const intDiscQaly = qalyDiscFactor * (intOn * qalyOnTx + intOff * qalyComparator);
|
|
457
|
+
const compDiscQaly = qalyDiscFactor * (compOn * qalyOnTx + compOff * qalyComparator);
|
|
458
|
+
const row = traceSheet.getRow(r);
|
|
459
|
+
// A: cycle number
|
|
460
|
+
row.getCell(1).value = cycle;
|
|
461
|
+
// B: Int-On
|
|
462
|
+
row.getCell(2).value = {
|
|
463
|
+
formula: `='Transition Matrix'!$B$2*B${prev}+'Transition Matrix'!$B$3*C${prev}+'Transition Matrix'!$B$4*D${prev}`,
|
|
464
|
+
result: intOn,
|
|
465
|
+
};
|
|
466
|
+
row.getCell(2).fill = FORMULA_FILL;
|
|
467
|
+
// C: Int-Off
|
|
468
|
+
row.getCell(3).value = {
|
|
469
|
+
formula: `='Transition Matrix'!$C$2*B${prev}+'Transition Matrix'!$C$3*C${prev}+'Transition Matrix'!$C$4*D${prev}`,
|
|
470
|
+
result: intOff,
|
|
471
|
+
};
|
|
472
|
+
row.getCell(3).fill = FORMULA_FILL;
|
|
473
|
+
// D: Int-Dead
|
|
474
|
+
row.getCell(4).value = {
|
|
475
|
+
formula: `='Transition Matrix'!$D$2*B${prev}+'Transition Matrix'!$D$3*C${prev}+'Transition Matrix'!$D$4*D${prev}`,
|
|
476
|
+
result: intDead,
|
|
477
|
+
};
|
|
478
|
+
row.getCell(4).fill = FORMULA_FILL;
|
|
479
|
+
// E: Comp-On
|
|
480
|
+
row.getCell(5).value = {
|
|
481
|
+
formula: `='Transition Matrix'!$B$6*E${prev}+'Transition Matrix'!$B$7*F${prev}+'Transition Matrix'!$B$8*G${prev}`,
|
|
482
|
+
result: compOn,
|
|
483
|
+
};
|
|
484
|
+
row.getCell(5).fill = FORMULA_FILL;
|
|
485
|
+
// F: Comp-Off
|
|
486
|
+
row.getCell(6).value = {
|
|
487
|
+
formula: `='Transition Matrix'!$C$6*E${prev}+'Transition Matrix'!$C$7*F${prev}+'Transition Matrix'!$C$8*G${prev}`,
|
|
488
|
+
result: compOff,
|
|
489
|
+
};
|
|
490
|
+
row.getCell(6).fill = FORMULA_FILL;
|
|
491
|
+
// G: Comp-Dead
|
|
492
|
+
row.getCell(7).value = {
|
|
493
|
+
formula: `='Transition Matrix'!$D$6*E${prev}+'Transition Matrix'!$D$7*F${prev}+'Transition Matrix'!$D$8*G${prev}`,
|
|
494
|
+
result: compDead,
|
|
495
|
+
};
|
|
496
|
+
row.getCell(7).fill = FORMULA_FILL;
|
|
497
|
+
// H: Cost discount factor (recurrence)
|
|
498
|
+
row.getCell(8).value = {
|
|
499
|
+
formula: `=H${prev}*(1/(1+Inputs!$B$10))`,
|
|
500
|
+
result: costDiscFactor,
|
|
501
|
+
};
|
|
502
|
+
row.getCell(8).fill = FORMULA_FILL;
|
|
503
|
+
// I: Int discounted cost
|
|
504
|
+
row.getCell(9).value = {
|
|
505
|
+
formula: `=H${r}*(B${r}*(Inputs!$B$2+Inputs!$B$4))`,
|
|
506
|
+
result: intDiscCost,
|
|
507
|
+
};
|
|
508
|
+
row.getCell(9).fill = FORMULA_FILL;
|
|
509
|
+
row.getCell(9).numFmt = `"${symbol}"#,##0`;
|
|
510
|
+
// J: Comp discounted cost
|
|
511
|
+
row.getCell(10).value = {
|
|
512
|
+
formula: `=H${r}*(E${r}*(Inputs!$B$3+Inputs!$B$4))`,
|
|
513
|
+
result: compDiscCost,
|
|
514
|
+
};
|
|
515
|
+
row.getCell(10).fill = FORMULA_FILL;
|
|
516
|
+
row.getCell(10).numFmt = `"${symbol}"#,##0`;
|
|
517
|
+
// K: QALY discount factor (recurrence)
|
|
518
|
+
row.getCell(11).value = {
|
|
519
|
+
formula: `=K${prev}*(1/(1+Inputs!$B$11))`,
|
|
520
|
+
result: qalyDiscFactor,
|
|
521
|
+
};
|
|
522
|
+
row.getCell(11).fill = FORMULA_FILL;
|
|
523
|
+
// L: Int discounted QALY
|
|
524
|
+
row.getCell(12).value = {
|
|
525
|
+
formula: `=K${r}*(B${r}*Inputs!$B$8+C${r}*Inputs!$B$9)`,
|
|
526
|
+
result: intDiscQaly,
|
|
527
|
+
};
|
|
528
|
+
row.getCell(12).fill = FORMULA_FILL;
|
|
529
|
+
row.getCell(12).numFmt = "0.000";
|
|
530
|
+
// M: Comp discounted QALY
|
|
531
|
+
row.getCell(13).value = {
|
|
532
|
+
formula: `=K${r}*(E${r}*Inputs!$B$8+F${r}*Inputs!$B$9)`,
|
|
533
|
+
result: compDiscQaly,
|
|
534
|
+
};
|
|
535
|
+
row.getCell(13).fill = FORMULA_FILL;
|
|
536
|
+
row.getCell(13).numFmt = "0.000";
|
|
537
|
+
}
|
|
538
|
+
// --- Tab 5: PSA Iterations ---
|
|
248
539
|
if (result.psa && result.psa.scatter.length > 0) {
|
|
249
540
|
const psaSheet = wb.addWorksheet("PSA");
|
|
250
541
|
psaSheet.columns = [
|
|
@@ -291,7 +582,7 @@ export async function ceModelToXlsx(params, result, audit) {
|
|
|
291
582
|
psaSheet.getRow(summaryRow + 1).getCell(1).font = { bold: true };
|
|
292
583
|
psaSheet.getRow(summaryRow + 1).getCell(2).numFmt = `"${symbol}"#,##0`;
|
|
293
584
|
}
|
|
294
|
-
// --- Tab
|
|
585
|
+
// --- Tab 6: CEAC (COUNTIF formulas referencing PSA sheet) ---
|
|
295
586
|
if (result.psa && result.psa.ceac.length > 0) {
|
|
296
587
|
const ceacSheet = wb.addWorksheet("CEAC");
|
|
297
588
|
ceacSheet.columns = [
|
|
@@ -299,15 +590,21 @@ export async function ceModelToXlsx(params, result, audit) {
|
|
|
299
590
|
{ header: "P(cost-effective)", key: "p", width: 20 },
|
|
300
591
|
];
|
|
301
592
|
["A1", "B1"].forEach((c) => styleHeaderCell(ceacSheet.getCell(c)));
|
|
593
|
+
const n_psa = result.psa.scatter.length;
|
|
594
|
+
const lastPSARow = n_psa + 1;
|
|
302
595
|
result.psa.ceac.forEach((pt, i) => {
|
|
303
596
|
const row = ceacSheet.getRow(i + 2);
|
|
304
597
|
row.getCell(1).value = pt.wtp;
|
|
305
|
-
row.getCell(2).value = pt.prob_ce;
|
|
306
598
|
row.getCell(1).numFmt = `"${symbol}"#,##0`;
|
|
599
|
+
row.getCell(2).value = {
|
|
600
|
+
formula: `=COUNTIFS(PSA!$D$2:PSA!$D$${lastPSARow},"<>N/A",PSA!$D$2:PSA!$D$${lastPSARow},"<="&A${i + 2})/${n_psa}`,
|
|
601
|
+
result: pt.prob_ce,
|
|
602
|
+
};
|
|
603
|
+
row.getCell(2).fill = FORMULA_FILL;
|
|
307
604
|
row.getCell(2).numFmt = "0.00%";
|
|
308
605
|
});
|
|
309
606
|
}
|
|
310
|
-
// --- Tab
|
|
607
|
+
// --- Tab 7: Audit ---
|
|
311
608
|
const auditSheet = wb.addWorksheet("Audit");
|
|
312
609
|
auditSheet.columns = [
|
|
313
610
|
{ header: "Field", key: "field", width: 25 },
|
|
@@ -340,6 +637,9 @@ export async function bimToXlsx(params, results, audit) {
|
|
|
340
637
|
wb.created = new Date();
|
|
341
638
|
const perspective = params.perspective ?? "nhs";
|
|
342
639
|
const symbol = perspective === "nhs" ? "£" : "$";
|
|
640
|
+
const totalNet = results.reduce((s, r) => s + r.net_budget_impact, 0);
|
|
641
|
+
const totalTreated = results.reduce((s, r) => s + r.treated_population, 0);
|
|
642
|
+
const dataLastRow = results.length + 1; // last data row in Year-by-Year (1-based, row 2 = year 1)
|
|
343
643
|
// --- Tab 1: Summary ---
|
|
344
644
|
const summary = wb.addWorksheet("Summary");
|
|
345
645
|
summary.columns = [
|
|
@@ -348,8 +648,6 @@ export async function bimToXlsx(params, results, audit) {
|
|
|
348
648
|
];
|
|
349
649
|
styleHeaderCell(summary.getCell("A1"));
|
|
350
650
|
styleHeaderCell(summary.getCell("B1"));
|
|
351
|
-
const totalNet = results.reduce((s, r) => s + r.net_budget_impact, 0);
|
|
352
|
-
const totalTreated = results.reduce((s, r) => s + r.treated_population, 0);
|
|
353
651
|
[
|
|
354
652
|
["Intervention", params.intervention],
|
|
355
653
|
["Comparator", params.comparator],
|
|
@@ -358,21 +656,35 @@ export async function bimToXlsx(params, results, audit) {
|
|
|
358
656
|
["Time Horizon (years)", results.length],
|
|
359
657
|
["Eligible Population (Year 1)", params.eligible_population],
|
|
360
658
|
["", ""],
|
|
361
|
-
["Total Net Budget Impact", totalNet],
|
|
362
|
-
["Total Patients Treated", totalTreated],
|
|
363
|
-
["Average Net Cost per Patient", totalNet / (totalTreated || 1)],
|
|
364
659
|
].forEach((r, i) => {
|
|
365
660
|
const row = summary.getRow(i + 2);
|
|
366
661
|
row.getCell(1).value = r[0];
|
|
367
662
|
row.getCell(2).value = r[1];
|
|
368
|
-
const metric = r[0];
|
|
369
|
-
if (metric.includes("Cost") || metric.includes("Impact")) {
|
|
370
|
-
row.getCell(2).numFmt = `"${symbol}"#,##0`;
|
|
371
|
-
}
|
|
372
|
-
else if (metric.includes("Population") || metric.includes("Patients")) {
|
|
373
|
-
row.getCell(2).numFmt = "#,##0";
|
|
374
|
-
}
|
|
375
663
|
});
|
|
664
|
+
// Row 9: Total Net Budget Impact — SUM formula referencing Year-by-Year
|
|
665
|
+
summary.getRow(9).getCell(1).value = "Total Net Budget Impact";
|
|
666
|
+
summary.getRow(9).getCell(2).value = {
|
|
667
|
+
formula: `=SUM('Year-by-Year'!H2:H${dataLastRow})`,
|
|
668
|
+
result: totalNet,
|
|
669
|
+
};
|
|
670
|
+
summary.getRow(9).getCell(2).numFmt = `"${symbol}"#,##0`;
|
|
671
|
+
summary.getRow(9).getCell(2).fill = FORMULA_FILL;
|
|
672
|
+
// Row 10: Total Patients Treated
|
|
673
|
+
summary.getRow(10).getCell(1).value = "Total Patients Treated";
|
|
674
|
+
summary.getRow(10).getCell(2).value = {
|
|
675
|
+
formula: `=SUM('Year-by-Year'!C2:C${dataLastRow})`,
|
|
676
|
+
result: totalTreated,
|
|
677
|
+
};
|
|
678
|
+
summary.getRow(10).getCell(2).numFmt = "#,##0";
|
|
679
|
+
summary.getRow(10).getCell(2).fill = FORMULA_FILL;
|
|
680
|
+
// Row 11: Average Net Cost per Patient
|
|
681
|
+
summary.getRow(11).getCell(1).value = "Average Net Cost per Patient";
|
|
682
|
+
summary.getRow(11).getCell(2).value = {
|
|
683
|
+
formula: `=IF(B10=0,0,B9/B10)`,
|
|
684
|
+
result: totalTreated > 0 ? totalNet / totalTreated : 0,
|
|
685
|
+
};
|
|
686
|
+
summary.getRow(11).getCell(2).numFmt = `"${symbol}"#,##0`;
|
|
687
|
+
summary.getRow(11).getCell(2).fill = FORMULA_FILL;
|
|
376
688
|
// --- Tab 2: Inputs (editable) ---
|
|
377
689
|
const inputs = wb.addWorksheet("Inputs");
|
|
378
690
|
inputs.columns = [
|
|
@@ -482,11 +794,20 @@ export async function bimToXlsx(params, results, audit) {
|
|
|
482
794
|
for (let c = 5; c <= 9; c++)
|
|
483
795
|
row.getCell(c).numFmt = `"${symbol}"#,##0`;
|
|
484
796
|
});
|
|
485
|
-
// Total row
|
|
486
|
-
const
|
|
797
|
+
// Total row — SUM formulas
|
|
798
|
+
const totalRowNum = results.length + 2;
|
|
799
|
+
const totalRow = yearly.getRow(totalRowNum);
|
|
487
800
|
totalRow.getCell(1).value = "Total";
|
|
488
|
-
totalRow.getCell(3).value =
|
|
489
|
-
|
|
801
|
+
totalRow.getCell(3).value = {
|
|
802
|
+
formula: `=SUM(C2:C${dataLastRow})`,
|
|
803
|
+
result: totalTreated,
|
|
804
|
+
};
|
|
805
|
+
totalRow.getCell(3).fill = FORMULA_FILL;
|
|
806
|
+
totalRow.getCell(8).value = {
|
|
807
|
+
formula: `=SUM(H2:H${dataLastRow})`,
|
|
808
|
+
result: totalNet,
|
|
809
|
+
};
|
|
810
|
+
totalRow.getCell(8).fill = FORMULA_FILL;
|
|
490
811
|
totalRow.font = { bold: true };
|
|
491
812
|
totalRow.getCell(3).numFmt = "#,##0";
|
|
492
813
|
totalRow.getCell(8).numFmt = `"${symbol}"#,##0`;
|