@opencxh/domain 1.234.0 → 1.236.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/entities/artifact/blocks.d.ts +8 -149
- package/dist/entities/artifact/markdown.d.ts +2 -6
- package/dist/entities/company/types.d.ts +2 -0
- package/dist/entities/contact/types.d.ts +8 -0
- package/dist/entities/document-template/index.d.ts +3 -0
- package/dist/entities/document-template/locale.d.ts +38 -0
- package/dist/entities/document-template/tokens.d.ts +27 -0
- package/dist/entities/document-template/types.d.ts +87 -0
- package/dist/entities/kb/types.d.ts +9 -0
- package/dist/entities/organization/types.d.ts +8 -0
- package/dist/entities/planning/capacity.d.ts +45 -2
- package/dist/index.cjs +21 -14
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1837 -1475
- package/dist/platform/document-blocks.d.ts +240 -0
- package/dist/platform/document-blocks.test.d.ts +1 -0
- package/dist/platform/document-markdown.d.ts +6 -0
- package/dist/platform/document-markdown.test.d.ts +1 -0
- package/dist/platform/document-parse.d.ts +2 -0
- package/dist/platform/document-text.d.ts +14 -0
- package/dist/platform/document-text.test.d.ts +1 -0
- package/package.json +1 -1
- /package/dist/entities/{artifact/blocks.test.d.ts → document-template/locale.test.d.ts} +0 -0
- /package/dist/entities/{artifact/markdown.test.d.ts → document-template/tokens.test.d.ts} +0 -0
|
@@ -1,153 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The artifact vocabulary, under its original names.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* paragraphs, a table and a KPI row, and its text is just text.
|
|
4
|
+
* The blocks moved to `platform/document-blocks.ts` when a second and third writer appeared: a
|
|
5
|
+
* knowledge base article and a quote are the same list of blocks, and a vocabulary with three
|
|
6
|
+
* consumers is a platform contract rather than one entity's model. See
|
|
7
|
+
* `plans/DOCUMENTS-2026-09-14.md`.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* - an optional `block_id`, so a block can be pointed at without rewriting the whole document;
|
|
13
|
-
* - {@link normalizeArtifactBlocks} **prunes instead of refusing** — one bad block must not make a
|
|
14
|
-
* whole document disappear — and reports *every* drop, because silent pruning makes "why is my
|
|
15
|
-
* table missing?" unanswerable;
|
|
16
|
-
* - unknown block types are skipped, so a newer writer can coexist with an older renderer;
|
|
17
|
-
* - semantic tones only, no free colours, so dark mode keeps working.
|
|
18
|
-
*
|
|
19
|
-
* **The inert boundary.** An artifact is stored data, not a program, and that is a property of the
|
|
20
|
-
* *model* rather than the outcome of a filter afterwards. Block texts are **markdown, never
|
|
21
|
-
* HTML**: the renderer hands them to ui-kit's `RichText` with `as="markdown"`, and that branch
|
|
22
|
-
* (react-markdown + remark-gfm) emits no raw HTML, so a `<script>` in the text reaches the screen
|
|
23
|
-
* as literal characters. Hence no tag stripping here — that would break "a < b" and win nothing.
|
|
24
|
-
* What *is* stripped is the only thing markdown itself makes dangerous: a link to a scheme outside
|
|
25
|
-
* {@link ALLOWED_LINK_SCHEMES}, and inline images. See {@link sanitizeInline}.
|
|
26
|
-
*/
|
|
27
|
-
/** Semantic colour. No free colours, so dark mode and the tokens keep working. */
|
|
28
|
-
export type ArtifactTone = "info" | "success" | "warning" | "destructive";
|
|
29
|
-
/**
|
|
30
|
-
* One line in a list.
|
|
31
|
-
*
|
|
32
|
-
* `lead` exists because a findings list puts its point up front in bold ("**Yealink outages
|
|
33
|
-
* dominate.** 14 of the 184 calls..."). Without a field of its own the writer has to invent
|
|
34
|
-
* markdown asterisks in a place where the formatting is supposed to be fixed — and then the text
|
|
35
|
-
* shows whether someone forgot.
|
|
36
|
-
*/
|
|
37
|
-
export interface ArtifactListItem {
|
|
38
|
-
lead?: string;
|
|
39
|
-
text: string;
|
|
40
|
-
}
|
|
41
|
-
export interface ArtifactTableColumn {
|
|
42
|
-
label: string;
|
|
43
|
-
/** Numbers right. Left by default. */
|
|
44
|
-
align?: "left" | "right";
|
|
45
|
-
}
|
|
46
|
-
export interface ArtifactKpiItem {
|
|
47
|
-
/** Already formatted by the writer ("1u 12m", "184", "-12%"). */
|
|
48
|
-
value: string;
|
|
49
|
-
label: string;
|
|
50
|
-
/** Only set when this number stands out; a tint on every tile is wallpaper. */
|
|
51
|
-
tone?: ArtifactTone;
|
|
52
|
-
}
|
|
53
|
-
export type ArtifactBlock = {
|
|
54
|
-
block_id?: string;
|
|
55
|
-
type: "heading";
|
|
56
|
-
level: 1 | 2 | 3;
|
|
57
|
-
text: string;
|
|
58
|
-
} | {
|
|
59
|
-
block_id?: string;
|
|
60
|
-
type: "paragraph";
|
|
61
|
-
text: string;
|
|
62
|
-
} | {
|
|
63
|
-
block_id?: string;
|
|
64
|
-
type: "list";
|
|
65
|
-
style: "bulleted" | "numbered";
|
|
66
|
-
items: ArtifactListItem[];
|
|
67
|
-
} | {
|
|
68
|
-
block_id?: string;
|
|
69
|
-
type: "quote";
|
|
70
|
-
text: string;
|
|
71
|
-
} | {
|
|
72
|
-
block_id?: string;
|
|
73
|
-
type: "code";
|
|
74
|
-
lang?: string;
|
|
75
|
-
text: string;
|
|
76
|
-
} | {
|
|
77
|
-
block_id?: string;
|
|
78
|
-
type: "divider";
|
|
79
|
-
} | {
|
|
80
|
-
block_id?: string;
|
|
81
|
-
type: "table";
|
|
82
|
-
columns: ArtifactTableColumn[];
|
|
83
|
-
rows: string[][];
|
|
84
|
-
caption?: string;
|
|
85
|
-
} | {
|
|
86
|
-
block_id?: string;
|
|
87
|
-
type: "kpi";
|
|
88
|
-
items: ArtifactKpiItem[];
|
|
89
|
-
} | {
|
|
90
|
-
block_id?: string;
|
|
91
|
-
type: "callout";
|
|
92
|
-
tone: ArtifactTone;
|
|
93
|
-
title?: string;
|
|
94
|
-
text: string;
|
|
95
|
-
};
|
|
96
|
-
export type ArtifactBlockType = ArtifactBlock["type"];
|
|
97
|
-
/**
|
|
98
|
-
* Ceilings. Wider than the timeline (which sits at 10 blocks) because this is a document, but not
|
|
99
|
-
* unlimited: the body travels as JSON through the same invoke channel as everything else and ends
|
|
100
|
-
* up in a single column.
|
|
101
|
-
*/
|
|
102
|
-
export declare const ARTIFACT_MAX_BLOCKS = 200;
|
|
103
|
-
export declare const ARTIFACT_MAX_LIST_ITEMS = 100;
|
|
104
|
-
export declare const ARTIFACT_MAX_TABLE_ROWS = 200;
|
|
105
|
-
export declare const ARTIFACT_MAX_TABLE_COLUMNS = 12;
|
|
106
|
-
export declare const ARTIFACT_MAX_KPI_ITEMS = 4;
|
|
107
|
-
export declare const ARTIFACT_MAX_TEXT_LEN = 4000;
|
|
108
|
-
export declare const ARTIFACT_MAX_CELL_LEN = 500;
|
|
109
|
-
/** Hard upper bound on the serialized body. */
|
|
110
|
-
export declare const ARTIFACT_MAX_BODY_BYTES: number;
|
|
111
|
-
/**
|
|
112
|
-
* Schemes a link in an artifact may carry.
|
|
113
|
-
*
|
|
114
|
-
* `javascript:` and `data:` are not among them, and that is the entire reason this list exists:
|
|
115
|
-
* those two are the only way markdown gets something executable into the document.
|
|
116
|
-
*/
|
|
117
|
-
export declare const ALLOWED_LINK_SCHEMES: readonly ["http:", "https:", "mailto:", "tel:"];
|
|
118
|
-
/**
|
|
119
|
-
* Makes one piece of inline markdown safe while keeping it readable.
|
|
120
|
-
*
|
|
121
|
-
* Three interventions, in this order:
|
|
122
|
-
*
|
|
123
|
-
* 1. **Inline images disappear**, with their alt text as replacement. This version has no `image`
|
|
124
|
-
* block precisely because the product has no publicly loadable URL (bytes come through the
|
|
125
|
-
* invoke channel as base64). An inline `` would open that gap through the back door and
|
|
126
|
-
* tell an external host who opens the document.
|
|
127
|
-
* 2. **Links to a forbidden scheme become plain text** — the label stays. Dropping the label would
|
|
128
|
-
* break the sentence over the link.
|
|
129
|
-
* 3. **Reference definitions to a forbidden scheme disappear.** Without this step `[click][x]`
|
|
130
|
-
* with `[x]: javascript:…` below it escapes step 2.
|
|
131
|
-
*/
|
|
132
|
-
export declare function sanitizeInline(text: string): string;
|
|
133
|
-
/**
|
|
134
|
-
* Prunes a written block list down to something a renderer can safely draw.
|
|
135
|
-
*
|
|
136
|
-
* Always returns **new** objects: the input comes from a model or an API client, and the sanitized
|
|
137
|
-
* text must not leak back into the original.
|
|
138
|
-
*
|
|
139
|
-
* `onDrop` should log or report back to the writer — a tool losing its table because it sent zero
|
|
140
|
-
* columns has to be able to hear that.
|
|
141
|
-
*/
|
|
142
|
-
export declare function normalizeArtifactBlocks(blocks: unknown, onDrop?: (reason: string) => void): ArtifactBlock[];
|
|
143
|
-
/** How many bytes this body costs on the wire. */
|
|
144
|
-
export declare function artifactBodyBytes(blocks: ArtifactBlock[]): number;
|
|
145
|
-
/**
|
|
146
|
-
* A short summary of what is inside, for the thumbnail on a card and for what the assistant sees
|
|
147
|
-
* when it wants to update an artifact without reading the whole body.
|
|
9
|
+
* These aliases exist so `apps/storage` and the AI tools keep compiling unchanged. New code reads
|
|
10
|
+
* the document names.
|
|
148
11
|
*/
|
|
149
|
-
export
|
|
150
|
-
blocks: number;
|
|
151
|
-
types: ArtifactBlockType[];
|
|
152
|
-
headings: string[];
|
|
153
|
-
};
|
|
12
|
+
export { ALLOWED_LINK_SCHEMES, DOCUMENT_MAX_BLOCKS as ARTIFACT_MAX_BLOCKS, DOCUMENT_MAX_BODY_BYTES as ARTIFACT_MAX_BODY_BYTES, DOCUMENT_MAX_CELL_LEN as ARTIFACT_MAX_CELL_LEN, DOCUMENT_MAX_KPI_ITEMS as ARTIFACT_MAX_KPI_ITEMS, DOCUMENT_MAX_LIST_ITEMS as ARTIFACT_MAX_LIST_ITEMS, DOCUMENT_MAX_TABLE_COLUMNS as ARTIFACT_MAX_TABLE_COLUMNS, DOCUMENT_MAX_TABLE_ROWS as ARTIFACT_MAX_TABLE_ROWS, DOCUMENT_MAX_TEXT_LEN as ARTIFACT_MAX_TEXT_LEN, type DocumentBlock as ArtifactBlock, type DocumentBlockType as ArtifactBlockType, type DocumentKpiItem as ArtifactKpiItem, type DocumentListItem as ArtifactListItem, type DocumentTableColumn as ArtifactTableColumn, type DocumentTone as ArtifactTone, documentBodyBytes as artifactBodyBytes, documentOutline as artifactOutline, normalizeDocumentBlocks as normalizeArtifactBlocks, sanitizeInline, } from '../../platform/document-blocks';
|
|
@@ -1,6 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* The whole document as markdown. `title` goes on top as an H1 when the blocks carry none
|
|
4
|
-
* themselves — an export without a title cannot be found back in a downloads folder.
|
|
5
|
-
*/
|
|
6
|
-
export declare function artifactToMarkdown(blocks: ArtifactBlock[], title?: string): string;
|
|
1
|
+
/** See `./blocks`: the serializer moved to `platform/document-markdown.ts` with the vocabulary. */
|
|
2
|
+
export { documentToMarkdown as artifactToMarkdown } from '../../platform/document-markdown';
|
|
@@ -39,6 +39,8 @@ export interface Company {
|
|
|
39
39
|
phone?: string;
|
|
40
40
|
website?: string;
|
|
41
41
|
notes?: string;
|
|
42
|
+
/** The language to write to this company in. The rung under {@link Contact.locale}. */
|
|
43
|
+
locale?: string;
|
|
42
44
|
}
|
|
43
45
|
/** What a resolve returns: which company, and how we got there. */
|
|
44
46
|
export interface CompanyMatch {
|
|
@@ -47,6 +47,14 @@ export interface Contact {
|
|
|
47
47
|
keys?: string[];
|
|
48
48
|
/** Namespaced source references (`ms:AAMk…`, `google:people/c123`, `hubspot:42`). */
|
|
49
49
|
externalIds?: string[];
|
|
50
|
+
/**
|
|
51
|
+
* The language to write to this person in (`nl`, `en`).
|
|
52
|
+
*
|
|
53
|
+
* On the contact and not only on the company, because a Dutch company has a German buyer often
|
|
54
|
+
* enough to matter. Absent means "no answer here" and the reader falls to the next rung — see
|
|
55
|
+
* `recipientLocale`.
|
|
56
|
+
*/
|
|
57
|
+
locale?: string;
|
|
50
58
|
origin?: ContactOrigin;
|
|
51
59
|
/** When a shadow row last saw its source. There is no refresh job; touching refreshes. */
|
|
52
60
|
syncedAt?: number;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which language to write to somebody in, and which template row that means.
|
|
3
|
+
*
|
|
4
|
+
* Three rungs, and the figure is the same one this platform uses everywhere: the most specific
|
|
5
|
+
* answer that exists wins, whole.
|
|
6
|
+
*
|
|
7
|
+
* | Rung | Says |
|
|
8
|
+
* |---|---|
|
|
9
|
+
* | the contact | this person reads German |
|
|
10
|
+
* | the company | this customer is Dutch |
|
|
11
|
+
* | the organisation | what we write by default |
|
|
12
|
+
*
|
|
13
|
+
* Without it somebody picks a language by hand on every letter, and sooner or later a Dutch
|
|
14
|
+
* final notice lands at a German customer.
|
|
15
|
+
*/
|
|
16
|
+
export interface RecipientLocaleSources {
|
|
17
|
+
/** `Contact.locale` — the person. */
|
|
18
|
+
contactLocale?: string;
|
|
19
|
+
/** `Company.locale` — the customer. */
|
|
20
|
+
companyLocale?: string;
|
|
21
|
+
/** The organisation's own language. The floor; there is nothing under it. */
|
|
22
|
+
organizationLocale?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function recipientLocale(sources: RecipientLocaleSources): string | undefined;
|
|
25
|
+
/** The minimum a template row has to carry to be picked. */
|
|
26
|
+
export interface LocalizedTemplate {
|
|
27
|
+
key: string;
|
|
28
|
+
locale?: string;
|
|
29
|
+
archived?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The template for this key in this language, or the nearest thing that exists.
|
|
33
|
+
*
|
|
34
|
+
* A reminder written only in Dutch still has to go out to a German customer — in Dutch. Returning
|
|
35
|
+
* nothing there would mean no letter at all, which is worse than the wrong language. The order is
|
|
36
|
+
* therefore: the language asked for, the organisation's own, then whatever is there.
|
|
37
|
+
*/
|
|
38
|
+
export declare function pickTemplate<T extends LocalizedTemplate>(templates: readonly T[], key: string, locale?: string, organizationLocale?: string): T | undefined;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { DocumentBlock } from '../../platform/document-blocks';
|
|
2
|
+
import { TemplateCondition } from './types';
|
|
3
|
+
/** A flat or nested plain object. Arrays resolve by index: `regels.0.naam`. */
|
|
4
|
+
export type TemplateContext = Record<string, unknown>;
|
|
5
|
+
/** Walk a dotted path. Returns `undefined` for anything the path does not reach. */
|
|
6
|
+
export declare function lookup(context: TemplateContext, path: string): unknown;
|
|
7
|
+
export interface FillResult {
|
|
8
|
+
text: string;
|
|
9
|
+
/** Paths that resolved to nothing, in the order they appeared. Deduplicated. */
|
|
10
|
+
unresolved: string[];
|
|
11
|
+
}
|
|
12
|
+
/** One string, filled in. */
|
|
13
|
+
export declare function fillText(text: string, context: TemplateContext): FillResult;
|
|
14
|
+
/** Does this block appear? A block with no condition always does. */
|
|
15
|
+
export declare function conditionHolds(condition: TemplateCondition | undefined, context: TemplateContext): boolean;
|
|
16
|
+
export interface FillDocumentResult {
|
|
17
|
+
blocks: DocumentBlock[];
|
|
18
|
+
unresolved: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A whole template, filled in: conditions applied, tokens replaced everywhere text lives.
|
|
22
|
+
*
|
|
23
|
+
* Every text-bearing field is walked, not just `text` — a token in a table cell or a letterhead
|
|
24
|
+
* value is the ordinary case for an invoice, and missing one would leave `{factuur.nummer}`
|
|
25
|
+
* printed on a document that went out the door.
|
|
26
|
+
*/
|
|
27
|
+
export declare function fillDocument(blocks: readonly DocumentBlock[], context: TemplateContext, conditions?: Record<string, TemplateCondition>): FillDocumentResult;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { DocumentBlock } from '../../platform/document-blocks';
|
|
2
|
+
/**
|
|
3
|
+
* A document template: the blocks a generated document is built from.
|
|
4
|
+
*
|
|
5
|
+
* A work order, a quote, a first reminder, a final notice. What it is **not** is the letterhead
|
|
6
|
+
* and the footer — those are the organisation's house style, drawn around every document, so a
|
|
7
|
+
* change to the address does not mean editing six templates.
|
|
8
|
+
*
|
|
9
|
+
* See `plans/DOCUMENTS-2026-09-14.md` §7.
|
|
10
|
+
*/
|
|
11
|
+
export interface DocumentTemplate {
|
|
12
|
+
id: string;
|
|
13
|
+
organizationId: string;
|
|
14
|
+
/** `quote`, `invoice`, `reminder_1`, `reminder_final`, `work_order`… Unique per language. */
|
|
15
|
+
key: string;
|
|
16
|
+
name: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Which document kind may use it. Absent = free-standing (a letter, a work order).
|
|
20
|
+
*
|
|
21
|
+
* A string and not a union: the kinds live in `apps/sales` and this app must not need to know
|
|
22
|
+
* them to store one.
|
|
23
|
+
*/
|
|
24
|
+
documentKindKey?: string;
|
|
25
|
+
/** The body. Plain document blocks — the same thing the editor and the renderer already speak. */
|
|
26
|
+
blocks: DocumentBlock[];
|
|
27
|
+
/**
|
|
28
|
+
* Which blocks only appear sometimes, keyed by `block_id`.
|
|
29
|
+
*
|
|
30
|
+
* **Beside the blocks, not wrapped around them.** A condition is part of the template, not of
|
|
31
|
+
* the document: keeping it out of the block means the editor, the renderer and the markdown
|
|
32
|
+
* export need to know nothing about it, and a template's body stays a thing any of them can
|
|
33
|
+
* open. A block with no entry here always appears.
|
|
34
|
+
*/
|
|
35
|
+
conditions?: Record<string, TemplateCondition>;
|
|
36
|
+
/**
|
|
37
|
+
* The language this template is written in.
|
|
38
|
+
*
|
|
39
|
+
* A Dutch reminder and its English version are **two rows**, tied by
|
|
40
|
+
* {@link DocumentTemplate.translationGroupId} — exactly how `KbArticle` does it, and for the
|
|
41
|
+
* same reason: a per-key text map cannot carry a translated table, and a translated list is not
|
|
42
|
+
* a string. The price is that the structure can drift between languages; the alternative is a
|
|
43
|
+
* mechanism that cannot express half the documents.
|
|
44
|
+
*/
|
|
45
|
+
locale?: string;
|
|
46
|
+
/** Ties the language versions together. Defaults to the template's own id. */
|
|
47
|
+
translationGroupId?: string;
|
|
48
|
+
isDefault?: boolean;
|
|
49
|
+
archived?: boolean;
|
|
50
|
+
createdBy: string;
|
|
51
|
+
createdAt?: number;
|
|
52
|
+
updatedAt?: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* When a block appears.
|
|
56
|
+
*
|
|
57
|
+
* Three operators and no expression language. "Show the direct-debit paragraph when the customer
|
|
58
|
+
* pays by direct debit" is the whole job; anything past that is a rule, and a rule belongs in the
|
|
59
|
+
* app that decides it.
|
|
60
|
+
*/
|
|
61
|
+
export interface TemplateCondition {
|
|
62
|
+
/** A path into the same context the tokens read: `klant.incasso`. */
|
|
63
|
+
key: string;
|
|
64
|
+
/** Show when the value equals this. */
|
|
65
|
+
equals?: unknown;
|
|
66
|
+
/** Show when the value is present (`true`) or absent (`false`). */
|
|
67
|
+
exists?: boolean;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The organisation's paper: what is drawn around every document, whichever template made it.
|
|
71
|
+
*
|
|
72
|
+
* Three fields, and the shortness is the point. The logo, the address and the VAT/KvK numbers are
|
|
73
|
+
* **not** here — they are already on the organisation (`logo`, `address`, `billing`), and a second
|
|
74
|
+
* copy is the one that goes stale. What is left is what a document needs and nothing here had.
|
|
75
|
+
*/
|
|
76
|
+
export interface OrganizationHouseStyle {
|
|
77
|
+
/** Hex, for the rules and the heading accents. */
|
|
78
|
+
accentColor?: string;
|
|
79
|
+
/** The line under every document. */
|
|
80
|
+
footerText?: string;
|
|
81
|
+
/**
|
|
82
|
+
* The account to pay into. The one identifier a letter needs that this platform did not already
|
|
83
|
+
* have — the VAT and chamber-of-commerce numbers live on `Organization.billing`, and a second
|
|
84
|
+
* copy is the one that goes stale.
|
|
85
|
+
*/
|
|
86
|
+
iban?: string;
|
|
87
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DocumentBlock } from '../../platform/document-blocks';
|
|
1
2
|
/** A language a knowledge base publishes in, e.g. `nl`, `en`, `de`. */
|
|
2
3
|
export type KbLocale = string;
|
|
3
4
|
export interface KnowledgeBase {
|
|
@@ -62,7 +63,15 @@ export interface KbArticle {
|
|
|
62
63
|
knowledgeBaseId: string;
|
|
63
64
|
categoryId?: string;
|
|
64
65
|
title: string;
|
|
66
|
+
/**
|
|
67
|
+
* The article as document blocks — the source of truth. Absent on a row written before the
|
|
68
|
+
* store moved to blocks; readers fall back to parsing {@link KbArticle.body}.
|
|
69
|
+
*/
|
|
70
|
+
blocks?: DocumentBlock[];
|
|
71
|
+
/** The same article as markdown. Derived on write, never parsed back. */
|
|
65
72
|
body: string;
|
|
73
|
+
/** The same article as plain text: what the embedding and the keyword score read. */
|
|
74
|
+
text?: string;
|
|
66
75
|
status: "draft" | "published";
|
|
67
76
|
authorId?: string;
|
|
68
77
|
order: number;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OrganizationHouseStyle } from '../document-template/types';
|
|
1
2
|
import { OrganizationProfile } from './profile';
|
|
2
3
|
export interface OrganizationAddress {
|
|
3
4
|
street?: string;
|
|
@@ -40,6 +41,13 @@ export interface Organization {
|
|
|
40
41
|
* which steps apply and what to propose. See `profile.ts`.
|
|
41
42
|
*/
|
|
42
43
|
profile?: OrganizationProfile;
|
|
44
|
+
/**
|
|
45
|
+
* What is drawn around every generated document — accent, footer, KvK/VAT/IBAN.
|
|
46
|
+
*
|
|
47
|
+
* The logo and the address are not in here: they are fields of their own on this record, and a
|
|
48
|
+
* document reads them from there. See `OrganizationHouseStyle`.
|
|
49
|
+
*/
|
|
50
|
+
houseStyle?: OrganizationHouseStyle;
|
|
43
51
|
/** Small company logo stored inline as a base64 data-URI. */
|
|
44
52
|
logo?: string;
|
|
45
53
|
/**
|
|
@@ -48,8 +48,14 @@ export interface CapacitySources {
|
|
|
48
48
|
*/
|
|
49
49
|
export declare function capacityTotal(range: DateRange, sources: CapacitySources): CapacityTotal;
|
|
50
50
|
export interface PersonCoverage {
|
|
51
|
-
/**
|
|
52
|
-
|
|
51
|
+
/**
|
|
52
|
+
* **Absent** is the pot nobody has taken yet — what is still to be divided.
|
|
53
|
+
*
|
|
54
|
+
* Absent and not `null`: a `null` in a response arrives at the client as `[]`, which is
|
|
55
|
+
* truthy, so `row.userId ? …` took the wrong branch and handed an array to everything that
|
|
56
|
+
* expected a name. An omitted key survives the wire as an omitted key.
|
|
57
|
+
*/
|
|
58
|
+
userId?: string;
|
|
53
59
|
requiredSeconds: number;
|
|
54
60
|
capacitySeconds: number;
|
|
55
61
|
/** `capacity − required`. Negative = this person is over-committed. */
|
|
@@ -65,5 +71,42 @@ export interface PersonCoverage {
|
|
|
65
71
|
* Work nobody has taken stays a pot of its own rather than being divided over the team: it is
|
|
66
72
|
* precisely what is still to be assigned, and pretending it is already somebody's would erase the
|
|
67
73
|
* one thing that is actionable.
|
|
74
|
+
*
|
|
75
|
+
* > **A source that names nobody gets no answer at all — not a table of zeroes.**
|
|
76
|
+
* > A queue or a fixed staffing level is demand on a *team*: four hundred conversations are not
|
|
77
|
+
* > Daan's and not Iris's, and they never will be. Answering anyway produced a row per rostered
|
|
78
|
+
* > person reading `0 / 8u` and a pot claiming the whole week was "still to be divided", which is
|
|
79
|
+
* > not an empty answer but a false one. Only a source that attributes *some* work to *somebody*
|
|
80
|
+
* > — a sprint with assignees, where an unassigned item genuinely is still to be handed out — can
|
|
81
|
+
* > be asked this question.
|
|
68
82
|
*/
|
|
69
83
|
export declare function coverageByPerson(points: readonly DemandPointResponse[], capacity: CapacityTotal): PersonCoverage[];
|
|
84
|
+
export interface PersonAvailability {
|
|
85
|
+
userId: string;
|
|
86
|
+
/** Hours this person has in the window, by the ladder, after approved absence. */
|
|
87
|
+
capacitySeconds: number;
|
|
88
|
+
/** Of that, already earmarked for this workstream. */
|
|
89
|
+
onThisSeconds: number;
|
|
90
|
+
/** Of that, spoken for by something else: another workstream, a meeting, training, travel. */
|
|
91
|
+
elsewhereSeconds: number;
|
|
92
|
+
/** `capacity − onThis − elsewhere`, floored at zero. What a planner can still hand out. */
|
|
93
|
+
freeSeconds: number;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Who is still free, and for how long.
|
|
97
|
+
*
|
|
98
|
+
* The other half of the planner's question, and the one `coverageByPerson` cannot answer. That
|
|
99
|
+
* one asks *who has too much work* — which needs a source that names people, so a queue or a
|
|
100
|
+
* fixed staffing level has no answer at all. This one asks *who is left*, and every source can
|
|
101
|
+
* answer it, because it reads the roster rather than the demand.
|
|
102
|
+
*
|
|
103
|
+
* > **A shift makes time; it does not spend it — unless it is earmarked.**
|
|
104
|
+
* > An unearmarked eight-hour shift is eight hours a planner can still fill. The same shift with
|
|
105
|
+
* > a workstream on it is eight hours already promised. Everything that is not a shift and not
|
|
106
|
+
* > absence spends time whatever it is: a meeting, training, travel, a break.
|
|
107
|
+
*
|
|
108
|
+
* Absence never appears here as spent time. It has already been taken off the capacity by
|
|
109
|
+
* `capacityTotal`, and subtracting it twice is the shrinkage mistake this file keeps warning
|
|
110
|
+
* about — a day of leave would read as minus eight hours of freedom on a day with none.
|
|
111
|
+
*/
|
|
112
|
+
export declare function availabilityByPerson(entries: readonly ScheduleEntry[], workstreamId: string, range: DateRange, capacity: CapacityTotal): PersonAvailability[];
|