@managemint-solutions/sdk 0.56.0 → 0.65.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/README.md +209 -7
- package/dist/audit/document-changes.d.ts +39 -0
- package/dist/audit/document-changes.js +332 -0
- package/dist/audit/index.d.ts +50 -2
- package/dist/audit/index.js +87 -44
- package/dist/client.d.ts +6 -0
- package/dist/client.js +9 -0
- package/dist/clients/dto.d.ts +18 -3
- package/dist/clients/dto.js +72 -2
- package/dist/clients/index.js +16 -0
- package/dist/configs/dto.js +4 -12
- package/dist/documents/content.d.ts +39 -0
- package/dist/documents/content.js +443 -0
- package/dist/documents/document-templates.d.ts +37 -0
- package/dist/documents/document-templates.js +143 -0
- package/dist/documents/documents.d.ts +154 -0
- package/dist/documents/documents.js +330 -0
- package/dist/documents/dto.d.ts +138 -0
- package/dist/documents/dto.js +594 -0
- package/dist/documents/index.d.ts +4 -0
- package/dist/documents/index.js +24 -0
- package/dist/documents/render/html.d.ts +64 -0
- package/dist/documents/render/html.js +413 -0
- package/dist/documents/render/index.d.ts +2 -0
- package/dist/documents/render/index.js +23 -0
- package/dist/documents/render/values.d.ts +86 -0
- package/dist/documents/render/values.js +142 -0
- package/dist/feature-requests/dto.js +4 -13
- package/dist/index.d.ts +8 -1
- package/dist/index.js +31 -3
- package/dist/leads/index.js +1 -3
- package/dist/notifications/index.d.ts +6 -0
- package/dist/notifications/index.js +16 -0
- package/dist/organization-payment-methods/dto.d.ts +10 -0
- package/dist/organization-payment-methods/dto.js +56 -0
- package/dist/organization-payment-methods/index.d.ts +8 -1
- package/dist/organization-payment-methods/index.js +23 -0
- package/dist/payments/index.d.ts +67 -0
- package/dist/payments/index.js +137 -0
- package/dist/permissions/dto.d.ts +4 -0
- package/dist/permissions/dto.js +20 -8
- package/dist/service-client/index.d.ts +44 -0
- package/dist/service-client/index.js +77 -0
- package/dist/staff-client.d.ts +3 -0
- package/dist/staff-client.js +5 -0
- package/dist/supporting-files/index.js +4 -6
- package/dist/supporting-files/paths.d.ts +2 -0
- package/dist/supporting-files/paths.js +3 -1
- package/dist/validators.d.ts +12 -0
- package/dist/validators.js +23 -1
- package/dist/visibility/dto.d.ts +15 -0
- package/dist/visibility/dto.js +68 -0
- package/dist/visibility/index.d.ts +59 -0
- package/dist/visibility/index.js +165 -0
- package/package.json +6 -2
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { DocumentBlock, DocumentVariable, DocumentVariableValues } from '@managemint-solutions/entities/documents';
|
|
2
|
+
import { DocumentStatus } from '@managemint-solutions/entities/documents/enum';
|
|
3
|
+
/** What is printed: a document, or a template (saved, or unsaved in the builder). */
|
|
4
|
+
export type PrintableDocument = {
|
|
5
|
+
title: string;
|
|
6
|
+
number: string | null;
|
|
7
|
+
/** Null for a template. */
|
|
8
|
+
status: DocumentStatus | null;
|
|
9
|
+
/** The document's currency, or the organization's when it has none. */
|
|
10
|
+
currency: string;
|
|
11
|
+
variables: DocumentVariable[];
|
|
12
|
+
/** Every value to print, the system values included. */
|
|
13
|
+
values: DocumentVariableValues;
|
|
14
|
+
blocks: DocumentBlock[];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Plain text with each `{{key}}` replaced by its value as the document prints it - money in its
|
|
18
|
+
* currency, dates written out - and a key with no value left empty: a stored email's subject and
|
|
19
|
+
* message, written before the document had a number, filled when it is sent. Text, not HTML, so
|
|
20
|
+
* nothing is escaped; the email template shows it as text.
|
|
21
|
+
*/
|
|
22
|
+
export declare const fillText: (text: string, document: Pick<PrintableDocument, "variables" | "values" | "currency" | "status">) => string;
|
|
23
|
+
/** A running header or footer: its left and right text as HTML, escaped, page counters as spans. */
|
|
24
|
+
export type DocumentBand = {
|
|
25
|
+
left: string;
|
|
26
|
+
right: string;
|
|
27
|
+
};
|
|
28
|
+
/** A document rendered: the page body, and the bands that repeat on every page. */
|
|
29
|
+
export type RenderedDocument = {
|
|
30
|
+
/** Self-contained HTML: the first page's letterhead, then the blocks. Header and footer blocks are not in it. */
|
|
31
|
+
html: string;
|
|
32
|
+
/** The header block's text, or null without one (no header at all). */
|
|
33
|
+
header: DocumentBand | null;
|
|
34
|
+
/** The footer block's text - or, with none, the organization, title and number, and "Page X of Y". */
|
|
35
|
+
footer: DocumentBand;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The page geometry, in inches: the side and bottom margins, and the top margin - grown to make
|
|
39
|
+
* room when the document has a header. The api's PDF and the portal's paged preview use the same.
|
|
40
|
+
*/
|
|
41
|
+
export declare const DOCUMENT_PAGE_MARGINS: {
|
|
42
|
+
readonly top: 0.62;
|
|
43
|
+
readonly topWithHeader: 0.95;
|
|
44
|
+
readonly bottom: 0.7;
|
|
45
|
+
readonly side: 0.7;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* One band as a styled block: left text left, right text right, in the footer's small grey type.
|
|
49
|
+
* The styles are inline (a placeholder's included) because Chromium's header and footer templates
|
|
50
|
+
* ignore the document's CSS; the side margins mirror the page's. The api hands it to Chromium as
|
|
51
|
+
* the header or footer template; the portal's preview places it on each page.
|
|
52
|
+
*/
|
|
53
|
+
export declare const renderBand: (band: DocumentBand, edge: "top" | "bottom") => string;
|
|
54
|
+
/**
|
|
55
|
+
* A document's blocks and values as HTML, plus its running header and footer - the only way a
|
|
56
|
+
* document or template is shown, so what is seen is what prints. Styled as the subscription
|
|
57
|
+
* invoice is: the first page's letterhead - the organization top left against the title, number,
|
|
58
|
+
* issue date and status top right - then the blocks. Header and footer blocks print nothing in the
|
|
59
|
+
* body: their text is `header` / `footer`, with `{{key}}`s printed as in the body and
|
|
60
|
+
* `{{page_number}}` / `{{page_count}}` as page-counter spans. `imageUrls` maps the image blocks'
|
|
61
|
+
* paths to URLs the caller signed; a path without one prints as `[Image]` on a draft or template
|
|
62
|
+
* and as nothing once issued.
|
|
63
|
+
*/
|
|
64
|
+
export declare const renderDocument: (document: PrintableDocument, imageUrls: Record<string, string>) => RenderedDocument;
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderDocument = exports.renderBand = exports.DOCUMENT_PAGE_MARGINS = exports.fillText = void 0;
|
|
4
|
+
const enum_1 = require("@managemint-solutions/entities/documents/enum");
|
|
5
|
+
const values_1 = require("./values");
|
|
6
|
+
const STATUS_LABELS = {
|
|
7
|
+
[enum_1.DocumentStatus.DRAFT]: 'Draft',
|
|
8
|
+
[enum_1.DocumentStatus.ISSUED]: 'Issued',
|
|
9
|
+
[enum_1.DocumentStatus.VOID]: 'Void',
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The page counters, which only the header and footer may use. Chromium fills these spans in its
|
|
13
|
+
* header and footer templates; the portal's preview fills them page by page.
|
|
14
|
+
*/
|
|
15
|
+
const PAGE_COUNTERS = {
|
|
16
|
+
[enum_1.DocumentSystemVariable.PAGE_NUMBER]: '<span class="pageNumber"></span>',
|
|
17
|
+
[enum_1.DocumentSystemVariable.PAGE_COUNT]: '<span class="totalPages"></span>',
|
|
18
|
+
};
|
|
19
|
+
const MARK_TAGS = { bold: 'strong', italic: 'em', underline: 'u' };
|
|
20
|
+
/** A variable's value as text, formatted for its type; null when it has none. */
|
|
21
|
+
const valueText = (key, ctx) => {
|
|
22
|
+
const value = ctx.values[key];
|
|
23
|
+
if (value === undefined || value === null || value === '')
|
|
24
|
+
return null;
|
|
25
|
+
switch (ctx.variables.get(key)?.type) {
|
|
26
|
+
case enum_1.DocumentVariableType.MONEY:
|
|
27
|
+
return typeof value === 'number' ? (0, values_1.money)(value, ctx.currency) : String(value);
|
|
28
|
+
case enum_1.DocumentVariableType.DATE:
|
|
29
|
+
return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value) ? (0, values_1.formatDocumentDate)(value) : String(value);
|
|
30
|
+
default:
|
|
31
|
+
return String(value);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
/** "Reg. no. 2020/123456/07", or null when there is no value to label. */
|
|
35
|
+
const prefixed = (label, value) => (value ? `${label} ${value}` : null);
|
|
36
|
+
/** The muted "[Label]" a draft or template shows where something will print. */
|
|
37
|
+
const placeholder = (label) => `<span class="placeholder">[${(0, values_1.escapeHtml)(label)}]</span>`;
|
|
38
|
+
const valueHtml = (key, ctx) => {
|
|
39
|
+
if (PAGE_COUNTERS[key])
|
|
40
|
+
return PAGE_COUNTERS[key];
|
|
41
|
+
const text = valueText(key, ctx);
|
|
42
|
+
if (text !== null)
|
|
43
|
+
return (0, values_1.escapeHtml)(text);
|
|
44
|
+
if (!ctx.placeholders)
|
|
45
|
+
return '';
|
|
46
|
+
return placeholder(ctx.variables.get(key)?.label ?? key);
|
|
47
|
+
};
|
|
48
|
+
/** Plain text with each `{{key}}` replaced by its value as text, and a key with no value left empty. */
|
|
49
|
+
const filledText = (text, ctx) => text.replace(values_1.DOCUMENT_VARIABLE_PATTERN, (_, key) => valueText(key, ctx) ?? '');
|
|
50
|
+
/**
|
|
51
|
+
* Plain text with each `{{key}}` replaced by its value; everything else escaped, and every line
|
|
52
|
+
* break a `<br>`. A break left as a newline would hang on `white-space: pre-wrap`, and between two
|
|
53
|
+
* placeholders it is a text node of whitespace alone, which the portal's paged preview drops -
|
|
54
|
+
* the two lines then print as one. On an issued document a line whose variables are all empty
|
|
55
|
+
* is left out - "VAT no. {{client_vat_number}}" for a client with no VAT number, or
|
|
56
|
+
* `{{client_address}}` with no address - rather than printing its words with nothing after them;
|
|
57
|
+
* a line with one value filled, or with no variable at all (a blank line included), stays.
|
|
58
|
+
*/
|
|
59
|
+
const plainHtml = (text, ctx) => {
|
|
60
|
+
if (typeof text !== 'string')
|
|
61
|
+
return '';
|
|
62
|
+
return text
|
|
63
|
+
.split(/\r?\n/)
|
|
64
|
+
.flatMap((line) => {
|
|
65
|
+
let html = '';
|
|
66
|
+
let last = 0;
|
|
67
|
+
let variables = 0;
|
|
68
|
+
let filled = 0;
|
|
69
|
+
for (const match of line.matchAll(values_1.DOCUMENT_VARIABLE_PATTERN)) {
|
|
70
|
+
variables += 1;
|
|
71
|
+
if (PAGE_COUNTERS[match[1]] || valueText(match[1], ctx) !== null)
|
|
72
|
+
filled += 1;
|
|
73
|
+
html += (0, values_1.escapeHtml)(line.slice(last, match.index)) + valueHtml(match[1], ctx);
|
|
74
|
+
last = match.index + match[0].length;
|
|
75
|
+
}
|
|
76
|
+
html += (0, values_1.escapeHtml)(line.slice(last));
|
|
77
|
+
return !ctx.placeholders && variables > 0 && filled === 0 ? [] : [html];
|
|
78
|
+
})
|
|
79
|
+
.join('<br>');
|
|
80
|
+
};
|
|
81
|
+
/** Bold, italic, underline and links around `html`; any other mark is ignored. */
|
|
82
|
+
const withMarks = (html, marks) => (Array.isArray(marks) ? marks : []).reduce((inner, mark) => {
|
|
83
|
+
if (!(0, values_1.isObject)(mark) || typeof mark.type !== 'string')
|
|
84
|
+
return inner;
|
|
85
|
+
const tag = MARK_TAGS[mark.type];
|
|
86
|
+
if (tag)
|
|
87
|
+
return `<${tag}>${inner}</${tag}>`;
|
|
88
|
+
const href = mark.type === 'link' && (0, values_1.isObject)(mark.attrs) ? (0, values_1.safeHref)(mark.attrs.href) : null;
|
|
89
|
+
return href ? `<a href="${(0, values_1.escapeHtml)(href)}">${inner}</a>` : inner;
|
|
90
|
+
}, html);
|
|
91
|
+
/**
|
|
92
|
+
* Rich text (the editor's ProseMirror JSON) as HTML. Only the node types the sdk accepts are
|
|
93
|
+
* printed; anything else prints nothing, and every text run is escaped.
|
|
94
|
+
*/
|
|
95
|
+
const richTextHtml = (node, ctx) => {
|
|
96
|
+
if (!(0, values_1.isObject)(node))
|
|
97
|
+
return '';
|
|
98
|
+
const attrs = (0, values_1.isObject)(node.attrs) ? node.attrs : {};
|
|
99
|
+
const children = () => (Array.isArray(node.content) ? node.content.map((child) => richTextHtml(child, ctx)).join('') : '');
|
|
100
|
+
switch (node.type) {
|
|
101
|
+
case 'doc':
|
|
102
|
+
return children();
|
|
103
|
+
case 'paragraph':
|
|
104
|
+
return `<p>${children() || '<br>'}</p>`;
|
|
105
|
+
case 'heading': {
|
|
106
|
+
const level = [1, 2, 3].includes(attrs.level) ? attrs.level : 3;
|
|
107
|
+
return `<h${level}>${children()}</h${level}>`;
|
|
108
|
+
}
|
|
109
|
+
case 'bulletList':
|
|
110
|
+
return `<ul>${children()}</ul>`;
|
|
111
|
+
case 'orderedList':
|
|
112
|
+
return `<ol>${children()}</ol>`;
|
|
113
|
+
case 'listItem':
|
|
114
|
+
return `<li>${children()}</li>`;
|
|
115
|
+
case 'text':
|
|
116
|
+
return typeof node.text === 'string' ? withMarks((0, values_1.escapeHtml)(node.text), node.marks) : '';
|
|
117
|
+
case 'hardBreak':
|
|
118
|
+
return '<br>';
|
|
119
|
+
case 'variable':
|
|
120
|
+
return typeof attrs.key === 'string' ? withMarks(valueHtml(attrs.key, ctx), node.marks) : '';
|
|
121
|
+
default:
|
|
122
|
+
return '';
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
/** An amount in cents in the document's currency, escaped. */
|
|
126
|
+
const moneyHtml = (amountMinor, ctx) => (0, values_1.escapeHtml)((0, values_1.money)(amountMinor, ctx.currency));
|
|
127
|
+
/** A line's quantity (a plain number) or unit price (money), written in or taken from its variable. */
|
|
128
|
+
const lineAmountHtml = (amount, isMoney, ctx) => {
|
|
129
|
+
if (typeof amount !== 'number')
|
|
130
|
+
return valueHtml(amount.variable, ctx);
|
|
131
|
+
return isMoney ? moneyHtml(amount, ctx) : (0, values_1.escapeHtml)(String(amount));
|
|
132
|
+
};
|
|
133
|
+
const lineItemsHtml = (block, ctx) => {
|
|
134
|
+
const showDiscount = block.items.some((line) => line.discount_percent > 0);
|
|
135
|
+
const showTax = block.items.some((line) => line.tax_rate > 0);
|
|
136
|
+
const totals = (0, values_1.documentTotals)([block], ctx.values);
|
|
137
|
+
const rows = block.items
|
|
138
|
+
.map((line) => `
|
|
139
|
+
<tr>
|
|
140
|
+
<td class="item">${plainHtml(line.description, ctx)}</td>
|
|
141
|
+
<td class="num">${lineAmountHtml(line.quantity, false, ctx)}</td>
|
|
142
|
+
<td class="num">${lineAmountHtml(line.unit_price_minor, true, ctx)}</td>
|
|
143
|
+
${showDiscount ? `<td class="num">${line.discount_percent}%</td>` : ''}
|
|
144
|
+
${showTax ? `<td class="num">${line.tax_rate}%</td>` : ''}
|
|
145
|
+
<td class="num">${moneyHtml((0, values_1.lineAmounts)(line, ctx.values).net, ctx)}</td>
|
|
146
|
+
</tr>`)
|
|
147
|
+
.join('');
|
|
148
|
+
const totalRows = [
|
|
149
|
+
`<tr><td>Subtotal</td><td class="num">${moneyHtml(totals.subtotal_minor, ctx)}</td></tr>`,
|
|
150
|
+
totals.discount_minor > 0 ? `<tr><td>Discount</td><td class="num">-${moneyHtml(totals.discount_minor, ctx)}</td></tr>` : '',
|
|
151
|
+
totals.tax_minor > 0 ? `<tr><td>Tax</td><td class="num">${moneyHtml(totals.tax_minor, ctx)}</td></tr>` : '',
|
|
152
|
+
`<tr class="grand"><td>Total</td><td class="num">${moneyHtml(totals.total_minor, ctx)}</td></tr>`,
|
|
153
|
+
].join('');
|
|
154
|
+
return `
|
|
155
|
+
<table class="items">
|
|
156
|
+
<thead><tr><th>Item</th><th>Qty</th><th>Unit price</th>${showDiscount ? '<th>Discount</th>' : ''}${showTax ? '<th>Tax</th>' : ''}<th>Amount</th></tr></thead>
|
|
157
|
+
<tbody>${rows}
|
|
158
|
+
</tbody>
|
|
159
|
+
</table>
|
|
160
|
+
<div class="totals-wrap"><table class="totals"><tbody>${totalRows}</tbody></table></div>`;
|
|
161
|
+
};
|
|
162
|
+
const blockHtml = (block, ctx, imageUrls) => {
|
|
163
|
+
switch (block.type) {
|
|
164
|
+
case enum_1.DocumentBlockType.TEXT:
|
|
165
|
+
return `<div class="block text">${richTextHtml(block.content, ctx)}</div>`;
|
|
166
|
+
case enum_1.DocumentBlockType.FIELDS: {
|
|
167
|
+
// A blank side shows where it prints on a template or draft; an issued document leaves out a
|
|
168
|
+
// row with no value - its label alone says nothing - and the block with no rows left.
|
|
169
|
+
const rows = block.fields.flatMap((field) => {
|
|
170
|
+
const label = plainHtml(field.label, ctx);
|
|
171
|
+
const value = plainHtml(field.value, ctx);
|
|
172
|
+
if (ctx.placeholders) {
|
|
173
|
+
return [`<tr><th>${label.trim() || placeholder('Label')}</th><td>${value.trim() || placeholder('Value')}</td></tr>`];
|
|
174
|
+
}
|
|
175
|
+
return value.trim() ? [`<tr><th>${label}</th><td>${value}</td></tr>`] : [];
|
|
176
|
+
});
|
|
177
|
+
return rows.length > 0 ? `<div class="block"><table class="fields"><tbody>${rows.join('')}</tbody></table></div>` : '';
|
|
178
|
+
}
|
|
179
|
+
case enum_1.DocumentBlockType.LINE_ITEMS:
|
|
180
|
+
return `<div class="block">${lineItemsHtml(block, ctx)}</div>`;
|
|
181
|
+
case enum_1.DocumentBlockType.TABLE:
|
|
182
|
+
return `<div class="block"><table class="grid"><thead><tr>${block.columns
|
|
183
|
+
.map((column) => `<th>${plainHtml(column, ctx)}</th>`)
|
|
184
|
+
.join('')}</tr></thead><tbody>${block.rows
|
|
185
|
+
.map((row) => `<tr>${row.map((cell) => `<td>${plainHtml(cell, ctx)}</td>`).join('')}</tr>`)
|
|
186
|
+
.join('')}</tbody></table></div>`;
|
|
187
|
+
case enum_1.DocumentBlockType.SIGNATURE: {
|
|
188
|
+
// Two signers a row, laid out as a grid row by row - signature lines, their labels, date
|
|
189
|
+
// lines, "Date" - so each line sits level across the signers however long a label wraps.
|
|
190
|
+
const rows = Array.from({ length: Math.ceil(block.signers.length / 2) }, (_, index) => block.signers.slice(index * 2, index * 2 + 2));
|
|
191
|
+
return `<div class="block signatures">${rows
|
|
192
|
+
.map((pair) => `
|
|
193
|
+
<div class="signature-row">
|
|
194
|
+
${pair.map(() => '<div class="sign-line"></div>').join('')}
|
|
195
|
+
${pair.map((signer) => `<div class="sign-label">${plainHtml(signer.label, ctx)}</div>`).join('')}
|
|
196
|
+
${pair.map(() => '<div class="sign-line date"></div>').join('')}
|
|
197
|
+
${pair.map(() => '<div class="sign-label">Date</div>').join('')}
|
|
198
|
+
</div>`)
|
|
199
|
+
.join('')}</div>`;
|
|
200
|
+
}
|
|
201
|
+
case enum_1.DocumentBlockType.IMAGE: {
|
|
202
|
+
const url = imageUrls[block.path];
|
|
203
|
+
if (!url)
|
|
204
|
+
return ctx.placeholders ? '<div class="block image-missing">[Image]</div>' : '';
|
|
205
|
+
// An attribute takes text, not markup, so a missing value there is simply left out.
|
|
206
|
+
const alt = typeof block.alt === 'string' ? filledText(block.alt, ctx) : '';
|
|
207
|
+
return `<div class="block image"><img src="${(0, values_1.escapeHtml)(url)}" alt="${(0, values_1.escapeHtml)(alt)}"></div>`;
|
|
208
|
+
}
|
|
209
|
+
case enum_1.DocumentBlockType.COLUMNS: {
|
|
210
|
+
// Side by side, each a muted heading over its text, the text's first line as a name. Blank
|
|
211
|
+
// sides show where they print on a template or draft; an issued column with no text prints
|
|
212
|
+
// only when its heading has some, and the block with no column left prints nothing.
|
|
213
|
+
const columns = block.columns.flatMap((column) => {
|
|
214
|
+
const heading = plainHtml(column.heading, ctx).trim();
|
|
215
|
+
// Its lines with the blank ones at either end dropped (an issued column's empty last values).
|
|
216
|
+
const lines = plainHtml(column.text, ctx).split('<br>');
|
|
217
|
+
while (lines.length > 0 && !lines[0].trim())
|
|
218
|
+
lines.shift();
|
|
219
|
+
while (lines.length > 0 && !lines[lines.length - 1].trim())
|
|
220
|
+
lines.pop();
|
|
221
|
+
const [name = '', ...rest] = lines.map((line) => line.trim());
|
|
222
|
+
if (!ctx.placeholders && !name && !heading)
|
|
223
|
+
return [];
|
|
224
|
+
const text = name || ctx.placeholders
|
|
225
|
+
? `<div class="column-name">${name || placeholder('Text')}</div>${rest.length > 0 ? `<div class="column-text">${rest.join('<br>')}</div>` : ''}`
|
|
226
|
+
: '';
|
|
227
|
+
return [`<div class="column"><div class="column-heading">${heading || (ctx.placeholders ? placeholder('Heading') : '')}</div>${text}</div>`];
|
|
228
|
+
});
|
|
229
|
+
return columns.length > 0
|
|
230
|
+
? `<div class="block columns" style="grid-template-columns: repeat(${columns.length}, 1fr);">${columns.join('')}</div>`
|
|
231
|
+
: '';
|
|
232
|
+
}
|
|
233
|
+
case enum_1.DocumentBlockType.PAGE_BREAK:
|
|
234
|
+
return '<div class="page-break"></div>';
|
|
235
|
+
// HEADER and FOOTER print in the PDF's running bands, not in the body.
|
|
236
|
+
default:
|
|
237
|
+
return '';
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const contextOf = (document) => ({
|
|
241
|
+
variables: new Map([
|
|
242
|
+
...Object.entries(values_1.SYSTEM_VARIABLES),
|
|
243
|
+
...document.variables.map((variable) => [variable.key, variable]),
|
|
244
|
+
]),
|
|
245
|
+
values: document.values,
|
|
246
|
+
currency: document.currency,
|
|
247
|
+
placeholders: document.status === null || document.status === enum_1.DocumentStatus.DRAFT,
|
|
248
|
+
});
|
|
249
|
+
/**
|
|
250
|
+
* Plain text with each `{{key}}` replaced by its value as the document prints it - money in its
|
|
251
|
+
* currency, dates written out - and a key with no value left empty: a stored email's subject and
|
|
252
|
+
* message, written before the document had a number, filled when it is sent. Text, not HTML, so
|
|
253
|
+
* nothing is escaped; the email template shows it as text.
|
|
254
|
+
*/
|
|
255
|
+
const fillText = (text, document) => filledText(text, contextOf(document));
|
|
256
|
+
exports.fillText = fillText;
|
|
257
|
+
/**
|
|
258
|
+
* The page geometry, in inches: the side and bottom margins, and the top margin - grown to make
|
|
259
|
+
* room when the document has a header. The api's PDF and the portal's paged preview use the same.
|
|
260
|
+
*/
|
|
261
|
+
exports.DOCUMENT_PAGE_MARGINS = { top: 0.62, topWithHeader: 0.95, bottom: 0.7, side: 0.7 };
|
|
262
|
+
/**
|
|
263
|
+
* One band as a styled block: left text left, right text right, in the footer's small grey type.
|
|
264
|
+
* The styles are inline (a placeholder's included) because Chromium's header and footer templates
|
|
265
|
+
* ignore the document's CSS; the side margins mirror the page's. The api hands it to Chromium as
|
|
266
|
+
* the header or footer template; the portal's preview places it on each page.
|
|
267
|
+
*/
|
|
268
|
+
const renderBand = (band, edge) => `
|
|
269
|
+
<style>.placeholder { color: #8a6100; background: #fff4d6; border-radius: 2px; padding: 0 2px; -webkit-print-color-adjust: exact; }</style>
|
|
270
|
+
<div style="width:100%;margin:0 ${exports.DOCUMENT_PAGE_MARGINS.side}in;${edge === 'top' ? 'padding-top:0.32in;' : ''}font-size:8.5px;color:#6b6960;
|
|
271
|
+
font-family:-apple-system,'Segoe UI',Helvetica,Arial,sans-serif;
|
|
272
|
+
display:flex;justify-content:space-between;align-items:baseline;gap:24px;">
|
|
273
|
+
<span style="text-align:left;">${band.left}</span>
|
|
274
|
+
<span style="text-align:right;">${band.right}</span>
|
|
275
|
+
</div>`;
|
|
276
|
+
exports.renderBand = renderBand;
|
|
277
|
+
/**
|
|
278
|
+
* A document's blocks and values as HTML, plus its running header and footer - the only way a
|
|
279
|
+
* document or template is shown, so what is seen is what prints. Styled as the subscription
|
|
280
|
+
* invoice is: the first page's letterhead - the organization top left against the title, number,
|
|
281
|
+
* issue date and status top right - then the blocks. Header and footer blocks print nothing in the
|
|
282
|
+
* body: their text is `header` / `footer`, with `{{key}}`s printed as in the body and
|
|
283
|
+
* `{{page_number}}` / `{{page_count}}` as page-counter spans. `imageUrls` maps the image blocks'
|
|
284
|
+
* paths to URLs the caller signed; a path without one prints as `[Image]` on a draft or template
|
|
285
|
+
* and as nothing once issued.
|
|
286
|
+
*/
|
|
287
|
+
const renderDocument = (document, imageUrls) => {
|
|
288
|
+
const ctx = contextOf(document);
|
|
289
|
+
const title = (0, values_1.escapeHtml)(document.title || 'Document');
|
|
290
|
+
const status = document.status ? STATUS_LABELS[document.status] : 'Template';
|
|
291
|
+
const statusColour = document.status === enum_1.DocumentStatus.ISSUED ? '#1d6b4a'
|
|
292
|
+
: document.status === enum_1.DocumentStatus.VOID ? '#b3261e'
|
|
293
|
+
: '#6b6960';
|
|
294
|
+
const address = valueText(enum_1.DocumentSystemVariable.ORGANIZATION_ADDRESS, ctx);
|
|
295
|
+
const header = document.blocks.find((block) => block.type === enum_1.DocumentBlockType.HEADER);
|
|
296
|
+
const footer = document.blocks.find((block) => block.type === enum_1.DocumentBlockType.FOOTER);
|
|
297
|
+
const defaultLeft = [valueText(enum_1.DocumentSystemVariable.ORGANIZATION_NAME, ctx), [document.title, document.number].filter(Boolean).join(' ')]
|
|
298
|
+
.filter(Boolean)
|
|
299
|
+
.map((part) => (0, values_1.escapeHtml)(part))
|
|
300
|
+
.join(' · ');
|
|
301
|
+
const html = `<!doctype html>
|
|
302
|
+
<html lang="en">
|
|
303
|
+
<head>
|
|
304
|
+
<meta charset="utf-8">
|
|
305
|
+
<title>${title}</title>
|
|
306
|
+
<style>
|
|
307
|
+
/* No @page rule: the PDF renderer's margins own the layout so every page gets them. */
|
|
308
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
309
|
+
html, body { background: #ffffff; }
|
|
310
|
+
body {
|
|
311
|
+
font-family: 'Instrument Sans', -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
312
|
+
color: #1b1b18; font-size: 10.5pt; line-height: 1.5;
|
|
313
|
+
}
|
|
314
|
+
.top { display: flex; justify-content: space-between; align-items: flex-start; gap: 40px;
|
|
315
|
+
padding-bottom: 18px; border-bottom: 1px solid #dedcd4; }
|
|
316
|
+
.brand-name { font-size: 19px; font-weight: 700; letter-spacing: -0.3px; color: #1d6b4a; line-height: 1.2; }
|
|
317
|
+
.brand-lines { font-size: 10pt; line-height: 15px; color: #5f5d56; margin-top: 6px; max-width: 60ch; }
|
|
318
|
+
.brand-lines + .brand-lines { margin-top: 2px; }
|
|
319
|
+
.doc { text-align: right; }
|
|
320
|
+
.doc-title { font-size: 26px; font-weight: 700; letter-spacing: -0.6px; line-height: 1.1; }
|
|
321
|
+
.doc-number { font-family: ${values_1.MONO_FONT}; font-size: 10.5pt; color: #5f5d56; margin-top: 8px; }
|
|
322
|
+
.doc-date { font-size: 10pt; color: #5f5d56; margin-top: 4px; }
|
|
323
|
+
.doc-status { font-size: 11pt; font-weight: 700; letter-spacing: 0.3px; color: ${statusColour}; margin-top: 10px; }
|
|
324
|
+
|
|
325
|
+
.block { margin-top: 18px; }
|
|
326
|
+
.text p { margin: 0 0 8px; }
|
|
327
|
+
.text h1, .text h2, .text h3 { line-height: 1.25; margin: 14px 0 6px; break-after: avoid; }
|
|
328
|
+
.text h1 { font-size: 16pt; } .text h2 { font-size: 13pt; } .text h3 { font-size: 11pt; }
|
|
329
|
+
.text ul, .text ol { margin: 0 0 8px 22px; }
|
|
330
|
+
.text a { color: #1d6b4a; }
|
|
331
|
+
|
|
332
|
+
table { width: 100%; border-collapse: collapse; }
|
|
333
|
+
tr, .totals-wrap, .signatures, .image { break-inside: avoid; }
|
|
334
|
+
/* The small uppercase label every heading cell and column heading prints in. */
|
|
335
|
+
.fields th, .column-heading, .items th, .grid th {
|
|
336
|
+
font-size: 8pt; font-weight: 700; letter-spacing: 1.1px; text-transform: uppercase; color: #6b6960;
|
|
337
|
+
}
|
|
338
|
+
.fields th { width: 32%; text-align: left; vertical-align: top; padding: 5px 16px 5px 0; }
|
|
339
|
+
.fields td { padding: 4px 0; white-space: pre-wrap; vertical-align: top; }
|
|
340
|
+
|
|
341
|
+
.columns { display: grid; column-gap: 32px; break-inside: avoid; }
|
|
342
|
+
.column-heading { margin-bottom: 4px; }
|
|
343
|
+
.column-name { font-weight: 600; white-space: pre-wrap; }
|
|
344
|
+
.column-text { white-space: pre-wrap; color: #3a3a38; }
|
|
345
|
+
|
|
346
|
+
.items th, .grid th { text-align: left; padding: 0 0 9px 16px; border-bottom: 1px solid #1b1b18; white-space: nowrap; }
|
|
347
|
+
.items th:first-child, .grid th:first-child, .grid td:first-child { padding-left: 0; }
|
|
348
|
+
.items th:not(:first-child) { text-align: right; }
|
|
349
|
+
.items td, .grid td { padding: 9px 0 9px 16px; border-bottom: 1px solid #ebe9e1; vertical-align: top; }
|
|
350
|
+
.items td.item { padding-left: 0; white-space: pre-wrap; }
|
|
351
|
+
.items td.num { text-align: right; font-family: ${values_1.MONO_FONT}; font-size: 10pt; white-space: nowrap; }
|
|
352
|
+
.grid td { white-space: pre-wrap; }
|
|
353
|
+
|
|
354
|
+
.totals-wrap { display: flex; justify-content: flex-end; margin-top: 12px; }
|
|
355
|
+
table.totals { width: auto; min-width: 300px; }
|
|
356
|
+
.totals td { padding: 4px 0; color: #5f5d56; }
|
|
357
|
+
.totals td.num { padding-left: 32px; text-align: right; font-family: ${values_1.MONO_FONT}; font-size: 10pt; color: inherit; }
|
|
358
|
+
.totals tr.grand td { padding-top: 10px; border-top: 1px solid #1b1b18; font-size: 11pt; font-weight: 700; color: #1b1b18; }
|
|
359
|
+
|
|
360
|
+
.signatures { margin-top: 36px; }
|
|
361
|
+
.signature-row { display: grid; grid-template-columns: 1fr 1fr; column-gap: 48px; align-items: start; }
|
|
362
|
+
.signature-row + .signature-row { margin-top: 28px; }
|
|
363
|
+
.sign-line { border-bottom: 1px solid #1b1b18; height: 40px; }
|
|
364
|
+
.sign-line.date { height: 28px; width: 60%; }
|
|
365
|
+
.sign-label { font-size: 9pt; color: #5f5d56; margin-top: 4px; }
|
|
366
|
+
|
|
367
|
+
.image img { display: block; max-width: 100%; max-height: 120mm; }
|
|
368
|
+
.image-missing { padding: 24px; text-align: center; color: #8a6100; background: #fff4d6; border-radius: 6px; }
|
|
369
|
+
.placeholder { color: #8a6100; background: #fff4d6; border-radius: 3px; padding: 0 3px; }
|
|
370
|
+
|
|
371
|
+
.page-break { break-before: page; page-break-before: always; }
|
|
372
|
+
</style>
|
|
373
|
+
</head>
|
|
374
|
+
<body>
|
|
375
|
+
<div class="top">
|
|
376
|
+
<div>
|
|
377
|
+
<div class="brand-name">${valueHtml(enum_1.DocumentSystemVariable.ORGANIZATION_NAME, ctx)}</div>
|
|
378
|
+
${[
|
|
379
|
+
[address],
|
|
380
|
+
[
|
|
381
|
+
prefixed('Reg. no.', valueText(enum_1.DocumentSystemVariable.ORGANIZATION_REGISTRATION_NUMBER, ctx)),
|
|
382
|
+
prefixed('VAT no.', valueText(enum_1.DocumentSystemVariable.ORGANIZATION_VAT_NUMBER, ctx)),
|
|
383
|
+
],
|
|
384
|
+
[valueText(enum_1.DocumentSystemVariable.ORGANIZATION_EMAIL, ctx), valueText(enum_1.DocumentSystemVariable.ORGANIZATION_PHONE, ctx)],
|
|
385
|
+
]
|
|
386
|
+
.map((parts) => parts.filter(Boolean).join(' · '))
|
|
387
|
+
.filter(Boolean)
|
|
388
|
+
.map((line) => `<div class="brand-lines">${(0, values_1.escapeHtml)(line)}</div>`)
|
|
389
|
+
.join('')}
|
|
390
|
+
</div>
|
|
391
|
+
<div class="doc">
|
|
392
|
+
<div class="doc-title">${title}</div>
|
|
393
|
+
<!-- The number line is kept on drafts too, so issuing never reflows the pages. -->
|
|
394
|
+
<div class="doc-number">${document.number ? (0, values_1.escapeHtml)(document.number) : ' '}</div>
|
|
395
|
+
<div class="doc-date">${valueHtml(enum_1.DocumentSystemVariable.ISSUE_DATE, ctx)}</div>
|
|
396
|
+
<div class="doc-status">${status}</div>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
${document.blocks.map((block) => blockHtml(block, ctx, imageUrls)).join('')}
|
|
400
|
+
</body>
|
|
401
|
+
</html>`;
|
|
402
|
+
return {
|
|
403
|
+
html,
|
|
404
|
+
header: header ? { left: plainHtml(header.left, ctx), right: plainHtml(header.right, ctx) } : null,
|
|
405
|
+
footer: footer
|
|
406
|
+
? { left: plainHtml(footer.left, ctx), right: plainHtml(footer.right, ctx) }
|
|
407
|
+
: {
|
|
408
|
+
left: defaultLeft,
|
|
409
|
+
right: `Page ${PAGE_COUNTERS[enum_1.DocumentSystemVariable.PAGE_NUMBER]} of ${PAGE_COUNTERS[enum_1.DocumentSystemVariable.PAGE_COUNT]}`,
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
};
|
|
413
|
+
exports.renderDocument = renderDocument;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { DOCUMENT_PAGE_MARGINS, fillText, renderBand, renderDocument, type DocumentBand, type PrintableDocument, type RenderedDocument, } from './html';
|
|
2
|
+
export { documentTotals, escapeHtml, formatDocumentDate, johannesburgDate, money, MONO_FONT, printsMoney, startingValues, SYSTEM_VARIABLES, systemValues, type DocumentTotals, type SystemValueInputs, } from './values';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.systemValues = exports.SYSTEM_VARIABLES = exports.startingValues = exports.printsMoney = exports.MONO_FONT = exports.money = exports.johannesburgDate = exports.formatDocumentDate = exports.escapeHtml = exports.documentTotals = exports.renderDocument = exports.renderBand = exports.fillText = exports.DOCUMENT_PAGE_MARGINS = void 0;
|
|
4
|
+
// `@managemint-solutions/sdk/documents`: the document renderer and the values it prints, as pure
|
|
5
|
+
// functions with no dependency beyond the shared entities - no Node, no Nest, no supabase-js, no
|
|
6
|
+
// DOM - so the api's PDF and the portal's in-browser preview are one renderer. The caller loads
|
|
7
|
+
// the organization and client and signs the image URLs; nothing here does I/O.
|
|
8
|
+
var html_1 = require("./html");
|
|
9
|
+
Object.defineProperty(exports, "DOCUMENT_PAGE_MARGINS", { enumerable: true, get: function () { return html_1.DOCUMENT_PAGE_MARGINS; } });
|
|
10
|
+
Object.defineProperty(exports, "fillText", { enumerable: true, get: function () { return html_1.fillText; } });
|
|
11
|
+
Object.defineProperty(exports, "renderBand", { enumerable: true, get: function () { return html_1.renderBand; } });
|
|
12
|
+
Object.defineProperty(exports, "renderDocument", { enumerable: true, get: function () { return html_1.renderDocument; } });
|
|
13
|
+
var values_1 = require("./values");
|
|
14
|
+
Object.defineProperty(exports, "documentTotals", { enumerable: true, get: function () { return values_1.documentTotals; } });
|
|
15
|
+
Object.defineProperty(exports, "escapeHtml", { enumerable: true, get: function () { return values_1.escapeHtml; } });
|
|
16
|
+
Object.defineProperty(exports, "formatDocumentDate", { enumerable: true, get: function () { return values_1.formatDocumentDate; } });
|
|
17
|
+
Object.defineProperty(exports, "johannesburgDate", { enumerable: true, get: function () { return values_1.johannesburgDate; } });
|
|
18
|
+
Object.defineProperty(exports, "money", { enumerable: true, get: function () { return values_1.money; } });
|
|
19
|
+
Object.defineProperty(exports, "MONO_FONT", { enumerable: true, get: function () { return values_1.MONO_FONT; } });
|
|
20
|
+
Object.defineProperty(exports, "printsMoney", { enumerable: true, get: function () { return values_1.printsMoney; } });
|
|
21
|
+
Object.defineProperty(exports, "startingValues", { enumerable: true, get: function () { return values_1.startingValues; } });
|
|
22
|
+
Object.defineProperty(exports, "SYSTEM_VARIABLES", { enumerable: true, get: function () { return values_1.SYSTEM_VARIABLES; } });
|
|
23
|
+
Object.defineProperty(exports, "systemValues", { enumerable: true, get: function () { return values_1.systemValues; } });
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { DocumentBlock, DocumentLineItem, DocumentVariable, DocumentVariableValues } from '@managemint-solutions/entities/documents';
|
|
2
|
+
import { DocumentSystemVariable } from '@managemint-solutions/entities/documents/enum';
|
|
3
|
+
/**
|
|
4
|
+
* A variable in plain text (field values, table cells, line descriptions, signer labels, header
|
|
5
|
+
* and footer text): `{{key}}`, spaces inside the braces allowed. The content rules check with it
|
|
6
|
+
* and the renderer replaces with it, so what is checked is exactly what prints. Global: use it
|
|
7
|
+
* with `matchAll` or `replace`, never `test`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const DOCUMENT_VARIABLE_PATTERN: RegExp;
|
|
10
|
+
/** How each system variable is labelled where it is missing, and the type it prints as. */
|
|
11
|
+
export declare const SYSTEM_VARIABLES: Record<DocumentSystemVariable, Pick<DocumentVariable, 'label' | 'type'>>;
|
|
12
|
+
/** Stored block and rich-text JSON, read property by property. */
|
|
13
|
+
export type Json = Record<string, unknown>;
|
|
14
|
+
export declare const isObject: (value: unknown) => value is Json;
|
|
15
|
+
/**
|
|
16
|
+
* A link's href, or null unless it is http, https or mailto. The protocol is read as a browser
|
|
17
|
+
* reads it: the URL parser drops the tabs and newlines a plain prefix check would let hide a
|
|
18
|
+
* javascript: link. The content rules refuse what this refuses, and the renderer prints only what
|
|
19
|
+
* it passes.
|
|
20
|
+
*/
|
|
21
|
+
export declare const safeHref: (href: unknown) => string | null;
|
|
22
|
+
export declare const escapeHtml: (value: string) => string;
|
|
23
|
+
/** Integer cents in `currency`, the en-ZA way: `R 1 299,00`. Subscription invoices print with it too. */
|
|
24
|
+
export declare const money: (amountMinor: number, currency: string) => string;
|
|
25
|
+
/** `2026-11-01` -> `1 November 2026`. */
|
|
26
|
+
export declare const formatDocumentDate: (date: string) => string;
|
|
27
|
+
/** The monospace stack for numbers - the design's JetBrains Mono, self-contained. */
|
|
28
|
+
export declare const MONO_FONT = "ui-monospace, 'Cascadia Mono', 'Segoe UI Mono', Consolas, monospace";
|
|
29
|
+
export type DocumentTotals = {
|
|
30
|
+
subtotal_minor: number;
|
|
31
|
+
discount_minor: number;
|
|
32
|
+
tax_minor: number;
|
|
33
|
+
total_minor: number;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* One line's amounts in cents: gross = quantity x unit price, the discount a percentage of the
|
|
37
|
+
* gross, net = gross - discount, the tax a percentage of the net - each rounded to the cent.
|
|
38
|
+
*/
|
|
39
|
+
export declare const lineAmounts: (line: DocumentLineItem, values: DocumentVariableValues) => {
|
|
40
|
+
gross: number;
|
|
41
|
+
discount: number;
|
|
42
|
+
net: number;
|
|
43
|
+
tax: number;
|
|
44
|
+
};
|
|
45
|
+
/** The sums over every line of every line items block in `blocks`; all zero without one. */
|
|
46
|
+
export declare const documentTotals: (blocks: DocumentBlock[], values: DocumentVariableValues) => DocumentTotals;
|
|
47
|
+
/** Whether the content prints money - a line items block or a money variable - and so needs a currency. */
|
|
48
|
+
export declare const printsMoney: (blocks: DocumentBlock[], variables: DocumentVariable[]) => boolean;
|
|
49
|
+
/** A new draft's values: each variable's default, overlaid by the values given. */
|
|
50
|
+
export declare const startingValues: (variables: DocumentVariable[], given?: DocumentVariableValues) => DocumentVariableValues;
|
|
51
|
+
/** The calendar date (`YYYY-MM-DD`) `now` falls on in Africa/Johannesburg - a draft's issue date. */
|
|
52
|
+
export declare const johannesburgDate: (now: Date) => string;
|
|
53
|
+
/** What the system values are read from, as plain values - the caller loads them. */
|
|
54
|
+
export type SystemValueInputs = {
|
|
55
|
+
organization: {
|
|
56
|
+
name: string | null | undefined;
|
|
57
|
+
trading_name: string | null | undefined;
|
|
58
|
+
/** The address parts in print order (line 1, line 2, city, state, post code, country); blanks are skipped. */
|
|
59
|
+
address: (string | null | undefined)[];
|
|
60
|
+
registration_number?: string | null;
|
|
61
|
+
vat_number?: string | null;
|
|
62
|
+
email?: string | null;
|
|
63
|
+
phone?: string | null;
|
|
64
|
+
};
|
|
65
|
+
client: {
|
|
66
|
+
name?: string | null;
|
|
67
|
+
email?: string | null;
|
|
68
|
+
phone?: string | null;
|
|
69
|
+
/** In the organization's print order; blanks are skipped. */
|
|
70
|
+
address?: (string | null | undefined)[];
|
|
71
|
+
registration_number?: string | null;
|
|
72
|
+
vat_number?: string | null;
|
|
73
|
+
} | null;
|
|
74
|
+
/** Null until the document is issued. */
|
|
75
|
+
number: string | null;
|
|
76
|
+
/** `YYYY-MM-DD`: today on a draft (`johannesburgDate(new Date())`), the issue date once issued. */
|
|
77
|
+
issueDate: string;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* The values filled in for the system variables: the organization's name (its trading name
|
|
81
|
+
* first), address, registration and VAT numbers, email and phone; the client's name, email, phone,
|
|
82
|
+
* address, registration and VAT numbers; the document number; and the issue date with its month
|
|
83
|
+
* name and year. A value that is empty is left out, so it prints as missing. The
|
|
84
|
+
* page number and count are not values: they are counted page by page as the PDF prints.
|
|
85
|
+
*/
|
|
86
|
+
export declare const systemValues: ({ organization, client, number, issueDate }: SystemValueInputs) => DocumentVariableValues;
|