tallyd 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +1493 -2
- package/dist/accounts/chart.d.ts +96 -0
- package/dist/accounts/chart.js +244 -0
- package/dist/accounts/index.d.ts +3 -0
- package/dist/accounts/index.js +3 -0
- package/dist/accounts/standard.d.ts +12 -0
- package/dist/accounts/standard.js +95 -0
- package/dist/accounts/types.d.ts +20 -0
- package/dist/accounts/types.js +40 -0
- package/dist/cli/args.d.ts +45 -0
- package/dist/cli/args.js +147 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/main.d.ts +6 -0
- package/dist/cli/main.js +25 -0
- package/dist/cli/run.d.ts +34 -0
- package/dist/cli/run.js +1660 -0
- package/dist/dashboard/charts.d.ts +38 -0
- package/dist/dashboard/charts.js +173 -0
- package/dist/dashboard/index.d.ts +3 -0
- package/dist/dashboard/index.js +3 -0
- package/dist/dashboard/model.d.ts +137 -0
- package/dist/dashboard/model.js +190 -0
- package/dist/dashboard/render.d.ts +23 -0
- package/dist/dashboard/render.js +231 -0
- package/dist/dashboard/script.d.ts +28 -0
- package/dist/dashboard/script.js +704 -0
- package/dist/dashboard/styles.d.ts +14 -0
- package/dist/dashboard/styles.js +726 -0
- package/dist/demo/disposal.d.ts +80 -0
- package/dist/demo/disposal.js +215 -0
- package/dist/demo/evidence.d.ts +153 -0
- package/dist/demo/evidence.js +348 -0
- package/dist/demo/foreign.d.ts +29 -0
- package/dist/demo/foreign.js +113 -0
- package/dist/demo/foreignReport.d.ts +10 -0
- package/dist/demo/foreignReport.js +89 -0
- package/dist/demo/generator.d.ts +108 -0
- package/dist/demo/generator.js +445 -0
- package/dist/demo/group.d.ts +70 -0
- package/dist/demo/group.js +195 -0
- package/dist/demo/groupPeriods.d.ts +27 -0
- package/dist/demo/groupPeriods.js +125 -0
- package/dist/demo/groupReport.d.ts +13 -0
- package/dist/demo/groupReport.js +87 -0
- package/dist/demo/ingest.d.ts +6 -0
- package/dist/demo/ingest.js +61 -0
- package/dist/demo/main.d.ts +6 -0
- package/dist/demo/main.js +7 -0
- package/dist/demo/mainDisposal.d.ts +1 -0
- package/dist/demo/mainDisposal.js +2 -0
- package/dist/demo/mainEvidence.d.ts +8 -0
- package/dist/demo/mainEvidence.js +9 -0
- package/dist/demo/mainForeign.d.ts +6 -0
- package/dist/demo/mainForeign.js +7 -0
- package/dist/demo/mainGroup.d.ts +6 -0
- package/dist/demo/mainGroup.js +7 -0
- package/dist/demo/mainGroupPeriods.d.ts +6 -0
- package/dist/demo/mainGroupPeriods.js +7 -0
- package/dist/demo/mainIngest.d.ts +6 -0
- package/dist/demo/mainIngest.js +7 -0
- package/dist/demo/mainMidYear.d.ts +6 -0
- package/dist/demo/mainMidYear.js +7 -0
- package/dist/demo/mainReconcile.d.ts +6 -0
- package/dist/demo/mainReconcile.js +7 -0
- package/dist/demo/mainStatements.d.ts +6 -0
- package/dist/demo/mainStatements.js +7 -0
- package/dist/demo/midYear.d.ts +51 -0
- package/dist/demo/midYear.js +162 -0
- package/dist/demo/month.d.ts +13 -0
- package/dist/demo/month.js +142 -0
- package/dist/demo/random.d.ts +48 -0
- package/dist/demo/random.js +94 -0
- package/dist/demo/receivables.d.ts +36 -0
- package/dist/demo/receivables.js +104 -0
- package/dist/demo/reconcile.d.ts +20 -0
- package/dist/demo/reconcile.js +104 -0
- package/dist/demo/report.d.ts +6 -0
- package/dist/demo/report.js +26 -0
- package/dist/demo/statement.d.ts +13 -0
- package/dist/demo/statement.js +51 -0
- package/dist/demo/statements.d.ts +6 -0
- package/dist/demo/statements.js +29 -0
- package/dist/demo/supplierRun.d.ts +46 -0
- package/dist/demo/supplierRun.js +94 -0
- package/dist/demo/unrealisedEvidence.d.ts +51 -0
- package/dist/demo/unrealisedEvidence.js +183 -0
- package/dist/fx/average.d.ts +60 -0
- package/dist/fx/average.js +139 -0
- package/dist/fx/document.d.ts +60 -0
- package/dist/fx/document.js +205 -0
- package/dist/fx/exposure.d.ts +108 -0
- package/dist/fx/exposure.js +262 -0
- package/dist/fx/index.d.ts +8 -0
- package/dist/fx/index.js +8 -0
- package/dist/fx/rate.d.ts +77 -0
- package/dist/fx/rate.js +200 -0
- package/dist/fx/revaluation.d.ts +92 -0
- package/dist/fx/revaluation.js +197 -0
- package/dist/fx/settlement.d.ts +84 -0
- package/dist/fx/settlement.js +162 -0
- package/dist/fx/table.d.ts +120 -0
- package/dist/fx/table.js +271 -0
- package/dist/fx/translate.d.ts +126 -0
- package/dist/fx/translate.js +258 -0
- package/dist/group/accounts.d.ts +74 -0
- package/dist/group/accounts.js +179 -0
- package/dist/group/acquisition.d.ts +324 -0
- package/dist/group/acquisition.js +416 -0
- package/dist/group/aggregate.d.ts +143 -0
- package/dist/group/aggregate.js +288 -0
- package/dist/group/comparative.d.ts +102 -0
- package/dist/group/comparative.js +166 -0
- package/dist/group/consolidate.d.ts +196 -0
- package/dist/group/consolidate.js +635 -0
- package/dist/group/disposal.d.ts +181 -0
- package/dist/group/disposal.js +233 -0
- package/dist/group/document.d.ts +41 -0
- package/dist/group/document.js +326 -0
- package/dist/group/index.d.ts +12 -0
- package/dist/group/index.js +12 -0
- package/dist/group/intercompany.d.ts +136 -0
- package/dist/group/intercompany.js +238 -0
- package/dist/group/interest.d.ts +109 -0
- package/dist/group/interest.js +224 -0
- package/dist/group/movement.d.ts +206 -0
- package/dist/group/movement.js +432 -0
- package/dist/group/structure.d.ts +151 -0
- package/dist/group/structure.js +327 -0
- package/dist/group/timeline.d.ts +109 -0
- package/dist/group/timeline.js +169 -0
- package/dist/group/unrealised.d.ts +165 -0
- package/dist/group/unrealised.js +314 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/ledger/close.d.ts +75 -0
- package/dist/ledger/close.js +142 -0
- package/dist/ledger/date.d.ts +48 -0
- package/dist/ledger/date.js +141 -0
- package/dist/ledger/entry.d.ts +150 -0
- package/dist/ledger/entry.js +264 -0
- package/dist/ledger/index.d.ts +6 -0
- package/dist/ledger/index.js +6 -0
- package/dist/ledger/ledger.d.ts +101 -0
- package/dist/ledger/ledger.js +308 -0
- package/dist/ledger/serialise.d.ts +68 -0
- package/dist/ledger/serialise.js +204 -0
- package/dist/ledger/trialBalance.d.ts +49 -0
- package/dist/ledger/trialBalance.js +103 -0
- package/dist/money/currency.d.ts +37 -0
- package/dist/money/currency.js +67 -0
- package/dist/money/index.d.ts +3 -0
- package/dist/money/index.js +3 -0
- package/dist/money/money.d.ts +106 -0
- package/dist/money/money.js +300 -0
- package/dist/money/rounding.d.ts +41 -0
- package/dist/money/rounding.js +117 -0
- package/dist/reconcile/accuracy.d.ts +65 -0
- package/dist/reconcile/accuracy.js +162 -0
- package/dist/reconcile/assignment.d.ts +83 -0
- package/dist/reconcile/assignment.js +326 -0
- package/dist/reconcile/bankView.d.ts +65 -0
- package/dist/reconcile/bankView.js +86 -0
- package/dist/reconcile/bridge.d.ts +76 -0
- package/dist/reconcile/bridge.js +129 -0
- package/dist/reconcile/candidates.d.ts +84 -0
- package/dist/reconcile/candidates.js +174 -0
- package/dist/reconcile/decisions.d.ts +116 -0
- package/dist/reconcile/decisions.js +168 -0
- package/dist/reconcile/index.d.ts +12 -0
- package/dist/reconcile/index.js +12 -0
- package/dist/reconcile/matcher.d.ts +89 -0
- package/dist/reconcile/matcher.js +306 -0
- package/dist/reconcile/memory.d.ts +120 -0
- package/dist/reconcile/memory.js +281 -0
- package/dist/reconcile/posting.d.ts +185 -0
- package/dist/reconcile/posting.js +481 -0
- package/dist/reconcile/scoring.d.ts +98 -0
- package/dist/reconcile/scoring.js +319 -0
- package/dist/reconcile/similarity.d.ts +88 -0
- package/dist/reconcile/similarity.js +272 -0
- package/dist/reconcile/subsetSum.d.ts +61 -0
- package/dist/reconcile/subsetSum.js +136 -0
- package/dist/reports/ageing.d.ts +75 -0
- package/dist/reports/ageing.js +149 -0
- package/dist/reports/balanceSheet.d.ts +57 -0
- package/dist/reports/balanceSheet.js +107 -0
- package/dist/reports/incomeStatement.d.ts +59 -0
- package/dist/reports/incomeStatement.js +132 -0
- package/dist/reports/index.d.ts +4 -0
- package/dist/reports/index.js +4 -0
- package/dist/reports/period.d.ts +37 -0
- package/dist/reports/period.js +78 -0
- package/dist/statement/amount.d.ts +55 -0
- package/dist/statement/amount.js +211 -0
- package/dist/statement/columns.d.ts +39 -0
- package/dist/statement/columns.js +221 -0
- package/dist/statement/csv.d.ts +50 -0
- package/dist/statement/csv.js +185 -0
- package/dist/statement/dates.d.ts +52 -0
- package/dist/statement/dates.js +204 -0
- package/dist/statement/duplicates.d.ts +51 -0
- package/dist/statement/duplicates.js +101 -0
- package/dist/statement/import.d.ts +52 -0
- package/dist/statement/import.js +172 -0
- package/dist/statement/index.d.ts +20 -0
- package/dist/statement/index.js +32 -0
- package/dist/statement/line.d.ts +60 -0
- package/dist/statement/line.js +73 -0
- package/dist/statement/ofx.d.ts +47 -0
- package/dist/statement/ofx.js +199 -0
- package/package.json +72 -4
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client script, inlined.
|
|
3
|
+
*
|
|
4
|
+
* It does one thing that matters: accepting or rejecting a suggestion changes
|
|
5
|
+
* the reconciliation and the bridge recomputes in front of you. A dashboard
|
|
6
|
+
* that renders the same static figures the CLI already prints is a worse CLI —
|
|
7
|
+
* the reason to have a screen is that a decision can have an effect.
|
|
8
|
+
*
|
|
9
|
+
* The arithmetic runs entirely in integer minor units. JavaScript numbers are
|
|
10
|
+
* exact for integers well past any sum a small company's bank account will
|
|
11
|
+
* reach, and the one thing this page must never do is show a reconciliation
|
|
12
|
+
* that is out by a penny because a float rounded.
|
|
13
|
+
*
|
|
14
|
+
* ## What leaves the page
|
|
15
|
+
*
|
|
16
|
+
* Decisions are exported as the decisions document, which `tallyd learn` reads.
|
|
17
|
+
* The payload for each suggestion was built in TypeScript and baked into
|
|
18
|
+
* `window.__TALLYD__`; this script stamps a verdict and today's date onto it
|
|
19
|
+
* and serialises the result. Nothing here knows the shape of the format beyond
|
|
20
|
+
* those two fields, which is the point — a format definition living in a
|
|
21
|
+
* string of hand-written client JavaScript is a format definition nobody can
|
|
22
|
+
* test.
|
|
23
|
+
*
|
|
24
|
+
* A decision is reversible until it is exported. A queue card is hidden rather
|
|
25
|
+
* than destroyed when it is decided, so undo is a matter of putting it back
|
|
26
|
+
* where it was rather than rebuilding it from data.
|
|
27
|
+
*/
|
|
28
|
+
export const CLIENT_SCRIPT = String.raw `
|
|
29
|
+
(function () {
|
|
30
|
+
"use strict";
|
|
31
|
+
|
|
32
|
+
var data = window.__TALLYD__;
|
|
33
|
+
if (!data) return;
|
|
34
|
+
|
|
35
|
+
var state = {
|
|
36
|
+
matched: data.matched.slice(),
|
|
37
|
+
suggested: data.suggested.slice(),
|
|
38
|
+
unmatchedBook: data.unmatchedBook.slice(),
|
|
39
|
+
unmatchedStatement: data.unmatchedStatement.slice(),
|
|
40
|
+
// Decided suggestions, oldest first. Undo pops from the end.
|
|
41
|
+
decided: []
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Where each suggestion sat in the queue to begin with, so an undo puts it
|
|
45
|
+
// back among its neighbours rather than at the bottom.
|
|
46
|
+
var queueOrder = {};
|
|
47
|
+
for (var q = 0; q < data.suggested.length; q++) queueOrder[data.suggested[q].id] = q;
|
|
48
|
+
|
|
49
|
+
/** Today, in the reviewer's own calendar rather than UTC's. */
|
|
50
|
+
function today() {
|
|
51
|
+
var now = new Date();
|
|
52
|
+
function pad(n) { return (n < 10 ? "0" : "") + n; }
|
|
53
|
+
return now.getFullYear() + "-" + pad(now.getMonth() + 1) + "-" + pad(now.getDate());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ------------------------------------------------------------ formatting
|
|
57
|
+
|
|
58
|
+
function money(minor) {
|
|
59
|
+
var negative = minor < 0;
|
|
60
|
+
var digits = String(Math.abs(minor));
|
|
61
|
+
while (digits.length < data.exponent + 1) digits = "0" + digits;
|
|
62
|
+
var whole = digits.slice(0, digits.length - data.exponent);
|
|
63
|
+
var fraction = data.exponent === 0 ? "" : "." + digits.slice(digits.length - data.exponent);
|
|
64
|
+
whole = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
65
|
+
return (negative ? "-" : "") + data.currencySymbol + whole + fraction;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sumMinor(lines) {
|
|
69
|
+
var total = 0;
|
|
70
|
+
for (var i = 0; i < lines.length; i++) total += lines[i].amount.minor;
|
|
71
|
+
return total;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------- bridge
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Bank closing plus everything in our books the bank has not seen, against
|
|
78
|
+
* ledger closing plus everything on the statement we have not booked. An
|
|
79
|
+
* undecided suggestion counts as outstanding on both sides: until someone
|
|
80
|
+
* confirms it, it is not evidence.
|
|
81
|
+
*/
|
|
82
|
+
function bridge() {
|
|
83
|
+
var outstandingBook = state.unmatchedBook.slice();
|
|
84
|
+
var outstandingStatement = state.unmatchedStatement.slice();
|
|
85
|
+
|
|
86
|
+
for (var i = 0; i < state.suggested.length; i++) {
|
|
87
|
+
outstandingBook = outstandingBook.concat(state.suggested[i].book);
|
|
88
|
+
outstandingStatement = outstandingStatement.concat(state.suggested[i].statement);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
var bookOutstanding = sumMinor(outstandingBook);
|
|
92
|
+
var statementOutstanding = sumMinor(outstandingStatement);
|
|
93
|
+
|
|
94
|
+
var adjustedBank = data.bankClosingMinor + bookOutstanding;
|
|
95
|
+
var adjustedBook = data.bookClosingMinor + statementOutstanding;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
depositsInTransit: outstandingBook.filter(function (line) { return line.amount.minor > 0; }),
|
|
99
|
+
unpresented: outstandingBook.filter(function (line) { return line.amount.minor <= 0; }),
|
|
100
|
+
bankCredits: outstandingStatement.filter(function (line) { return line.amount.minor > 0; }),
|
|
101
|
+
bankDebits: outstandingStatement.filter(function (line) { return line.amount.minor <= 0; }),
|
|
102
|
+
adjustedBank: adjustedBank,
|
|
103
|
+
adjustedBook: adjustedBook,
|
|
104
|
+
difference: adjustedBank - adjustedBook
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ------------------------------------------------------------- rendering
|
|
109
|
+
|
|
110
|
+
function element(tag, className, text) {
|
|
111
|
+
var node = document.createElement(tag);
|
|
112
|
+
if (className) node.className = className;
|
|
113
|
+
if (text !== undefined) node.textContent = text;
|
|
114
|
+
return node;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function scheduleRow(list, label, lines) {
|
|
118
|
+
if (lines.length === 0) return;
|
|
119
|
+
var heading = element("dt", null, label);
|
|
120
|
+
var total = element("dd", null, money(sumMinor(lines)));
|
|
121
|
+
list.appendChild(heading);
|
|
122
|
+
list.appendChild(total);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function renderBridge() {
|
|
126
|
+
var result = bridge();
|
|
127
|
+
var list = document.getElementById("bridge-list");
|
|
128
|
+
list.textContent = "";
|
|
129
|
+
|
|
130
|
+
function row(label, value, lead) {
|
|
131
|
+
var dt = element("dt", lead ? "lead-row" : null, label);
|
|
132
|
+
var dd = element("dd", lead ? "lead-row" : null, money(value));
|
|
133
|
+
list.appendChild(dt);
|
|
134
|
+
list.appendChild(dd);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function divider() {
|
|
138
|
+
list.appendChild(element("div", "rule-row"));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
row("Balance per bank statement", data.bankClosingMinor);
|
|
142
|
+
scheduleRow(list, "Add: receipts not yet on the statement", result.depositsInTransit);
|
|
143
|
+
scheduleRow(list, "Less: payments not yet on the statement", result.unpresented);
|
|
144
|
+
divider();
|
|
145
|
+
row("Adjusted bank balance", result.adjustedBank, true);
|
|
146
|
+
|
|
147
|
+
divider();
|
|
148
|
+
|
|
149
|
+
row("Balance per the ledger", data.bookClosingMinor);
|
|
150
|
+
scheduleRow(list, "Add: bank credits not yet booked", result.bankCredits);
|
|
151
|
+
scheduleRow(list, "Less: bank debits not yet booked", result.bankDebits);
|
|
152
|
+
divider();
|
|
153
|
+
row("Adjusted ledger balance", result.adjustedBook, true);
|
|
154
|
+
|
|
155
|
+
var verdict = document.getElementById("bridge-verdict");
|
|
156
|
+
var balanced = result.difference === 0;
|
|
157
|
+
verdict.setAttribute("data-state", balanced ? "balanced" : "off");
|
|
158
|
+
verdict.querySelector(".label").textContent = balanced
|
|
159
|
+
? "Reconciled"
|
|
160
|
+
: "The two sides do not agree";
|
|
161
|
+
verdict.querySelector(".figure").textContent = money(result.difference);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function renderCounts() {
|
|
165
|
+
document.getElementById("count-matched").textContent = String(state.matched.length);
|
|
166
|
+
document.getElementById("count-review").textContent = String(state.suggested.length);
|
|
167
|
+
document.getElementById("count-book").textContent = String(state.unmatchedBook.length);
|
|
168
|
+
document.getElementById("count-bank").textContent = String(state.unmatchedStatement.length);
|
|
169
|
+
|
|
170
|
+
var queueTally = document.getElementById("queue-tally");
|
|
171
|
+
if (queueTally) {
|
|
172
|
+
queueTally.textContent =
|
|
173
|
+
state.suggested.length === 0
|
|
174
|
+
? "clear"
|
|
175
|
+
: state.suggested.length + (state.suggested.length === 1 ? " item" : " items");
|
|
176
|
+
}
|
|
177
|
+
var matchedTally = document.getElementById("matched-tally");
|
|
178
|
+
if (matchedTally) matchedTally.textContent = String(state.matched.length);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function renderLeftovers() {
|
|
182
|
+
renderLineTable("leftover-book", state.unmatchedBook, "Everything in the books is on the statement.");
|
|
183
|
+
renderLineTable(
|
|
184
|
+
"leftover-bank",
|
|
185
|
+
state.unmatchedStatement,
|
|
186
|
+
"Every statement line is accounted for."
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function renderLineTable(id, lines, emptyMessage) {
|
|
191
|
+
var host = document.getElementById(id);
|
|
192
|
+
if (!host) return;
|
|
193
|
+
host.textContent = "";
|
|
194
|
+
|
|
195
|
+
if (lines.length === 0) {
|
|
196
|
+
var empty = element("div", "empty");
|
|
197
|
+
empty.appendChild(element("strong", null, "Nothing left"));
|
|
198
|
+
empty.appendChild(document.createTextNode(emptyMessage));
|
|
199
|
+
host.appendChild(empty);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
var table = document.createElement("table");
|
|
204
|
+
var head = document.createElement("thead");
|
|
205
|
+
var headRow = document.createElement("tr");
|
|
206
|
+
["Date", "Description", "Amount"].forEach(function (label, index) {
|
|
207
|
+
var cell = element("th", index === 2 ? "num" : null, label);
|
|
208
|
+
headRow.appendChild(cell);
|
|
209
|
+
});
|
|
210
|
+
head.appendChild(headRow);
|
|
211
|
+
table.appendChild(head);
|
|
212
|
+
|
|
213
|
+
var body = document.createElement("tbody");
|
|
214
|
+
lines.forEach(function (line) {
|
|
215
|
+
var row = document.createElement("tr");
|
|
216
|
+
row.appendChild(element("td", "when", line.date));
|
|
217
|
+
row.appendChild(element("td", null, line.description));
|
|
218
|
+
var amount = element("td", "num" + (line.amount.minor < 0 ? " negative" : ""), line.amount.text);
|
|
219
|
+
row.appendChild(amount);
|
|
220
|
+
body.appendChild(row);
|
|
221
|
+
});
|
|
222
|
+
table.appendChild(body);
|
|
223
|
+
host.appendChild(table);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function announce(message) {
|
|
227
|
+
var live = document.getElementById("live-region");
|
|
228
|
+
if (live) live.textContent = message;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// --------------------------------------------------------------- actions
|
|
232
|
+
|
|
233
|
+
function findSuggestion(id) {
|
|
234
|
+
for (var i = 0; i < state.suggested.length; i++) {
|
|
235
|
+
if (state.suggested[i].id === id) return { match: state.suggested[i], index: i };
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function decide(id, verdict) {
|
|
241
|
+
var found = findSuggestion(id);
|
|
242
|
+
if (!found) return;
|
|
243
|
+
|
|
244
|
+
state.suggested.splice(found.index, 1);
|
|
245
|
+
state.decided.push({ id: id, verdict: verdict, match: found.match });
|
|
246
|
+
|
|
247
|
+
if (verdict === "accepted") {
|
|
248
|
+
state.matched = state.matched.concat([found.match]);
|
|
249
|
+
announce("Match accepted: " + found.match.amount.text + ". " + state.suggested.length + " left to review.");
|
|
250
|
+
} else {
|
|
251
|
+
state.unmatchedBook = state.unmatchedBook.concat(found.match.book);
|
|
252
|
+
state.unmatchedStatement = state.unmatchedStatement.concat(found.match.statement);
|
|
253
|
+
state.unmatchedBook.sort(byDate);
|
|
254
|
+
state.unmatchedStatement.sort(byDate);
|
|
255
|
+
announce("Match rejected. Both lines returned to the leftovers.");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
var card = queueCard(id);
|
|
259
|
+
if (card) {
|
|
260
|
+
card.setAttribute("data-leaving", "true");
|
|
261
|
+
window.setTimeout(function () {
|
|
262
|
+
if (card.getAttribute("data-leaving") === "true") card.hidden = true;
|
|
263
|
+
renderQueueEmptyState();
|
|
264
|
+
}, 200);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
refresh();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Put a decision back, card and all, as if it had never been made. */
|
|
271
|
+
function undo(id) {
|
|
272
|
+
var index = -1;
|
|
273
|
+
for (var i = 0; i < state.decided.length; i++) {
|
|
274
|
+
if (state.decided[i].id === id) { index = i; break; }
|
|
275
|
+
}
|
|
276
|
+
if (index === -1) return;
|
|
277
|
+
|
|
278
|
+
var entry = state.decided.splice(index, 1)[0];
|
|
279
|
+
var match = entry.match;
|
|
280
|
+
|
|
281
|
+
if (entry.verdict === "accepted") {
|
|
282
|
+
state.matched = state.matched.filter(function (m) { return m.id !== id; });
|
|
283
|
+
} else {
|
|
284
|
+
state.unmatchedBook = withoutLines(state.unmatchedBook, match.book);
|
|
285
|
+
state.unmatchedStatement = withoutLines(state.unmatchedStatement, match.statement);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Back among its neighbours: before the first suggestion that started
|
|
289
|
+
// life after it did.
|
|
290
|
+
var at = state.suggested.length;
|
|
291
|
+
for (var j = 0; j < state.suggested.length; j++) {
|
|
292
|
+
if (queueOrder[state.suggested[j].id] > queueOrder[id]) { at = j; break; }
|
|
293
|
+
}
|
|
294
|
+
state.suggested.splice(at, 0, match);
|
|
295
|
+
|
|
296
|
+
var card = queueCard(id);
|
|
297
|
+
if (card) {
|
|
298
|
+
card.hidden = false;
|
|
299
|
+
card.removeAttribute("data-leaving");
|
|
300
|
+
}
|
|
301
|
+
var placeholder = document.querySelector("#queue .empty");
|
|
302
|
+
if (placeholder && placeholder.parentNode) placeholder.parentNode.removeChild(placeholder);
|
|
303
|
+
|
|
304
|
+
announce("Decision undone. " + state.suggested.length + " to review.");
|
|
305
|
+
refresh();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function withoutLines(lines, remove) {
|
|
309
|
+
var ids = {};
|
|
310
|
+
for (var i = 0; i < remove.length; i++) ids[remove[i].id] = true;
|
|
311
|
+
return lines.filter(function (line) { return !ids[line.id]; });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function queueCard(id) {
|
|
315
|
+
return document.querySelector('#queue [data-match-id="' + id + '"]');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ------------------------------------------------------------- decisions
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* The decisions document, in the format "tallyd learn" reads.
|
|
322
|
+
*
|
|
323
|
+
* Every payload the server computed for a decided suggestion, with the
|
|
324
|
+
* verdict and the date stamped on. A group match contributes several
|
|
325
|
+
* records from one click; that expansion happened in TypeScript.
|
|
326
|
+
*/
|
|
327
|
+
function decisionsDocument() {
|
|
328
|
+
var on = today();
|
|
329
|
+
var records = [];
|
|
330
|
+
for (var i = 0; i < state.decided.length; i++) {
|
|
331
|
+
var entry = state.decided[i];
|
|
332
|
+
var payloads = entry.match.decision || [];
|
|
333
|
+
for (var j = 0; j < payloads.length; j++) {
|
|
334
|
+
records.push({
|
|
335
|
+
statement: payloads[j].statement,
|
|
336
|
+
book: payloads[j].book,
|
|
337
|
+
accepted: entry.verdict === "accepted",
|
|
338
|
+
on: on,
|
|
339
|
+
context: payloads[j].context
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return records;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function decisionsJson() {
|
|
347
|
+
return JSON.stringify(decisionsDocument(), null, 2) + "\n";
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function exportDecisions() {
|
|
351
|
+
var text = decisionsJson();
|
|
352
|
+
var blob = new Blob([text], { type: "application/json" });
|
|
353
|
+
var url = URL.createObjectURL(blob);
|
|
354
|
+
var link = document.createElement("a");
|
|
355
|
+
link.href = url;
|
|
356
|
+
link.download = "decisions-" + data.account + "-" + today() + ".json";
|
|
357
|
+
document.body.appendChild(link);
|
|
358
|
+
link.click();
|
|
359
|
+
document.body.removeChild(link);
|
|
360
|
+
window.setTimeout(function () { URL.revokeObjectURL(url); }, 0);
|
|
361
|
+
announce("Decisions file written.");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Copy, for the browser that will not download from a file:// document.
|
|
366
|
+
* The clipboard API needs a secure context and this page is often opened
|
|
367
|
+
* off a disk, so the textarea route is the one that actually runs.
|
|
368
|
+
*/
|
|
369
|
+
function copyDecisions(button) {
|
|
370
|
+
var text = decisionsJson();
|
|
371
|
+
var done = function (ok) {
|
|
372
|
+
var label = button.textContent;
|
|
373
|
+
button.textContent = ok ? "Copied" : "Press ⌘C";
|
|
374
|
+
window.setTimeout(function () { button.textContent = label; }, 1400);
|
|
375
|
+
announce(ok ? "Decisions copied to the clipboard." : "Select the text and copy it.");
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
379
|
+
navigator.clipboard.writeText(text).then(function () { done(true); }, function () { legacyCopy(text, done); });
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
legacyCopy(text, done);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function legacyCopy(text, done) {
|
|
386
|
+
var area = document.createElement("textarea");
|
|
387
|
+
area.value = text;
|
|
388
|
+
area.setAttribute("readonly", "readonly");
|
|
389
|
+
area.style.position = "fixed";
|
|
390
|
+
area.style.top = "-1000px";
|
|
391
|
+
document.body.appendChild(area);
|
|
392
|
+
area.select();
|
|
393
|
+
var ok = false;
|
|
394
|
+
try { ok = document.execCommand("copy"); } catch (error) { ok = false; }
|
|
395
|
+
document.body.removeChild(area);
|
|
396
|
+
done(ok);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function renderDecided() {
|
|
400
|
+
var section = document.getElementById("decided-section");
|
|
401
|
+
var host = document.getElementById("decided-list");
|
|
402
|
+
var bar = document.getElementById("decision-bar");
|
|
403
|
+
var tally = document.getElementById("decision-tally");
|
|
404
|
+
if (!section || !host || !bar || !tally) return;
|
|
405
|
+
|
|
406
|
+
var count = state.decided.length;
|
|
407
|
+
section.hidden = count === 0;
|
|
408
|
+
bar.setAttribute("data-open", count === 0 ? "false" : "true");
|
|
409
|
+
|
|
410
|
+
var facts = decisionsDocument().length;
|
|
411
|
+
tally.innerHTML = "";
|
|
412
|
+
tally.appendChild(element("strong", null, String(count)));
|
|
413
|
+
tally.appendChild(
|
|
414
|
+
document.createTextNode(
|
|
415
|
+
(count === 1 ? " decision" : " decisions") +
|
|
416
|
+
", " + facts + (facts === 1 ? " counterparty fact" : " counterparty facts") +
|
|
417
|
+
" to export"
|
|
418
|
+
)
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
host.textContent = "";
|
|
422
|
+
// Newest first: the thing most likely to be undone is the thing just done.
|
|
423
|
+
for (var i = state.decided.length - 1; i >= 0; i--) {
|
|
424
|
+
var entry = state.decided[i];
|
|
425
|
+
var row = element("div", "decided-row");
|
|
426
|
+
|
|
427
|
+
var verdict = element("span", "verdict", entry.verdict);
|
|
428
|
+
verdict.setAttribute("data-verdict", entry.verdict);
|
|
429
|
+
row.appendChild(verdict);
|
|
430
|
+
|
|
431
|
+
var bank = entry.match.statement[0] || entry.match.book[0];
|
|
432
|
+
row.appendChild(element("span", "when", bank ? bank.date : ""));
|
|
433
|
+
row.appendChild(element("span", "what", bank ? bank.description : "—"));
|
|
434
|
+
row.appendChild(
|
|
435
|
+
element(
|
|
436
|
+
"span",
|
|
437
|
+
"how-much" + (entry.match.amount.minor < 0 ? " negative" : ""),
|
|
438
|
+
entry.match.amount.text
|
|
439
|
+
)
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
var button = element("button", null, "Undo");
|
|
443
|
+
button.setAttribute("type", "button");
|
|
444
|
+
button.setAttribute("data-action", "undo");
|
|
445
|
+
button.setAttribute("data-match-id", entry.id);
|
|
446
|
+
row.appendChild(button);
|
|
447
|
+
|
|
448
|
+
host.appendChild(row);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
var undoLast = document.querySelector('[data-action="undo-last"]');
|
|
452
|
+
if (undoLast) undoLast.disabled = count === 0;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// --------------------------------------------------------------- implied
|
|
456
|
+
|
|
457
|
+
var impliedByLine = {};
|
|
458
|
+
for (var e = 0; e < (data.implied || []).length; e++) {
|
|
459
|
+
impliedByLine[data.implied[e].lineId] = data.implied[e];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* The entries the current state of the review implies.
|
|
464
|
+
*
|
|
465
|
+
* Every statement line has a proposal precomputed; which ones are live
|
|
466
|
+
* follows from the leftovers, so rejecting a suggestion adds a row here in
|
|
467
|
+
* the same gesture that pushes its lines back into the leftovers.
|
|
468
|
+
*/
|
|
469
|
+
function renderImplied() {
|
|
470
|
+
var section = document.getElementById("implied-section");
|
|
471
|
+
var host = document.getElementById("implied");
|
|
472
|
+
if (!section || !host) return;
|
|
473
|
+
|
|
474
|
+
var rows = [];
|
|
475
|
+
for (var i = 0; i < state.unmatchedStatement.length; i++) {
|
|
476
|
+
var proposal = impliedByLine[state.unmatchedStatement[i].id];
|
|
477
|
+
if (proposal) rows.push(proposal);
|
|
478
|
+
}
|
|
479
|
+
rows.sort(function (a, b) { return a.date < b.date ? -1 : a.date > b.date ? 1 : 0; });
|
|
480
|
+
|
|
481
|
+
host.textContent = "";
|
|
482
|
+
|
|
483
|
+
if (rows.length === 0) {
|
|
484
|
+
var empty = element("div", "empty");
|
|
485
|
+
empty.appendChild(element("strong", null, "Nothing to book"));
|
|
486
|
+
empty.appendChild(
|
|
487
|
+
document.createTextNode("Every statement line has a counterpart in the books.")
|
|
488
|
+
);
|
|
489
|
+
host.appendChild(empty);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
var booked = 0;
|
|
494
|
+
var total = 0;
|
|
495
|
+
|
|
496
|
+
for (var j = 0; j < rows.length; j++) {
|
|
497
|
+
var row = rows[j];
|
|
498
|
+
var node = element("div", "implied-row");
|
|
499
|
+
node.setAttribute("data-outcome", row.outcome);
|
|
500
|
+
|
|
501
|
+
node.appendChild(element("span", "when", row.date));
|
|
502
|
+
node.appendChild(element("span", "what", row.description));
|
|
503
|
+
node.appendChild(
|
|
504
|
+
element("span", "how-much" + (row.amount.minor < 0 ? " negative" : ""), row.amount.text)
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
var lands = element("span", "lands");
|
|
508
|
+
if (row.outcome === "book" && row.account) {
|
|
509
|
+
var code = element("code", null, row.account);
|
|
510
|
+
lands.appendChild(code);
|
|
511
|
+
lands.appendChild(document.createTextNode(" " + (row.accountName || "")));
|
|
512
|
+
booked += 1;
|
|
513
|
+
total += row.amount.minor;
|
|
514
|
+
} else if (row.outcome === "skip") {
|
|
515
|
+
lands.textContent = "not a transaction";
|
|
516
|
+
} else if (row.outcome === "already-booked") {
|
|
517
|
+
lands.textContent = "already booked";
|
|
518
|
+
} else {
|
|
519
|
+
lands.textContent = "no rule";
|
|
520
|
+
}
|
|
521
|
+
lands.title = row.reason;
|
|
522
|
+
node.appendChild(lands);
|
|
523
|
+
|
|
524
|
+
host.appendChild(node);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
var summary = element("div", "implied-total");
|
|
528
|
+
summary.appendChild(
|
|
529
|
+
element(
|
|
530
|
+
"span",
|
|
531
|
+
null,
|
|
532
|
+
booked + (booked === 1 ? " entry to book" : " entries to book") +
|
|
533
|
+
", " + (rows.length - booked) + " left for a person"
|
|
534
|
+
)
|
|
535
|
+
);
|
|
536
|
+
summary.appendChild(element("span", "figure", money(total)));
|
|
537
|
+
host.appendChild(summary);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function refresh() {
|
|
541
|
+
renderCounts();
|
|
542
|
+
renderBridge();
|
|
543
|
+
renderLeftovers();
|
|
544
|
+
renderDecided();
|
|
545
|
+
renderImplied();
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function byDate(a, b) {
|
|
549
|
+
if (a.date === b.date) return a.id < b.id ? -1 : 1;
|
|
550
|
+
return a.date < b.date ? -1 : 1;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function renderQueueEmptyState() {
|
|
554
|
+
var queue = document.getElementById("queue");
|
|
555
|
+
if (!queue) return;
|
|
556
|
+
var existing = queue.querySelector(".empty");
|
|
557
|
+
if (state.suggested.length === 0 && !existing) {
|
|
558
|
+
var empty = element("div", "empty");
|
|
559
|
+
empty.appendChild(element("strong", null, "Queue clear"));
|
|
560
|
+
empty.appendChild(
|
|
561
|
+
document.createTextNode("Every suggestion has been decided. The bridge above is final.")
|
|
562
|
+
);
|
|
563
|
+
queue.appendChild(empty);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
document.addEventListener("click", function (event) {
|
|
568
|
+
var button = event.target.closest ? event.target.closest("[data-action]") : null;
|
|
569
|
+
if (!button) return;
|
|
570
|
+
var action = button.getAttribute("data-action");
|
|
571
|
+
var id = button.getAttribute("data-match-id");
|
|
572
|
+
if (action === "accept") decide(id, "accepted");
|
|
573
|
+
if (action === "reject") decide(id, "rejected");
|
|
574
|
+
if (action === "undo") undo(id);
|
|
575
|
+
if (action === "undo-last" && state.decided.length > 0) {
|
|
576
|
+
undo(state.decided[state.decided.length - 1].id);
|
|
577
|
+
}
|
|
578
|
+
if (action === "export-decisions") exportDecisions();
|
|
579
|
+
if (action === "copy-decisions") copyDecisions(button);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// ------------------------------------------------------- ledger explorer
|
|
583
|
+
|
|
584
|
+
var selectedAccount = null;
|
|
585
|
+
|
|
586
|
+
function renderPostings() {
|
|
587
|
+
var host = document.getElementById("postings");
|
|
588
|
+
if (!host) return;
|
|
589
|
+
host.textContent = "";
|
|
590
|
+
|
|
591
|
+
if (!selectedAccount) {
|
|
592
|
+
var hint = element("div", "empty");
|
|
593
|
+
hint.appendChild(element("strong", null, "Pick an account"));
|
|
594
|
+
hint.appendChild(document.createTextNode("Its postings appear here, oldest first."));
|
|
595
|
+
host.appendChild(hint);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
var rows = data.postings.filter(function (posting) {
|
|
600
|
+
return posting.account === selectedAccount;
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
if (rows.length === 0) {
|
|
604
|
+
var empty = element("div", "empty");
|
|
605
|
+
empty.appendChild(element("strong", null, "No postings"));
|
|
606
|
+
empty.appendChild(document.createTextNode("This account groups others but is never posted to."));
|
|
607
|
+
host.appendChild(empty);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
var table = document.createElement("table");
|
|
612
|
+
var head = document.createElement("thead");
|
|
613
|
+
var headRow = document.createElement("tr");
|
|
614
|
+
["Date", "Entry", "Narration", "Contra", "Amount"].forEach(function (label, index) {
|
|
615
|
+
headRow.appendChild(element("th", index === 4 ? "num" : null, label));
|
|
616
|
+
});
|
|
617
|
+
head.appendChild(headRow);
|
|
618
|
+
table.appendChild(head);
|
|
619
|
+
|
|
620
|
+
var body = document.createElement("tbody");
|
|
621
|
+
var running = 0;
|
|
622
|
+
rows.forEach(function (posting) {
|
|
623
|
+
running += posting.amount.minor;
|
|
624
|
+
var row = document.createElement("tr");
|
|
625
|
+
row.appendChild(element("td", "when", posting.date));
|
|
626
|
+
row.appendChild(element("td", "when", posting.entryId));
|
|
627
|
+
row.appendChild(element("td", null, posting.narration));
|
|
628
|
+
row.appendChild(element("td", "when", posting.contra.join(" ")));
|
|
629
|
+
row.appendChild(
|
|
630
|
+
element("td", "num" + (posting.amount.minor < 0 ? " negative" : ""), posting.amount.text)
|
|
631
|
+
);
|
|
632
|
+
body.appendChild(row);
|
|
633
|
+
});
|
|
634
|
+
table.appendChild(body);
|
|
635
|
+
host.appendChild(table);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
document.addEventListener("click", function (event) {
|
|
639
|
+
var row = event.target.closest ? event.target.closest("[data-account]") : null;
|
|
640
|
+
if (!row) return;
|
|
641
|
+
selectAccount(row.getAttribute("data-account"));
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
document.addEventListener("keydown", function (event) {
|
|
645
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
646
|
+
var row = event.target.closest ? event.target.closest("[data-account]") : null;
|
|
647
|
+
if (!row) return;
|
|
648
|
+
event.preventDefault();
|
|
649
|
+
selectAccount(row.getAttribute("data-account"));
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
function selectAccount(code) {
|
|
653
|
+
selectedAccount = code;
|
|
654
|
+
var rows = document.querySelectorAll("[data-account]");
|
|
655
|
+
for (var i = 0; i < rows.length; i++) {
|
|
656
|
+
rows[i].setAttribute("aria-selected", rows[i].getAttribute("data-account") === code ? "true" : "false");
|
|
657
|
+
}
|
|
658
|
+
renderPostings();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ---------------------------------------------------------- chart hover
|
|
662
|
+
|
|
663
|
+
var cash = document.querySelector('[data-chart="cash"]');
|
|
664
|
+
if (cash) {
|
|
665
|
+
var tooltip = cash.querySelector(".tooltip");
|
|
666
|
+
var crosshair = cash.querySelector(".crosshair");
|
|
667
|
+
var svg = cash.querySelector("svg");
|
|
668
|
+
|
|
669
|
+
cash.addEventListener("mousemove", function (event) {
|
|
670
|
+
var hit = event.target.closest ? event.target.closest(".hit") : null;
|
|
671
|
+
if (!hit) return;
|
|
672
|
+
var box = svg.getBoundingClientRect();
|
|
673
|
+
var viewBox = svg.viewBox.baseVal;
|
|
674
|
+
var scale = box.width / viewBox.width;
|
|
675
|
+
|
|
676
|
+
var pointX = parseFloat(hit.getAttribute("data-x"));
|
|
677
|
+
var pointY = parseFloat(hit.getAttribute("data-y"));
|
|
678
|
+
|
|
679
|
+
tooltip.textContent = hit.getAttribute("data-label");
|
|
680
|
+
tooltip.style.left = pointX * scale + "px";
|
|
681
|
+
tooltip.style.top = pointY * scale + "px";
|
|
682
|
+
tooltip.setAttribute("data-visible", "true");
|
|
683
|
+
|
|
684
|
+
crosshair.setAttribute("x1", String(pointX));
|
|
685
|
+
crosshair.setAttribute("x2", String(pointX));
|
|
686
|
+
crosshair.style.display = "";
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
cash.addEventListener("mouseleave", function () {
|
|
690
|
+
tooltip.setAttribute("data-visible", "false");
|
|
691
|
+
crosshair.style.display = "none";
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// ------------------------------------------------------------------ boot
|
|
696
|
+
|
|
697
|
+
refresh();
|
|
698
|
+
renderPostings();
|
|
699
|
+
renderQueueEmptyState();
|
|
700
|
+
|
|
701
|
+
// A test harness, and the only supported way in: the page owns its state.
|
|
702
|
+
window.__TALLYD_DECISIONS__ = decisionsDocument;
|
|
703
|
+
})();
|
|
704
|
+
`;
|