@hailer/mcp 1.1.12 → 1.1.13
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/CHANGELOG.md +0 -7
- package/{.claude → dist}/CLAUDE.md +2 -2
- package/dist/app.js +18 -5
- package/dist/bot/bot-config.d.ts +10 -1
- package/dist/bot/bot-config.js +64 -3
- package/dist/bot/bot-manager.d.ts +2 -0
- package/dist/bot/bot-manager.js +9 -2
- package/dist/bot/bot.d.ts +33 -0
- package/dist/bot/bot.js +461 -160
- package/dist/bot/services/message-classifier.js +17 -0
- package/dist/bot/services/permission-guard.d.ts +52 -0
- package/dist/bot/services/permission-guard.js +149 -0
- package/dist/bot/services/types.d.ts +5 -0
- package/dist/bot/services/typing-indicator.d.ts +6 -1
- package/dist/bot/services/typing-indicator.js +19 -3
- package/dist/cli.js +0 -0
- package/dist/config.d.ts +6 -1
- package/dist/config.js +43 -0
- package/dist/core.js +3 -6
- package/dist/lib/discussion-lock.d.ts +42 -0
- package/dist/lib/discussion-lock.js +110 -0
- package/dist/mcp/UserContextCache.d.ts +5 -0
- package/dist/mcp/UserContextCache.js +51 -19
- package/dist/mcp/hailer-clients.d.ts +19 -1
- package/dist/mcp/hailer-clients.js +158 -24
- package/dist/mcp/session-store.d.ts +68 -0
- package/dist/mcp/session-store.js +169 -0
- package/dist/mcp/signal-handler.js +2 -0
- package/dist/mcp/tool-registry.d.ts +17 -4
- package/dist/mcp/tool-registry.js +37 -7
- package/dist/mcp/tools/activity.js +99 -7
- package/dist/mcp/tools/app-scaffold.js +304 -336
- package/dist/mcp/tools/bot-config/constants.d.ts +23 -0
- package/dist/mcp/tools/bot-config/constants.js +94 -0
- package/dist/mcp/tools/bot-config/core.d.ts +253 -0
- package/dist/mcp/tools/bot-config/core.js +2456 -0
- package/dist/mcp/tools/bot-config/index.d.ts +10 -0
- package/dist/mcp/tools/bot-config/index.js +59 -0
- package/dist/mcp/tools/bot-config/tools.d.ts +7 -0
- package/dist/mcp/tools/bot-config/tools.js +15 -0
- package/dist/mcp/tools/bot-config/types.d.ts +50 -0
- package/dist/mcp/tools/bot-config/types.js +6 -0
- package/dist/mcp/tools/bug-fixer-tools.d.ts +45 -0
- package/dist/mcp/tools/bug-fixer-tools.js +1096 -0
- package/dist/mcp/tools/company.d.ts +9 -0
- package/dist/mcp/tools/company.js +88 -0
- package/dist/mcp/tools/discussion.js +68 -0
- package/dist/mcp/tools/document.d.ts +11 -0
- package/dist/mcp/tools/document.js +741 -0
- package/dist/mcp/tools/investigate.d.ts +9 -0
- package/dist/mcp/tools/investigate.js +254 -0
- package/dist/mcp/tools/workflow-permissions.d.ts +15 -0
- package/dist/mcp/tools/workflow-permissions.js +204 -0
- package/dist/mcp/tools/workflow.js +57 -18
- package/dist/mcp/utils/index.d.ts +2 -0
- package/dist/mcp/utils/index.js +12 -1
- package/dist/mcp/utils/role-utils.d.ts +74 -0
- package/dist/mcp/utils/role-utils.js +151 -0
- package/dist/mcp/utils/types.d.ts +43 -1
- package/dist/mcp/utils/types.js +14 -0
- package/dist/mcp/webhook-handler.d.ts +4 -0
- package/dist/mcp/webhook-handler.js +8 -0
- package/dist/mcp-server.d.ts +23 -2
- package/dist/mcp-server.js +639 -127
- package/dist/plugins/vipunen/client.d.ts +150 -0
- package/dist/plugins/vipunen/client.js +535 -0
- package/dist/plugins/vipunen/config/schema-config.json +19 -0
- package/dist/plugins/vipunen/config/schema-doc.json +22 -0
- package/dist/plugins/vipunen/index.d.ts +41 -0
- package/dist/plugins/vipunen/index.js +88 -0
- package/dist/plugins/vipunen/tools.d.ts +26 -0
- package/dist/plugins/vipunen/tools.js +501 -0
- package/dist/stdio-server.d.ts +14 -0
- package/dist/stdio-server.js +101 -0
- package/package.json +2 -1
- package/.claude/agents/agent-ada-skill-builder.md +0 -94
- package/.claude/agents/agent-alejandro-function-fields.md +0 -342
- package/.claude/agents/agent-bjorn-config-audit.md +0 -103
- package/.claude/agents/agent-builder-agent-creator.md +0 -130
- package/.claude/agents/agent-code-simplifier.md +0 -53
- package/.claude/agents/agent-dmitri-activity-crud.md +0 -159
- package/.claude/agents/agent-giuseppe-app-builder.md +0 -247
- package/.claude/agents/agent-gunther-mcp-tools.md +0 -39
- package/.claude/agents/agent-helga-workflow-config.md +0 -204
- package/.claude/agents/agent-igor-activity-mover-automation.md +0 -125
- package/.claude/agents/agent-ingrid-doc-templates.md +0 -261
- package/.claude/agents/agent-ivan-monolith.md +0 -154
- package/.claude/agents/agent-kenji-data-reader.md +0 -86
- package/.claude/agents/agent-lars-code-inspector.md +0 -102
- package/.claude/agents/agent-marco-mockup-builder.md +0 -110
- package/.claude/agents/agent-marcus-api-documenter.md +0 -323
- package/.claude/agents/agent-marketplace-publisher.md +0 -280
- package/.claude/agents/agent-marketplace-reviewer.md +0 -309
- package/.claude/agents/agent-permissions-handler.md +0 -208
- package/.claude/agents/agent-simple-writer.md +0 -48
- package/.claude/agents/agent-svetlana-code-review.md +0 -171
- package/.claude/agents/agent-tanya-test-runner.md +0 -333
- package/.claude/agents/agent-ui-designer.md +0 -100
- package/.claude/agents/agent-viktor-sql-insights.md +0 -212
- package/.claude/agents/agent-web-search.md +0 -55
- package/.claude/agents/agent-yevgeni-discussions.md +0 -45
- package/.claude/agents/agent-zara-zapier.md +0 -159
- package/.claude/commands/app-squad.md +0 -135
- package/.claude/commands/audit-squad.md +0 -158
- package/.claude/commands/autoplan.md +0 -563
- package/.claude/commands/cleanup-squad.md +0 -98
- package/.claude/commands/config-squad.md +0 -106
- package/.claude/commands/crud-squad.md +0 -87
- package/.claude/commands/data-squad.md +0 -97
- package/.claude/commands/debug-squad.md +0 -303
- package/.claude/commands/doc-squad.md +0 -65
- package/.claude/commands/handoff.md +0 -137
- package/.claude/commands/health.md +0 -49
- package/.claude/commands/help.md +0 -29
- package/.claude/commands/help:agents.md +0 -151
- package/.claude/commands/help:commands.md +0 -78
- package/.claude/commands/help:faq.md +0 -79
- package/.claude/commands/help:plugins.md +0 -50
- package/.claude/commands/help:skills.md +0 -93
- package/.claude/commands/help:tools.md +0 -75
- package/.claude/commands/hotfix-squad.md +0 -112
- package/.claude/commands/integration-squad.md +0 -82
- package/.claude/commands/janitor-squad.md +0 -167
- package/.claude/commands/learn-auto.md +0 -120
- package/.claude/commands/learn.md +0 -120
- package/.claude/commands/mcp-list.md +0 -27
- package/.claude/commands/onboard-squad.md +0 -140
- package/.claude/commands/plan-workspace.md +0 -732
- package/.claude/commands/prd.md +0 -130
- package/.claude/commands/project-status.md +0 -82
- package/.claude/commands/publish.md +0 -138
- package/.claude/commands/recap.md +0 -69
- package/.claude/commands/restore.md +0 -64
- package/.claude/commands/review-squad.md +0 -152
- package/.claude/commands/save.md +0 -24
- package/.claude/commands/stats.md +0 -19
- package/.claude/commands/swarm.md +0 -210
- package/.claude/commands/tool-builder.md +0 -39
- package/.claude/commands/ws-pull.md +0 -44
- package/.claude/hooks/_shared-memory.cjs +0 -305
- package/.claude/hooks/_utils.cjs +0 -108
- package/.claude/hooks/agent-failure-detector.cjs +0 -383
- package/.claude/hooks/agent-usage-logger.cjs +0 -204
- package/.claude/hooks/app-edit-guard.cjs +0 -494
- package/.claude/hooks/auto-learn.cjs +0 -304
- package/.claude/hooks/bash-guard.cjs +0 -272
- package/.claude/hooks/builder-mode-manager.cjs +0 -354
- package/.claude/hooks/bulk-activity-guard.cjs +0 -271
- package/.claude/hooks/context-watchdog.cjs +0 -230
- package/.claude/hooks/delegation-reminder.cjs +0 -465
- package/.claude/hooks/design-system-lint.cjs +0 -271
- package/.claude/hooks/post-scaffold-hook.cjs +0 -181
- package/.claude/hooks/prompt-guard.cjs +0 -354
- package/.claude/hooks/publish-template-guard.cjs +0 -147
- package/.claude/hooks/session-start.cjs +0 -35
- package/.claude/hooks/shared-memory-writer.cjs +0 -147
- package/.claude/hooks/skill-injector.cjs +0 -140
- package/.claude/hooks/skill-usage-logger.cjs +0 -258
- package/.claude/hooks/src-edit-guard.cjs +0 -240
- package/.claude/hooks/sync-marketplace-agents.cjs +0 -346
- package/.claude/settings.json +0 -257
- package/.claude/skills/SDK-activity-patterns/SKILL.md +0 -428
- package/.claude/skills/SDK-document-templates/SKILL.md +0 -1033
- package/.claude/skills/SDK-function-fields/SKILL.md +0 -542
- package/.claude/skills/SDK-generate-skill/SKILL.md +0 -92
- package/.claude/skills/SDK-init-skill/SKILL.md +0 -127
- package/.claude/skills/SDK-insight-queries/SKILL.md +0 -787
- package/.claude/skills/SDK-ws-config-skill/SKILL.md +0 -1139
- package/.claude/skills/agent-structure/SKILL.md +0 -98
- package/.claude/skills/api-documentation-patterns/SKILL.md +0 -474
- package/.claude/skills/chrome-mcp-reference/SKILL.md +0 -370
- package/.claude/skills/delegation-routing/SKILL.md +0 -202
- package/.claude/skills/frontend-design/SKILL.md +0 -254
- package/.claude/skills/hailer-activity-mover/SKILL.md +0 -213
- package/.claude/skills/hailer-api-client/SKILL.md +0 -518
- package/.claude/skills/hailer-app-builder/SKILL.md +0 -1434
- package/.claude/skills/hailer-apps-pictures/SKILL.md +0 -269
- package/.claude/skills/hailer-design-system/SKILL.md +0 -235
- package/.claude/skills/hailer-monolith-automations/SKILL.md +0 -686
- package/.claude/skills/hailer-permissions-system/SKILL.md +0 -121
- package/.claude/skills/hailer-project-protocol/SKILL.md +0 -488
- package/.claude/skills/hailer-rest-api/SKILL.md +0 -61
- package/.claude/skills/hailer-rest-api/hailer-activities.md +0 -184
- package/.claude/skills/hailer-rest-api/hailer-admin.md +0 -473
- package/.claude/skills/hailer-rest-api/hailer-calendar.md +0 -256
- package/.claude/skills/hailer-rest-api/hailer-feed.md +0 -249
- package/.claude/skills/hailer-rest-api/hailer-insights.md +0 -195
- package/.claude/skills/hailer-rest-api/hailer-messaging.md +0 -276
- package/.claude/skills/hailer-rest-api/hailer-workflows.md +0 -283
- package/.claude/skills/insight-join-patterns/SKILL.md +0 -174
- package/.claude/skills/integration-patterns/SKILL.md +0 -421
- package/.claude/skills/json-only-output/SKILL.md +0 -72
- package/.claude/skills/lsp-setup/SKILL.md +0 -160
- package/.claude/skills/mcp-direct-tools/SKILL.md +0 -153
- package/.claude/skills/optional-parameters/SKILL.md +0 -72
- package/.claude/skills/publish-hailer-app/SKILL.md +0 -244
- package/.claude/skills/testing-patterns/SKILL.md +0 -630
- package/.claude/skills/tool-builder/SKILL.md +0 -250
- package/.claude/skills/tool-parameter-usage/SKILL.md +0 -126
- package/.claude/skills/tool-response-verification/SKILL.md +0 -92
- package/.claude/skills/zapier-hailer-patterns/SKILL.md +0 -581
- package/.mcp.json +0 -13
- package/.opencode/agent/agent-ada-skill-builder.md +0 -35
- package/.opencode/agent/agent-alejandro-function-fields.md +0 -39
- package/.opencode/agent/agent-bjorn-config-audit.md +0 -36
- package/.opencode/agent/agent-builder-agent-creator.md +0 -39
- package/.opencode/agent/agent-code-simplifier.md +0 -31
- package/.opencode/agent/agent-dmitri-activity-crud.md +0 -40
- package/.opencode/agent/agent-giuseppe-app-builder.md +0 -37
- package/.opencode/agent/agent-gunther-mcp-tools.md +0 -39
- package/.opencode/agent/agent-helga-workflow-config.md +0 -203
- package/.opencode/agent/agent-igor-activity-mover-automation.md +0 -46
- package/.opencode/agent/agent-ingrid-doc-templates.md +0 -39
- package/.opencode/agent/agent-ivan-monolith.md +0 -46
- package/.opencode/agent/agent-kenji-data-reader.md +0 -53
- package/.opencode/agent/agent-lars-code-inspector.md +0 -28
- package/.opencode/agent/agent-marco-mockup-builder.md +0 -42
- package/.opencode/agent/agent-marcus-api-documenter.md +0 -53
- package/.opencode/agent/agent-marketplace-publisher.md +0 -44
- package/.opencode/agent/agent-marketplace-reviewer.md +0 -42
- package/.opencode/agent/agent-permissions-handler.md +0 -50
- package/.opencode/agent/agent-simple-writer.md +0 -45
- package/.opencode/agent/agent-svetlana-code-review.md +0 -39
- package/.opencode/agent/agent-tanya-test-runner.md +0 -57
- package/.opencode/agent/agent-ui-designer.md +0 -56
- package/.opencode/agent/agent-viktor-sql-insights.md +0 -34
- package/.opencode/agent/agent-web-search.md +0 -42
- package/.opencode/agent/agent-yevgeni-discussions.md +0 -37
- package/.opencode/agent/agent-zara-zapier.md +0 -53
- package/.opencode/commands/app-squad.md +0 -135
- package/.opencode/commands/audit-squad.md +0 -158
- package/.opencode/commands/autoplan.md +0 -563
- package/.opencode/commands/cleanup-squad.md +0 -98
- package/.opencode/commands/config-squad.md +0 -106
- package/.opencode/commands/crud-squad.md +0 -87
- package/.opencode/commands/data-squad.md +0 -97
- package/.opencode/commands/debug-squad.md +0 -303
- package/.opencode/commands/doc-squad.md +0 -65
- package/.opencode/commands/handoff.md +0 -137
- package/.opencode/commands/health.md +0 -49
- package/.opencode/commands/help-agents.md +0 -151
- package/.opencode/commands/help-commands.md +0 -32
- package/.opencode/commands/help-faq.md +0 -29
- package/.opencode/commands/help-plugins.md +0 -28
- package/.opencode/commands/help-skills.md +0 -7
- package/.opencode/commands/help-tools.md +0 -40
- package/.opencode/commands/help.md +0 -28
- package/.opencode/commands/hotfix-squad.md +0 -112
- package/.opencode/commands/integration-squad.md +0 -82
- package/.opencode/commands/janitor-squad.md +0 -167
- package/.opencode/commands/learn-auto.md +0 -120
- package/.opencode/commands/learn.md +0 -120
- package/.opencode/commands/mcp-list.md +0 -27
- package/.opencode/commands/onboard-squad.md +0 -140
- package/.opencode/commands/plan-workspace.md +0 -732
- package/.opencode/commands/prd.md +0 -131
- package/.opencode/commands/project-status.md +0 -82
- package/.opencode/commands/publish.md +0 -138
- package/.opencode/commands/recap.md +0 -69
- package/.opencode/commands/restore.md +0 -64
- package/.opencode/commands/review-squad.md +0 -152
- package/.opencode/commands/save.md +0 -24
- package/.opencode/commands/stats.md +0 -19
- package/.opencode/commands/swarm.md +0 -210
- package/.opencode/commands/tool-builder.md +0 -39
- package/.opencode/commands/ws-pull.md +0 -44
- package/.opencode/opencode.json +0 -28
- package/SESSION-HANDOFF.md +0 -68
- package/inbox/2026-03-04-bot-config-patterns.md +0 -24
- package/scripts/postinstall.cjs +0 -64
- package/scripts/test-hal-tools.ts +0 -154
|
@@ -1,1033 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: SDK-document-templates
|
|
3
|
-
description: PDF/CSV document template patterns - field mappings, pdfmake structure, generation code
|
|
4
|
-
version: 1.2.0
|
|
5
|
-
triggers: Document template, PDF generation, CSV export, pdfmake, field mapping
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# Document Template Patterns
|
|
9
|
-
|
|
10
|
-
## File Structure
|
|
11
|
-
|
|
12
|
-
```
|
|
13
|
-
workspace/[Workflow]_[id]/
|
|
14
|
-
├── templates.ts # Template registry
|
|
15
|
-
└── templates/[TemplateName]_[id]/
|
|
16
|
-
├── template.config.ts # Field mappings, options
|
|
17
|
-
└── template.code.ts # Generation function (pdfmake)
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
## Template Lifecycle
|
|
23
|
-
|
|
24
|
-
### Creating New Template
|
|
25
|
-
|
|
26
|
-
```
|
|
27
|
-
1. npm run pull
|
|
28
|
-
2. Edit templates.ts:
|
|
29
|
-
{ templateId: "", name: "Invoice", fileType: "pdf", folder: "" }
|
|
30
|
-
3. Return ["npm run templates-sync:force"]
|
|
31
|
-
4. npm run pull (generates template folder + files)
|
|
32
|
-
5. Edit template.config.ts (field mappings)
|
|
33
|
-
6. Edit template.code.ts (generation logic)
|
|
34
|
-
7. Return ["npm run templates-push"]
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
### Updating Template
|
|
38
|
-
|
|
39
|
-
```
|
|
40
|
-
1. npm run pull
|
|
41
|
-
2. Edit template.config.ts or template.code.ts
|
|
42
|
-
3. Return ["npm run templates-push"]
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
---
|
|
46
|
-
|
|
47
|
-
## CRITICAL: Zero vs Null Filtering
|
|
48
|
-
|
|
49
|
-
**#1 PDF gotcha:** Numeric fields may have `0` as a meaningful value OR as an empty placeholder. Filter logic that uses truthiness (`if (value)`) will hide valid zeros.
|
|
50
|
-
|
|
51
|
-
**Core helper function:**
|
|
52
|
-
```typescript
|
|
53
|
-
function shouldDisplayValue(value: unknown): boolean {
|
|
54
|
-
if (value === null || value === undefined) return false;
|
|
55
|
-
if (value === '') return false;
|
|
56
|
-
if (typeof value === 'number' && isNaN(value)) return false;
|
|
57
|
-
if (value === 0) return true; // KEEP meaningful zeros!
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Usage in PDFs
|
|
62
|
-
if (shouldDisplayValue(fields.quantity?.value)) {
|
|
63
|
-
// Displays: 0 ✓ (meaningful zero)
|
|
64
|
-
// Hides: null, undefined, NaN ✓
|
|
65
|
-
}
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
For field-type-specific logic, see **"Distinguishing Meaningful Zero from Empty/Null"** section below.
|
|
69
|
-
|
|
70
|
-
---
|
|
71
|
-
|
|
72
|
-
## templates.ts (Registry)
|
|
73
|
-
|
|
74
|
-
```typescript
|
|
75
|
-
export const templates: DocumentTemplateEntry[] = [
|
|
76
|
-
// NEW template - empty templateId
|
|
77
|
-
{
|
|
78
|
-
templateId: "",
|
|
79
|
-
name: "Invoice",
|
|
80
|
-
fileType: "pdf",
|
|
81
|
-
folder: ""
|
|
82
|
-
},
|
|
83
|
-
// EXISTING template
|
|
84
|
-
{
|
|
85
|
-
templateId: "6932a64fb1353d454b74d743",
|
|
86
|
-
name: "Invoice",
|
|
87
|
-
fileType: "pdf",
|
|
88
|
-
folder: "invoice_6932a64fb1353d454b74d743"
|
|
89
|
-
}
|
|
90
|
-
];
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
---
|
|
94
|
-
|
|
95
|
-
## template.config.ts (Field Mappings)
|
|
96
|
-
|
|
97
|
-
```typescript
|
|
98
|
-
import { Invoice_FieldIds, Customer_FieldIds } from "../../enums";
|
|
99
|
-
|
|
100
|
-
export const template: DocumentTemplateUpdatePayload = {
|
|
101
|
-
content: "@function:invoice_abc",
|
|
102
|
-
name: "Invoice Template",
|
|
103
|
-
fileType: "pdf",
|
|
104
|
-
fieldMap: {
|
|
105
|
-
fields: {
|
|
106
|
-
invoiceNumber: {
|
|
107
|
-
label: "Invoice Number",
|
|
108
|
-
description: "The invoice reference",
|
|
109
|
-
value: `::${Invoice_FieldIds.invoice_number_abc}`
|
|
110
|
-
},
|
|
111
|
-
customerName: {
|
|
112
|
-
label: "Customer",
|
|
113
|
-
description: "Customer name from linked activity",
|
|
114
|
-
value: `::${Invoice_FieldIds.customer_link_def}/::${Customer_FieldIds.name_ghi}`
|
|
115
|
-
},
|
|
116
|
-
amount: {
|
|
117
|
-
label: "Total Amount",
|
|
118
|
-
value: `::${Invoice_FieldIds.total_amount_jkl}`
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
images: {
|
|
122
|
-
logo: {
|
|
123
|
-
description: "Company logo",
|
|
124
|
-
value: "67640282d1346d04eacf4b05" // Image ID from Hailer
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
|
-
activityLinks: {
|
|
128
|
-
customer: {
|
|
129
|
-
label: "Customer",
|
|
130
|
-
description: "Linked customer",
|
|
131
|
-
field: Invoice_FieldIds.customer_link_def,
|
|
132
|
-
process: WorkflowIds.customers_mno
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
opts: {
|
|
137
|
-
formatFieldValue: true,
|
|
138
|
-
decimalSeparator: ",",
|
|
139
|
-
thousandSeparator: " ",
|
|
140
|
-
toFixedDigits: "2",
|
|
141
|
-
toFixedIds: `${Invoice_FieldIds.total_amount_jkl}`
|
|
142
|
-
},
|
|
143
|
-
templateId: "existing_id_or_empty"
|
|
144
|
-
};
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### Field Value Syntax
|
|
148
|
-
|
|
149
|
-
| Pattern | Description | Example |
|
|
150
|
-
|---------|-------------|---------|
|
|
151
|
-
| `::fieldId` | Field from current activity | `::${Invoice_FieldIds.number_abc}` |
|
|
152
|
-
| `::fieldId/::otherFieldId` | Field from linked activity | `::${Invoice_FieldIds.customer_def}/::${Customer_FieldIds.name_ghi}` |
|
|
153
|
-
| `::fieldId/::name` | **Linked activity's name** | `::${Invoice_FieldIds.customer_def}/::name` |
|
|
154
|
-
| `::name` | Activity name | `"::name"` |
|
|
155
|
-
| Static text | Literal value | `"Fixed text here"` |
|
|
156
|
-
|
|
157
|
-
**ActivityLink → Name shortcut:** To get a linked activity's name directly, use `/::name` suffix:
|
|
158
|
-
```typescript
|
|
159
|
-
customerName: {
|
|
160
|
-
label: "Customer",
|
|
161
|
-
value: `::${Invoice_FieldIds.customer_link}/::name` // Returns linked activity's name string
|
|
162
|
-
}
|
|
163
|
-
```
|
|
164
|
-
Without `/::name`, you get the raw object/ID which won't display properly.
|
|
165
|
-
|
|
166
|
-
**CRITICAL:** Use template literals with `${}`:
|
|
167
|
-
```typescript
|
|
168
|
-
// ✅ Correct
|
|
169
|
-
value: `::${Invoice_FieldIds.number_abc}`
|
|
170
|
-
|
|
171
|
-
// ❌ Wrong
|
|
172
|
-
value: `::{Invoice_FieldIds.number_abc}`
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
---
|
|
176
|
-
|
|
177
|
-
## Linked Activity Field Values via itemTable
|
|
178
|
-
|
|
179
|
-
### The Problem
|
|
180
|
-
- `linkedfrom` fields DO NOT support cross-link syntax (`::linkedfromField/::targetField` returns empty)
|
|
181
|
-
- Cross-link syntax (`::field/::otherField`) only works for `activitylink` (forward link) fields
|
|
182
|
-
- `this.fieldMap.activityLinks` at runtime echoes config, not resolved data
|
|
183
|
-
|
|
184
|
-
### The Solution: itemTable
|
|
185
|
-
When you add fields from linked activities to the template's `activityLinks` config, Hailer populates `this.fieldMap.itemTable` with resolved values.
|
|
186
|
-
|
|
187
|
-
**Config example:**
|
|
188
|
-
```typescript
|
|
189
|
-
activityLinks: {
|
|
190
|
-
tilavaraukset: {
|
|
191
|
-
label: "Tilavaraukset",
|
|
192
|
-
field: FieldIds.tilavaraukset_link,
|
|
193
|
-
process: WorkflowIds.tilavaraukset,
|
|
194
|
-
fields: {
|
|
195
|
-
lisatiedot: {
|
|
196
|
-
label: "Lisätiedot",
|
|
197
|
-
value: `::${Tilavaraukset_FieldIds.varauksen_lisatiedot}`
|
|
198
|
-
},
|
|
199
|
-
ajankohta: {
|
|
200
|
-
label: "Ajankohta",
|
|
201
|
-
value: `::${Tilavaraukset_FieldIds.ajankohta}`
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
**itemTable structure at runtime:**
|
|
209
|
-
|
|
210
|
-
**CRITICAL: Workflow name is ALWAYS the LAST column** in the headers array. Filter tables by checking `headers[headers.length - 1]`, NOT `headers[0]`.
|
|
211
|
-
|
|
212
|
-
```javascript
|
|
213
|
-
// this.fieldMap.itemTable = array of table objects, one per activityLink
|
|
214
|
-
[
|
|
215
|
-
{
|
|
216
|
-
headers: ["Lisätiedot", "Ajankohta", "Tilavaraukset"], // Last header = workflow name
|
|
217
|
-
rows: [
|
|
218
|
-
["Kaksi pöytää", "Feb 23, 2026 14:15 – Feb 24, 2026 10:00", ""],
|
|
219
|
-
// one row per linked activity
|
|
220
|
-
]
|
|
221
|
-
},
|
|
222
|
-
// ... more tables for other activityLinks
|
|
223
|
-
]
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
**Helper pattern for extracting columns:**
|
|
227
|
-
```javascript
|
|
228
|
-
function getItemTableColumn(itemTable, workflowName, columnLabel) {
|
|
229
|
-
if (!itemTable) return [];
|
|
230
|
-
for (const table of itemTable) {
|
|
231
|
-
const headers = table.headers || [];
|
|
232
|
-
// Workflow name is LAST header
|
|
233
|
-
if (headers[headers.length - 1] !== workflowName) continue;
|
|
234
|
-
const colIdx = headers.indexOf(columnLabel);
|
|
235
|
-
if (colIdx === -1) return [];
|
|
236
|
-
return (table.rows || []).map(row => row[colIdx] || '-');
|
|
237
|
-
}
|
|
238
|
-
return [];
|
|
239
|
-
}
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
**Usage in template code:**
|
|
243
|
-
```javascript
|
|
244
|
-
const lisatiedot = getItemTableColumn(
|
|
245
|
-
this.fieldMap.itemTable,
|
|
246
|
-
'Tilavaraukset', // workflow name (last header column)
|
|
247
|
-
'Lisätiedot' // field label to extract
|
|
248
|
-
);
|
|
249
|
-
// lisatiedot = ["Kaksi pöytää", "Toinen varaus kuvaus", ...]
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
### Gotchas
|
|
253
|
-
|
|
254
|
-
- **`linkedfrom` cross-links: DON'T WORK** in templates. Use `itemTable` instead.
|
|
255
|
-
- **`activitylink` (forward) cross-links: WORK fine** with `::field/::otherField` syntax.
|
|
256
|
-
- **Dates in itemTable** come as English strings (e.g., "Feb 23, 2026 14:15") - parse and reformat for Finnish if needed.
|
|
257
|
-
|
|
258
|
-
---
|
|
259
|
-
|
|
260
|
-
## template.code.ts (Generation Function)
|
|
261
|
-
|
|
262
|
-
### Class Structure
|
|
263
|
-
|
|
264
|
-
```typescript
|
|
265
|
-
export class invoice_abc {
|
|
266
|
-
// Context properties (available in Hailer backend)
|
|
267
|
-
activity!: ActivityDoc;
|
|
268
|
-
process!: ProcessDoc;
|
|
269
|
-
workspaceId!: string;
|
|
270
|
-
fieldMap!: FieldMap;
|
|
271
|
-
templateId!: string;
|
|
272
|
-
templateName!: string;
|
|
273
|
-
renderedActivity!: RenderedActivity[];
|
|
274
|
-
|
|
275
|
-
// Methods
|
|
276
|
-
setDocument!: (doc: any, filename: string) => void;
|
|
277
|
-
|
|
278
|
-
// <---- UNDER THIS LINE EVERYTHING IS SENT TO HAILER ---->
|
|
279
|
-
|
|
280
|
-
async setPdfDefinition() {
|
|
281
|
-
const doc = this.getEmptyPdfDoc(this.fieldMap);
|
|
282
|
-
|
|
283
|
-
// Build document content
|
|
284
|
-
doc.content.push(this.buildHeader());
|
|
285
|
-
doc.content.push(this.buildBody());
|
|
286
|
-
|
|
287
|
-
const filename = `invoice_${this.fieldMap.fields.invoiceNumber?.value}`;
|
|
288
|
-
this.setDocument(doc, filename);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Helper methods...
|
|
292
|
-
}
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
### Available Context Properties
|
|
296
|
-
|
|
297
|
-
| Property | Type | Description |
|
|
298
|
-
|----------|------|-------------|
|
|
299
|
-
| `this.fieldMap.fields` | Record | Field values with label/description |
|
|
300
|
-
| `this.fieldMap.images` | Record | Image IDs |
|
|
301
|
-
| `this.fieldMap.activityLinks` | Record | Linked activity data |
|
|
302
|
-
| `this.fieldMap.itemTable` | Array | Table data from linked activities |
|
|
303
|
-
| `this.activity` | Object | Current activity (name, currentPhase) |
|
|
304
|
-
| `this.process` | Object | Workflow info (name, phases) |
|
|
305
|
-
| `this.templateName` | string | Template name |
|
|
306
|
-
| `this.localeDate` | Date | Current date |
|
|
307
|
-
|
|
308
|
-
### Available Methods
|
|
309
|
-
|
|
310
|
-
| Method | Description |
|
|
311
|
-
|--------|-------------|
|
|
312
|
-
| `this.getEmptyPdfDoc(fieldMap)` | Base PDF structure with styles |
|
|
313
|
-
| `this.setDocument(doc, filename)` | Set final document (required) |
|
|
314
|
-
| `this.getChecked()` | Checked checkbox image |
|
|
315
|
-
| `this.getUnchecked()` | Unchecked checkbox image |
|
|
316
|
-
| `this.getCheckbox(value)` | Checkbox based on boolean |
|
|
317
|
-
|
|
318
|
-
---
|
|
319
|
-
|
|
320
|
-
## pdfmake Patterns
|
|
321
|
-
|
|
322
|
-
### Basic Text
|
|
323
|
-
```javascript
|
|
324
|
-
{ text: 'Hello World' }
|
|
325
|
-
{ text: 'Bold text', bold: true }
|
|
326
|
-
{ text: 'Large text', fontSize: 14 }
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
### Using Field Values
|
|
330
|
-
```javascript
|
|
331
|
-
const fields = this.fieldMap.fields;
|
|
332
|
-
|
|
333
|
-
{ text: fields.invoiceNumber?.value || 'N/A' }
|
|
334
|
-
{ text: fields.customerName?.label + ': ' + fields.customerName?.value }
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
### Columns
|
|
338
|
-
```javascript
|
|
339
|
-
{
|
|
340
|
-
columns: [
|
|
341
|
-
{ width: '*', text: 'Left side' },
|
|
342
|
-
{ width: 'auto', text: 'Right side' }
|
|
343
|
-
]
|
|
344
|
-
}
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
### Tables
|
|
348
|
-
```javascript
|
|
349
|
-
{
|
|
350
|
-
table: {
|
|
351
|
-
headerRows: 1,
|
|
352
|
-
widths: ['*', 100, 100],
|
|
353
|
-
body: [
|
|
354
|
-
// Header row
|
|
355
|
-
[
|
|
356
|
-
{ text: 'Item', bold: true },
|
|
357
|
-
{ text: 'Qty', bold: true },
|
|
358
|
-
{ text: 'Price', bold: true }
|
|
359
|
-
],
|
|
360
|
-
// Data rows
|
|
361
|
-
['Product A', '10', '€100.00'],
|
|
362
|
-
['Product B', '5', '€50.00']
|
|
363
|
-
]
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
### Table Spacing for Label:Value Grids
|
|
369
|
-
|
|
370
|
-
For 4-column label:value layouts, use narrower label columns to keep values close:
|
|
371
|
-
```javascript
|
|
372
|
-
// ✅ Good: Values stay close to labels
|
|
373
|
-
{
|
|
374
|
-
table: {
|
|
375
|
-
widths: [90, 'auto', 90, 'auto'],
|
|
376
|
-
body: [
|
|
377
|
-
[
|
|
378
|
-
{ text: 'Tilaaja:', bold: true }, fields.customer?.value || '',
|
|
379
|
-
{ text: 'Päivämäärä:', bold: true }, fields.date?.value || ''
|
|
380
|
-
]
|
|
381
|
-
]
|
|
382
|
-
},
|
|
383
|
-
layout: 'noBorders'
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// ❌ Avoid: [120, '*', 120, '*'] spreads content too wide
|
|
387
|
-
```
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
### Images
|
|
391
|
-
```javascript
|
|
392
|
-
// From fieldMap
|
|
393
|
-
{
|
|
394
|
-
image: this.fieldMap.images?.logo?.value,
|
|
395
|
-
width: 150
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// With fallback
|
|
399
|
-
this.fieldMap.images?.logo?.value
|
|
400
|
-
? { image: this.fieldMap.images.logo.value, width: 150 }
|
|
401
|
-
: { text: 'No logo', italics: true }
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
### Logo in Header (Two-Step Requirement)
|
|
405
|
-
|
|
406
|
-
**CRITICAL:** Logo in PDF header requires BOTH config AND code reference:
|
|
407
|
-
|
|
408
|
-
**Step 1 - template.config.ts:**
|
|
409
|
-
```typescript
|
|
410
|
-
fieldMap: {
|
|
411
|
-
images: {
|
|
412
|
-
logo: { value: "67640282d1346d04eacf4b05" } // Image ID from Hailer
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
**Step 2 - template.code.ts (getEmptyPdfDoc header function):**
|
|
418
|
-
```javascript
|
|
419
|
-
getEmptyPdfDoc(fieldMap) {
|
|
420
|
-
const images = fieldMap.images; // Must pass images to scope
|
|
421
|
-
return {
|
|
422
|
-
header(currentPage, pageCount) {
|
|
423
|
-
return [{
|
|
424
|
-
columns: [
|
|
425
|
-
// Must explicitly reference images here
|
|
426
|
-
images?.logo?.value
|
|
427
|
-
? { image: images.logo.value, width: 140, margin: [40, 20, 0, 0] }
|
|
428
|
-
: { text: '', margin: [40, 20, 0, 0] },
|
|
429
|
-
// ... rest of header
|
|
430
|
-
]
|
|
431
|
-
}];
|
|
432
|
-
},
|
|
433
|
-
// ...
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
**Common mistake:** Adding logo to config but not referencing in header function → logo doesn't appear.
|
|
439
|
-
|
|
440
|
-
### Margins & Spacing
|
|
441
|
-
```javascript
|
|
442
|
-
// [left, top, right, bottom]
|
|
443
|
-
{ text: 'Padded', margin: [10, 20, 10, 20] }
|
|
444
|
-
|
|
445
|
-
// Blank line
|
|
446
|
-
{ text: ' ' }
|
|
447
|
-
{ text: '', margin: [0, 10, 0, 0] }
|
|
448
|
-
```
|
|
449
|
-
|
|
450
|
-
### Styles
|
|
451
|
-
```javascript
|
|
452
|
-
// Define in getEmptyPdfDoc
|
|
453
|
-
styles: {
|
|
454
|
-
header: { fontSize: 18, bold: true },
|
|
455
|
-
subheader: { fontSize: 14, bold: true },
|
|
456
|
-
tableHeader: { bold: true, fillColor: '#eeeeee' }
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Use
|
|
460
|
-
{ text: 'Title', style: 'header' }
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
---
|
|
464
|
-
|
|
465
|
-
## Complete PDF Example
|
|
466
|
-
|
|
467
|
-
```javascript
|
|
468
|
-
async setPdfDefinition() {
|
|
469
|
-
const fields = this.fieldMap.fields;
|
|
470
|
-
const images = this.fieldMap.images;
|
|
471
|
-
|
|
472
|
-
const doc = this.getEmptyPdfDoc(this.fieldMap);
|
|
473
|
-
|
|
474
|
-
// Header with logo
|
|
475
|
-
doc.content.push({
|
|
476
|
-
columns: [
|
|
477
|
-
images?.logo?.value
|
|
478
|
-
? { image: images.logo.value, width: 120 }
|
|
479
|
-
: { text: '' },
|
|
480
|
-
{
|
|
481
|
-
alignment: 'right',
|
|
482
|
-
stack: [
|
|
483
|
-
{ text: 'INVOICE', style: 'header' },
|
|
484
|
-
{ text: fields.invoiceNumber?.value || '' }
|
|
485
|
-
]
|
|
486
|
-
}
|
|
487
|
-
]
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
// Customer info
|
|
491
|
-
doc.content.push({ text: ' ' });
|
|
492
|
-
doc.content.push({
|
|
493
|
-
text: fields.customerName?.value || 'Unknown Customer',
|
|
494
|
-
style: 'subheader'
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
// Line items table
|
|
498
|
-
doc.content.push({ text: ' ' });
|
|
499
|
-
doc.content.push(this.buildLineItemsTable());
|
|
500
|
-
|
|
501
|
-
// Total
|
|
502
|
-
doc.content.push({
|
|
503
|
-
alignment: 'right',
|
|
504
|
-
text: `Total: €${fields.amount?.value || '0.00'}`,
|
|
505
|
-
bold: true,
|
|
506
|
-
margin: [0, 20, 0, 0]
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
const filename = `invoice_${fields.invoiceNumber?.value || 'draft'}`;
|
|
510
|
-
this.setDocument(doc, filename);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
buildLineItemsTable() {
|
|
514
|
-
// Build from itemTable if available
|
|
515
|
-
const items = this.fieldMap.itemTable || [];
|
|
516
|
-
|
|
517
|
-
const body = [
|
|
518
|
-
// Header
|
|
519
|
-
[
|
|
520
|
-
{ text: 'Description', bold: true },
|
|
521
|
-
{ text: 'Qty', bold: true },
|
|
522
|
-
{ text: 'Price', bold: true }
|
|
523
|
-
]
|
|
524
|
-
];
|
|
525
|
-
|
|
526
|
-
// Data rows
|
|
527
|
-
items.forEach(item => {
|
|
528
|
-
body.push([
|
|
529
|
-
item.description || '',
|
|
530
|
-
item.quantity || '',
|
|
531
|
-
item.price || ''
|
|
532
|
-
]);
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
return {
|
|
536
|
-
table: {
|
|
537
|
-
headerRows: 1,
|
|
538
|
-
widths: ['*', 60, 80],
|
|
539
|
-
body
|
|
540
|
-
}
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
---
|
|
546
|
-
|
|
547
|
-
## CSV Templates
|
|
548
|
-
|
|
549
|
-
For CSV, return array of arrays:
|
|
550
|
-
|
|
551
|
-
```javascript
|
|
552
|
-
async setCsvDefinition() {
|
|
553
|
-
const fields = this.fieldMap.fields;
|
|
554
|
-
|
|
555
|
-
const rows = [
|
|
556
|
-
// Header row
|
|
557
|
-
['Invoice', 'Customer', 'Amount', 'Date'],
|
|
558
|
-
// Data row
|
|
559
|
-
[
|
|
560
|
-
fields.invoiceNumber?.value || '',
|
|
561
|
-
fields.customerName?.value || '',
|
|
562
|
-
fields.amount?.value || '',
|
|
563
|
-
new Date().toLocaleDateString('fi-FI')
|
|
564
|
-
]
|
|
565
|
-
];
|
|
566
|
-
|
|
567
|
-
const filename = `invoice_${fields.invoiceNumber?.value || 'export'}`;
|
|
568
|
-
this.setDocument(rows, filename);
|
|
569
|
-
}
|
|
570
|
-
```
|
|
571
|
-
|
|
572
|
-
---
|
|
573
|
-
|
|
574
|
-
## Options (opts)
|
|
575
|
-
|
|
576
|
-
```typescript
|
|
577
|
-
opts: {
|
|
578
|
-
formatFieldValue: true, // Apply formatting
|
|
579
|
-
decimalSeparator: ",", // Finnish: comma
|
|
580
|
-
thousandSeparator: " ", // Finnish: space
|
|
581
|
-
toFixedDigits: "2", // Decimal places
|
|
582
|
-
toFixedIds: "field1,field2" // Fields to format
|
|
583
|
-
}
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
---
|
|
587
|
-
|
|
588
|
-
## Common Mistakes
|
|
589
|
-
|
|
590
|
-
| Wrong | Right |
|
|
591
|
-
|-------|-------|
|
|
592
|
-
| `::{FieldIds.x}` | `::${FieldIds.x}` (template literal) |
|
|
593
|
-
| Missing `?.` on field access | Always use `fields.x?.value` |
|
|
594
|
-
| Forgetting `setDocument()` | Must call at end of function |
|
|
595
|
-
| Creating folder manually | Let `npm run pull` create it after sync |
|
|
596
|
-
| Editing templateId | Never change after creation |
|
|
597
|
-
| Pushing before sync (new) | New templates: sync first, then push |
|
|
598
|
-
|
|
599
|
-
---
|
|
600
|
-
|
|
601
|
-
## Real-World Example: Meeting Minutes PDF
|
|
602
|
-
|
|
603
|
-
Complete working example with brand colors, conditional rendering, and Finnish locale.
|
|
604
|
-
|
|
605
|
-
### template.config.ts
|
|
606
|
-
```typescript
|
|
607
|
-
export const template: DocumentTemplateUpdatePayload = {
|
|
608
|
-
content: "@function:palaverimuistio_pdf_b8b",
|
|
609
|
-
description: "Palaverimuistion PDF-tuloste",
|
|
610
|
-
fieldMap: {
|
|
611
|
-
fields: {
|
|
612
|
-
name: { label: "Otsikko", value: "::name" },
|
|
613
|
-
shortDescription: { label: "Lyhyt kuvaus", value: "::681adfcadc3d7d989f58a38d" },
|
|
614
|
-
date: { label: "Ajankohta", value: "::67e697da6ada809b961c35b5" },
|
|
615
|
-
participants: { label: "Osallistujat", value: "::67e698076ada809b961c36d7" },
|
|
616
|
-
title1: { label: "Otsikko 1", value: "::6819ed047be62a4af274b5fa" },
|
|
617
|
-
text1: { label: "Teksti 1", value: "::67e697106ada809b961c310c" }
|
|
618
|
-
// ... more fields
|
|
619
|
-
},
|
|
620
|
-
images: {
|
|
621
|
-
logo: { description: "Company logo", value: "68009ab7e01c4e8528bfd901" }
|
|
622
|
-
}
|
|
623
|
-
},
|
|
624
|
-
fileType: "pdf",
|
|
625
|
-
name: "Palaverimuistio PDF",
|
|
626
|
-
opts: { formatFieldValue: true, decimalSeparator: ",", thousandSeparator: " " },
|
|
627
|
-
templateId: "6960ae0978e1a7b04df30b8b"
|
|
628
|
-
};
|
|
629
|
-
```
|
|
630
|
-
|
|
631
|
-
### template.code.ts
|
|
632
|
-
```javascript
|
|
633
|
-
async setPdfDefinition() {
|
|
634
|
-
const doc = this.getEmptyPdfDoc(this.fieldMap);
|
|
635
|
-
const fields = this.fieldMap.fields;
|
|
636
|
-
|
|
637
|
-
// Title with brand color
|
|
638
|
-
doc.content.push({
|
|
639
|
-
text: fields.name?.value || 'Palaverimuistio',
|
|
640
|
-
style: 'header'
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
// Blue divider line
|
|
644
|
-
doc.content.push({
|
|
645
|
-
canvas: [{ type: 'line', x1: 0, y1: 0, x2: 515, y2: 0, lineWidth: 1, color: '#005EAA' }],
|
|
646
|
-
margin: [0, 10, 0, 15]
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
// Conditional info rows - only render if value exists
|
|
650
|
-
if (fields.date?.value) {
|
|
651
|
-
doc.content.push({
|
|
652
|
-
columns: [
|
|
653
|
-
{ width: 120, text: 'Ajankohta:', bold: true, color: '#005EAA' },
|
|
654
|
-
{ width: '*', text: fields.date.value }
|
|
655
|
-
],
|
|
656
|
-
margin: [0, 3, 0, 3]
|
|
657
|
-
});
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// Dynamic note sections - only show if content exists
|
|
661
|
-
const notePairs = [
|
|
662
|
-
{ title: fields.title1, text: fields.text1 },
|
|
663
|
-
{ title: fields.title2, text: fields.text2 }
|
|
664
|
-
];
|
|
665
|
-
|
|
666
|
-
notePairs.forEach(pair => {
|
|
667
|
-
if (pair.title?.value || pair.text?.value) {
|
|
668
|
-
if (pair.title?.value) {
|
|
669
|
-
doc.content.push({ text: pair.title.value, style: 'sectionHeader', margin: [0, 15, 0, 8] });
|
|
670
|
-
}
|
|
671
|
-
if (pair.text?.value) {
|
|
672
|
-
doc.content.push({ text: pair.text.value, style: 'bodyText', margin: [0, 0, 0, 10] });
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
});
|
|
676
|
-
|
|
677
|
-
// Filename from activity name + date
|
|
678
|
-
const activityName = this.activity?.name || 'palaverimuistio';
|
|
679
|
-
const dateStr = fields.date?.value || new Date().toISOString().split('T')[0];
|
|
680
|
-
const filename = `${activityName}_${dateStr}`.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
681
|
-
|
|
682
|
-
this.setDocument(doc, filename);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
getEmptyPdfDoc(fieldMap) {
|
|
686
|
-
const images = fieldMap.images;
|
|
687
|
-
return {
|
|
688
|
-
header(currentPage, pageCount) {
|
|
689
|
-
return [{
|
|
690
|
-
columns: [
|
|
691
|
-
images?.logo?.value
|
|
692
|
-
? { image: images.logo.value, width: 140, margin: [40, 20, 0, 0] }
|
|
693
|
-
: { text: '', width: 140, margin: [40, 20, 0, 0] },
|
|
694
|
-
{
|
|
695
|
-
width: '*',
|
|
696
|
-
alignment: 'right',
|
|
697
|
-
stack: [
|
|
698
|
-
{ text: 'PALAVERIMUISTIO', fontSize: 10, color: '#005EAA', bold: true },
|
|
699
|
-
{ text: `Sivu ${currentPage} / ${pageCount}`, fontSize: 8, color: '#666666' }
|
|
700
|
-
],
|
|
701
|
-
margin: [0, 25, 40, 0]
|
|
702
|
-
}
|
|
703
|
-
]
|
|
704
|
-
}];
|
|
705
|
-
},
|
|
706
|
-
footer() {
|
|
707
|
-
const datetime = new Date().toLocaleDateString('fi-FI');
|
|
708
|
-
return [{
|
|
709
|
-
columns: [
|
|
710
|
-
{ width: '*', text: 'Luotu: ' + datetime, fontSize: 7, color: '#999999', margin: [40, 0, 0, 0] },
|
|
711
|
-
{ width: '*', text: 'hailer.com', fontSize: 7, color: '#999999', alignment: 'right', margin: [0, 0, 40, 0] }
|
|
712
|
-
]
|
|
713
|
-
}];
|
|
714
|
-
},
|
|
715
|
-
pageSize: 'A4',
|
|
716
|
-
pageMargins: [40, 80, 40, 50],
|
|
717
|
-
content: [],
|
|
718
|
-
styles: {
|
|
719
|
-
header: { fontSize: 22, bold: true, color: '#005EAA' },
|
|
720
|
-
sectionHeader: { fontSize: 14, bold: true, color: '#005EAA' },
|
|
721
|
-
bodyText: { fontSize: 10, color: '#333333', lineHeight: 1.4 }
|
|
722
|
-
},
|
|
723
|
-
defaultStyle: { font: 'nunito', fontSize: 10 }
|
|
724
|
-
};
|
|
725
|
-
}
|
|
726
|
-
```
|
|
727
|
-
|
|
728
|
-
---
|
|
729
|
-
|
|
730
|
-
## Additional Patterns
|
|
731
|
-
|
|
732
|
-
### Using Raw Field IDs vs Enums
|
|
733
|
-
|
|
734
|
-
When orchestrator provides field IDs directly, use them as strings:
|
|
735
|
-
```typescript
|
|
736
|
-
// Raw field ID (common)
|
|
737
|
-
value: "::67e697da6ada809b961c35b5"
|
|
738
|
-
|
|
739
|
-
// With enums (when imported)
|
|
740
|
-
value: `::${Workflow_FieldIds.date_abc}`
|
|
741
|
-
```
|
|
742
|
-
|
|
743
|
-
### Clickable Links
|
|
744
|
-
```javascript
|
|
745
|
-
if (fields.materialsLink?.value) {
|
|
746
|
-
doc.content.push({
|
|
747
|
-
text: fields.materialsLink.value,
|
|
748
|
-
link: fields.materialsLink.value,
|
|
749
|
-
color: '#005EAA',
|
|
750
|
-
decoration: 'underline'
|
|
751
|
-
});
|
|
752
|
-
}
|
|
753
|
-
```
|
|
754
|
-
|
|
755
|
-
### Finnish Locale Dates
|
|
756
|
-
```javascript
|
|
757
|
-
new Date().toLocaleDateString('fi-FI') // "9.1.2025"
|
|
758
|
-
new Date().toLocaleTimeString('fi-FI', { hour: '2-digit', minute: '2-digit' }) // "14:30"
|
|
759
|
-
```
|
|
760
|
-
|
|
761
|
-
### Parsing Pre-Formatted Hailer Dates
|
|
762
|
-
|
|
763
|
-
**Problem:** Hailer pre-formats date values even with `formatFieldValue: false`. Date/daterange fields arrive as English strings like "Feb 5, 2026" or "Feb 5, 2026 Feb 5, 2026".
|
|
764
|
-
|
|
765
|
-
**Solution:** Parse and reformat to Finnish:
|
|
766
|
-
```javascript
|
|
767
|
-
function formatToFinnishDate(hailerDate) {
|
|
768
|
-
if (!hailerDate) return '';
|
|
769
|
-
|
|
770
|
-
// Extract dates with regex (handles single date and date ranges)
|
|
771
|
-
const datePattern = /([A-Za-z]{3} \d{1,2}, \d{4})/g;
|
|
772
|
-
const matches = hailerDate.match(datePattern);
|
|
773
|
-
|
|
774
|
-
if (!matches) return hailerDate;
|
|
775
|
-
|
|
776
|
-
const finnishDates = matches.map(dateStr => {
|
|
777
|
-
const date = new Date(dateStr);
|
|
778
|
-
return date.toLocaleDateString('fi-FI', {
|
|
779
|
-
day: '2-digit',
|
|
780
|
-
month: '2-digit',
|
|
781
|
-
year: '2-digit'
|
|
782
|
-
});
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
// Single date or range
|
|
786
|
-
return finnishDates.length === 1
|
|
787
|
-
? finnishDates[0] // "05.02.26"
|
|
788
|
-
: `${finnishDates[0]} - ${finnishDates[1]}`; // "05.02.26 - 06.02.26"
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// Usage
|
|
792
|
-
const finnishDate = formatToFinnishDate(fields.eventDate?.value);
|
|
793
|
-
```
|
|
794
|
-
|
|
795
|
-
### Safe Filename Generation
|
|
796
|
-
```javascript
|
|
797
|
-
const filename = `${activityName}_${dateStr}`.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
798
|
-
```
|
|
799
|
-
|
|
800
|
-
### File Upload Fields as Images
|
|
801
|
-
|
|
802
|
-
When a field contains uploaded images (FileUpload field type), the value comes as a JSON array string of base64 data URLs.
|
|
803
|
-
|
|
804
|
-
**Config:** Put in `fieldMap.fields` (NOT `fieldMap.images`):
|
|
805
|
-
```typescript
|
|
806
|
-
fieldMap: {
|
|
807
|
-
fields: {
|
|
808
|
-
photos: {
|
|
809
|
-
label: "Photos",
|
|
810
|
-
value: `::${FieldIds.photos_abc}` // FileUpload field
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
```
|
|
815
|
-
|
|
816
|
-
**Code:** Parse and render each image:
|
|
817
|
-
```javascript
|
|
818
|
-
// Parse the JSON array of base64 data URLs
|
|
819
|
-
const photos = fields.photos?.value;
|
|
820
|
-
if (photos) {
|
|
821
|
-
try {
|
|
822
|
-
const imageArray = JSON.parse(photos);
|
|
823
|
-
|
|
824
|
-
imageArray.forEach((dataUrl) => {
|
|
825
|
-
// Validate it's actually an image data URL
|
|
826
|
-
if (typeof dataUrl === 'string' && dataUrl.startsWith('data:image')) {
|
|
827
|
-
doc.content.push({
|
|
828
|
-
image: dataUrl,
|
|
829
|
-
fit: [400, 300], // Preserves aspect ratio within bounds
|
|
830
|
-
margin: [0, 10, 0, 10]
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
|
-
});
|
|
834
|
-
} catch (e) {
|
|
835
|
-
// Not valid JSON - skip
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
```
|
|
839
|
-
|
|
840
|
-
**Key points:**
|
|
841
|
-
- Value is a **JSON array string**, not raw array
|
|
842
|
-
- Each item is a **base64 data URL** (`data:image/png;base64,...`)
|
|
843
|
-
- Use `JSON.parse()` to convert to array
|
|
844
|
-
- Validate with `startsWith('data:image')` before rendering
|
|
845
|
-
- Use `fit: [width, height]` to preserve aspect ratio
|
|
846
|
-
|
|
847
|
-
### Null Value Filtering in PDFs
|
|
848
|
-
|
|
849
|
-
Function fields can return formatted defaults (e.g., "0 kgCO2e/yksikkö") even when underlying data is null. Use a helper to distinguish meaningful values from null placeholders.
|
|
850
|
-
|
|
851
|
-
```javascript
|
|
852
|
-
// Helper to check if value is displayable
|
|
853
|
-
function isValidValue(value) {
|
|
854
|
-
if (value === null || value === undefined) return false;
|
|
855
|
-
const str = String(value).trim();
|
|
856
|
-
|
|
857
|
-
// Filter obvious null placeholders
|
|
858
|
-
if (str === '' || str === '−' || str === '::' || str === '-') return false;
|
|
859
|
-
|
|
860
|
-
// Filter dimension zeros (0 mm, 0 cm, etc.)
|
|
861
|
-
if (/^0\s*(mm|cm|m|kg|kpl)?$/i.test(str)) return false;
|
|
862
|
-
|
|
863
|
-
return true;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
// Usage in PDF generation
|
|
867
|
-
if (isValidValue(fields.weight?.value)) {
|
|
868
|
-
doc.content.push({
|
|
869
|
-
columns: [
|
|
870
|
-
{ width: 120, text: 'Paino:', bold: true },
|
|
871
|
-
{ width: '*', text: fields.weight.value }
|
|
872
|
-
]
|
|
873
|
-
});
|
|
874
|
-
}
|
|
875
|
-
```
|
|
876
|
-
|
|
877
|
-
**Field-specific logic:**
|
|
878
|
-
- **Dimension zeros** (0 mm, 0 cm): Usually hide - indicates unmeasured
|
|
879
|
-
- **Price zeros** (0 €): Context-dependent - sometimes meaningful
|
|
880
|
-
- **Formatted function fields**: Check if underlying calculation returned null
|
|
881
|
-
|
|
882
|
-
---
|
|
883
|
-
|
|
884
|
-
### Distinguishing Meaningful Zero from Empty/Null
|
|
885
|
-
|
|
886
|
-
**Problem:** CO2 fields in Spolia required multiple iterations to filter correctly. Some zeros are meaningful (calculated values), others are empty/null placeholders.
|
|
887
|
-
|
|
888
|
-
**Solution:** Use type-specific filtering logic:
|
|
889
|
-
|
|
890
|
-
```typescript
|
|
891
|
-
// Core pattern: Filter out truly empty values but keep meaningful zeros
|
|
892
|
-
function shouldDisplayValue(value: unknown): boolean {
|
|
893
|
-
// Null and undefined are always empty
|
|
894
|
-
if (value === null || value === undefined) return false;
|
|
895
|
-
|
|
896
|
-
// Empty string is empty
|
|
897
|
-
if (value === '') return false;
|
|
898
|
-
|
|
899
|
-
// NaN is empty (for numeric fields)
|
|
900
|
-
if (typeof value === 'number' && isNaN(value)) return false;
|
|
901
|
-
|
|
902
|
-
// CRITICAL: Keep 0 - it's a meaningful numeric value
|
|
903
|
-
if (value === 0) return true;
|
|
904
|
-
|
|
905
|
-
return true;
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
// Pattern: Filter fields for display based on value type
|
|
909
|
-
const displayFields = fields.filter(f => {
|
|
910
|
-
const value = activity.fields?.[f.id]?.value;
|
|
911
|
-
|
|
912
|
-
// Different rules per field type
|
|
913
|
-
switch (f.fieldType) {
|
|
914
|
-
case 'number':
|
|
915
|
-
// Keep 0, filter null/undefined/NaN
|
|
916
|
-
return shouldDisplayValue(value);
|
|
917
|
-
|
|
918
|
-
case 'text':
|
|
919
|
-
// Filter null, undefined, empty string
|
|
920
|
-
// Consider whitespace-only as empty
|
|
921
|
-
if (!value) return false;
|
|
922
|
-
if (typeof value === 'string' && value.trim() === '') return false;
|
|
923
|
-
return true;
|
|
924
|
-
|
|
925
|
-
case 'reference':
|
|
926
|
-
// Reference fields must have a valid ID string
|
|
927
|
-
// ID length varies by workflow - check for non-empty string
|
|
928
|
-
return typeof value === 'string' && value.trim().length > 0;
|
|
929
|
-
|
|
930
|
-
case 'date':
|
|
931
|
-
// Valid ISO date or timestamp
|
|
932
|
-
return value && !isNaN(new Date(value).getTime());
|
|
933
|
-
|
|
934
|
-
case 'boolean':
|
|
935
|
-
// Keep false and true - both are valid
|
|
936
|
-
return typeof value === 'boolean';
|
|
937
|
-
|
|
938
|
-
default:
|
|
939
|
-
return shouldDisplayValue(value);
|
|
940
|
-
}
|
|
941
|
-
});
|
|
942
|
-
```
|
|
943
|
-
|
|
944
|
-
**Field-type specific examples:**
|
|
945
|
-
|
|
946
|
-
```typescript
|
|
947
|
-
// Numeric field: 0 is valid, undefined is empty
|
|
948
|
-
const co2Value = fields.co2_emissions?.value; // Could be 0, could be undefined
|
|
949
|
-
if (shouldDisplayValue(co2Value)) {
|
|
950
|
-
// Display: "0 kgCO2e" ✓
|
|
951
|
-
// Don't display: undefined/null/NaN ✓
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
// Text field: Empty string is empty, whitespace-only is empty
|
|
955
|
-
const description = fields.description?.value;
|
|
956
|
-
if (description && typeof description === 'string' && description.trim() !== '') {
|
|
957
|
-
// Display: "Sample text" ✓
|
|
958
|
-
// Don't display: "" or " " ✓
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
// Reference field: Check for non-empty string ID
|
|
962
|
-
const linkedActivityId = fields.linked_activity?.value;
|
|
963
|
-
if (typeof linkedActivityId === 'string' && linkedActivityId.trim().length > 0) {
|
|
964
|
-
// Display: "507f1f77bcf86cd799439011" ✓
|
|
965
|
-
// Don't display: "" or null ✓
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
// Boolean field: Both true and false are valid values
|
|
969
|
-
const isActive = fields.active?.value;
|
|
970
|
-
if (typeof isActive === 'boolean') {
|
|
971
|
-
// Display: false ✓ (don't filter out)
|
|
972
|
-
// Don't display: null/undefined ✓
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
// Function field returning formatted number: Keep meaningful zeros
|
|
976
|
-
const calculatedCost = fields.total_cost?.value; // Example: "0 €" or "150 €"
|
|
977
|
-
if (shouldDisplayValue(calculatedCost)) {
|
|
978
|
-
// Display: "0 €" if it's a calculated zero ✓
|
|
979
|
-
// But if underlying value was null, filtered function may still output placeholder
|
|
980
|
-
// → Use isValidValue() helper (above) for extra safety
|
|
981
|
-
}
|
|
982
|
-
```
|
|
983
|
-
|
|
984
|
-
**Real-world scenario from Spolia CO2 case:**
|
|
985
|
-
|
|
986
|
-
```typescript
|
|
987
|
-
// ❌ Bad: Filters out valid zeros
|
|
988
|
-
const fields = activity.fields.filter(f => f.value); // Loses 0 values!
|
|
989
|
-
|
|
990
|
-
// ❌ Also bad: Checks truthiness
|
|
991
|
-
if (activity.fields[fieldId]?.value) { // false for 0!
|
|
992
|
-
// Won't render CO2: 0 kgCO2e/yksikkö
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
// ✅ Good: Type-aware filtering
|
|
996
|
-
if (shouldDisplayValue(activity.fields[fieldId]?.value)) {
|
|
997
|
-
// Renders CO2: 0 kgCO2e/yksikkö ✓
|
|
998
|
-
// Skips CO2: undefined ✓
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
// ✅ Better: Field-specific logic
|
|
1002
|
-
const co2 = activity.fields[CO2_FIELD_ID]?.value;
|
|
1003
|
-
if (co2 !== null && co2 !== undefined && !isNaN(Number(co2))) {
|
|
1004
|
-
doc.content.push({
|
|
1005
|
-
text: `CO2: ${co2} kgCO2e/yksikkö`
|
|
1006
|
-
});
|
|
1007
|
-
}
|
|
1008
|
-
```
|
|
1009
|
-
|
|
1010
|
-
**Troubleshooting:**
|
|
1011
|
-
|
|
1012
|
-
| Symptom | Likely Cause | Fix |
|
|
1013
|
-
|---------|--------------|-----|
|
|
1014
|
-
| "0 € " values missing from PDF | Using truthiness check (if value) | Use `shouldDisplayValue()` |
|
|
1015
|
-
| Function field shows "−" or placeholder | Underlying data is null but function formatted it | Use `isValidValue()` helper |
|
|
1016
|
-
| Some zeros showing, some not | Inconsistent filter logic per field type | Use switch statement by fieldType |
|
|
1017
|
-
| NaN appearing in output | Not checking `isNaN()` for numbers | Add NaN check |
|
|
1018
|
-
| Whitespace-only text showing | Not trimming string values | Use `value.trim() !== ''` for text |
|
|
1019
|
-
|
|
1020
|
-
---
|
|
1021
|
-
|
|
1022
|
-
## Checklist
|
|
1023
|
-
|
|
1024
|
-
Before pushing template changes:
|
|
1025
|
-
|
|
1026
|
-
- [ ] Used template literals `${}` in field mappings (or raw IDs as strings)
|
|
1027
|
-
- [ ] All field accesses use optional chaining `?.`
|
|
1028
|
-
- [ ] `setDocument(doc, filename)` called at end
|
|
1029
|
-
- [ ] Filename has no extension (added automatically)
|
|
1030
|
-
- [ ] New template: sync first, pull, then edit config/code
|
|
1031
|
-
- [ ] Conditional rendering for optional fields
|
|
1032
|
-
- [ ] Brand colors as hex codes (e.g., '#005EAA')
|
|
1033
|
-
- [ ] Tested PDF opens correctly
|