@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.
Files changed (271) hide show
  1. package/CHANGELOG.md +0 -7
  2. package/{.claude → dist}/CLAUDE.md +2 -2
  3. package/dist/app.js +18 -5
  4. package/dist/bot/bot-config.d.ts +10 -1
  5. package/dist/bot/bot-config.js +64 -3
  6. package/dist/bot/bot-manager.d.ts +2 -0
  7. package/dist/bot/bot-manager.js +9 -2
  8. package/dist/bot/bot.d.ts +33 -0
  9. package/dist/bot/bot.js +461 -160
  10. package/dist/bot/services/message-classifier.js +17 -0
  11. package/dist/bot/services/permission-guard.d.ts +52 -0
  12. package/dist/bot/services/permission-guard.js +149 -0
  13. package/dist/bot/services/types.d.ts +5 -0
  14. package/dist/bot/services/typing-indicator.d.ts +6 -1
  15. package/dist/bot/services/typing-indicator.js +19 -3
  16. package/dist/cli.js +0 -0
  17. package/dist/config.d.ts +6 -1
  18. package/dist/config.js +43 -0
  19. package/dist/core.js +3 -6
  20. package/dist/lib/discussion-lock.d.ts +42 -0
  21. package/dist/lib/discussion-lock.js +110 -0
  22. package/dist/mcp/UserContextCache.d.ts +5 -0
  23. package/dist/mcp/UserContextCache.js +51 -19
  24. package/dist/mcp/hailer-clients.d.ts +19 -1
  25. package/dist/mcp/hailer-clients.js +158 -24
  26. package/dist/mcp/session-store.d.ts +68 -0
  27. package/dist/mcp/session-store.js +169 -0
  28. package/dist/mcp/signal-handler.js +2 -0
  29. package/dist/mcp/tool-registry.d.ts +17 -4
  30. package/dist/mcp/tool-registry.js +37 -7
  31. package/dist/mcp/tools/activity.js +99 -7
  32. package/dist/mcp/tools/app-scaffold.js +304 -336
  33. package/dist/mcp/tools/bot-config/constants.d.ts +23 -0
  34. package/dist/mcp/tools/bot-config/constants.js +94 -0
  35. package/dist/mcp/tools/bot-config/core.d.ts +253 -0
  36. package/dist/mcp/tools/bot-config/core.js +2456 -0
  37. package/dist/mcp/tools/bot-config/index.d.ts +10 -0
  38. package/dist/mcp/tools/bot-config/index.js +59 -0
  39. package/dist/mcp/tools/bot-config/tools.d.ts +7 -0
  40. package/dist/mcp/tools/bot-config/tools.js +15 -0
  41. package/dist/mcp/tools/bot-config/types.d.ts +50 -0
  42. package/dist/mcp/tools/bot-config/types.js +6 -0
  43. package/dist/mcp/tools/bug-fixer-tools.d.ts +45 -0
  44. package/dist/mcp/tools/bug-fixer-tools.js +1096 -0
  45. package/dist/mcp/tools/company.d.ts +9 -0
  46. package/dist/mcp/tools/company.js +88 -0
  47. package/dist/mcp/tools/discussion.js +68 -0
  48. package/dist/mcp/tools/document.d.ts +11 -0
  49. package/dist/mcp/tools/document.js +741 -0
  50. package/dist/mcp/tools/investigate.d.ts +9 -0
  51. package/dist/mcp/tools/investigate.js +254 -0
  52. package/dist/mcp/tools/workflow-permissions.d.ts +15 -0
  53. package/dist/mcp/tools/workflow-permissions.js +204 -0
  54. package/dist/mcp/tools/workflow.js +57 -18
  55. package/dist/mcp/utils/index.d.ts +2 -0
  56. package/dist/mcp/utils/index.js +12 -1
  57. package/dist/mcp/utils/role-utils.d.ts +74 -0
  58. package/dist/mcp/utils/role-utils.js +151 -0
  59. package/dist/mcp/utils/types.d.ts +43 -1
  60. package/dist/mcp/utils/types.js +14 -0
  61. package/dist/mcp/webhook-handler.d.ts +4 -0
  62. package/dist/mcp/webhook-handler.js +8 -0
  63. package/dist/mcp-server.d.ts +23 -2
  64. package/dist/mcp-server.js +639 -127
  65. package/dist/plugins/vipunen/client.d.ts +150 -0
  66. package/dist/plugins/vipunen/client.js +535 -0
  67. package/dist/plugins/vipunen/config/schema-config.json +19 -0
  68. package/dist/plugins/vipunen/config/schema-doc.json +22 -0
  69. package/dist/plugins/vipunen/index.d.ts +41 -0
  70. package/dist/plugins/vipunen/index.js +88 -0
  71. package/dist/plugins/vipunen/tools.d.ts +26 -0
  72. package/dist/plugins/vipunen/tools.js +501 -0
  73. package/dist/stdio-server.d.ts +14 -0
  74. package/dist/stdio-server.js +101 -0
  75. package/package.json +2 -1
  76. package/.claude/agents/agent-ada-skill-builder.md +0 -94
  77. package/.claude/agents/agent-alejandro-function-fields.md +0 -342
  78. package/.claude/agents/agent-bjorn-config-audit.md +0 -103
  79. package/.claude/agents/agent-builder-agent-creator.md +0 -130
  80. package/.claude/agents/agent-code-simplifier.md +0 -53
  81. package/.claude/agents/agent-dmitri-activity-crud.md +0 -159
  82. package/.claude/agents/agent-giuseppe-app-builder.md +0 -247
  83. package/.claude/agents/agent-gunther-mcp-tools.md +0 -39
  84. package/.claude/agents/agent-helga-workflow-config.md +0 -204
  85. package/.claude/agents/agent-igor-activity-mover-automation.md +0 -125
  86. package/.claude/agents/agent-ingrid-doc-templates.md +0 -261
  87. package/.claude/agents/agent-ivan-monolith.md +0 -154
  88. package/.claude/agents/agent-kenji-data-reader.md +0 -86
  89. package/.claude/agents/agent-lars-code-inspector.md +0 -102
  90. package/.claude/agents/agent-marco-mockup-builder.md +0 -110
  91. package/.claude/agents/agent-marcus-api-documenter.md +0 -323
  92. package/.claude/agents/agent-marketplace-publisher.md +0 -280
  93. package/.claude/agents/agent-marketplace-reviewer.md +0 -309
  94. package/.claude/agents/agent-permissions-handler.md +0 -208
  95. package/.claude/agents/agent-simple-writer.md +0 -48
  96. package/.claude/agents/agent-svetlana-code-review.md +0 -171
  97. package/.claude/agents/agent-tanya-test-runner.md +0 -333
  98. package/.claude/agents/agent-ui-designer.md +0 -100
  99. package/.claude/agents/agent-viktor-sql-insights.md +0 -212
  100. package/.claude/agents/agent-web-search.md +0 -55
  101. package/.claude/agents/agent-yevgeni-discussions.md +0 -45
  102. package/.claude/agents/agent-zara-zapier.md +0 -159
  103. package/.claude/commands/app-squad.md +0 -135
  104. package/.claude/commands/audit-squad.md +0 -158
  105. package/.claude/commands/autoplan.md +0 -563
  106. package/.claude/commands/cleanup-squad.md +0 -98
  107. package/.claude/commands/config-squad.md +0 -106
  108. package/.claude/commands/crud-squad.md +0 -87
  109. package/.claude/commands/data-squad.md +0 -97
  110. package/.claude/commands/debug-squad.md +0 -303
  111. package/.claude/commands/doc-squad.md +0 -65
  112. package/.claude/commands/handoff.md +0 -137
  113. package/.claude/commands/health.md +0 -49
  114. package/.claude/commands/help.md +0 -29
  115. package/.claude/commands/help:agents.md +0 -151
  116. package/.claude/commands/help:commands.md +0 -78
  117. package/.claude/commands/help:faq.md +0 -79
  118. package/.claude/commands/help:plugins.md +0 -50
  119. package/.claude/commands/help:skills.md +0 -93
  120. package/.claude/commands/help:tools.md +0 -75
  121. package/.claude/commands/hotfix-squad.md +0 -112
  122. package/.claude/commands/integration-squad.md +0 -82
  123. package/.claude/commands/janitor-squad.md +0 -167
  124. package/.claude/commands/learn-auto.md +0 -120
  125. package/.claude/commands/learn.md +0 -120
  126. package/.claude/commands/mcp-list.md +0 -27
  127. package/.claude/commands/onboard-squad.md +0 -140
  128. package/.claude/commands/plan-workspace.md +0 -732
  129. package/.claude/commands/prd.md +0 -130
  130. package/.claude/commands/project-status.md +0 -82
  131. package/.claude/commands/publish.md +0 -138
  132. package/.claude/commands/recap.md +0 -69
  133. package/.claude/commands/restore.md +0 -64
  134. package/.claude/commands/review-squad.md +0 -152
  135. package/.claude/commands/save.md +0 -24
  136. package/.claude/commands/stats.md +0 -19
  137. package/.claude/commands/swarm.md +0 -210
  138. package/.claude/commands/tool-builder.md +0 -39
  139. package/.claude/commands/ws-pull.md +0 -44
  140. package/.claude/hooks/_shared-memory.cjs +0 -305
  141. package/.claude/hooks/_utils.cjs +0 -108
  142. package/.claude/hooks/agent-failure-detector.cjs +0 -383
  143. package/.claude/hooks/agent-usage-logger.cjs +0 -204
  144. package/.claude/hooks/app-edit-guard.cjs +0 -494
  145. package/.claude/hooks/auto-learn.cjs +0 -304
  146. package/.claude/hooks/bash-guard.cjs +0 -272
  147. package/.claude/hooks/builder-mode-manager.cjs +0 -354
  148. package/.claude/hooks/bulk-activity-guard.cjs +0 -271
  149. package/.claude/hooks/context-watchdog.cjs +0 -230
  150. package/.claude/hooks/delegation-reminder.cjs +0 -465
  151. package/.claude/hooks/design-system-lint.cjs +0 -271
  152. package/.claude/hooks/post-scaffold-hook.cjs +0 -181
  153. package/.claude/hooks/prompt-guard.cjs +0 -354
  154. package/.claude/hooks/publish-template-guard.cjs +0 -147
  155. package/.claude/hooks/session-start.cjs +0 -35
  156. package/.claude/hooks/shared-memory-writer.cjs +0 -147
  157. package/.claude/hooks/skill-injector.cjs +0 -140
  158. package/.claude/hooks/skill-usage-logger.cjs +0 -258
  159. package/.claude/hooks/src-edit-guard.cjs +0 -240
  160. package/.claude/hooks/sync-marketplace-agents.cjs +0 -346
  161. package/.claude/settings.json +0 -257
  162. package/.claude/skills/SDK-activity-patterns/SKILL.md +0 -428
  163. package/.claude/skills/SDK-document-templates/SKILL.md +0 -1033
  164. package/.claude/skills/SDK-function-fields/SKILL.md +0 -542
  165. package/.claude/skills/SDK-generate-skill/SKILL.md +0 -92
  166. package/.claude/skills/SDK-init-skill/SKILL.md +0 -127
  167. package/.claude/skills/SDK-insight-queries/SKILL.md +0 -787
  168. package/.claude/skills/SDK-ws-config-skill/SKILL.md +0 -1139
  169. package/.claude/skills/agent-structure/SKILL.md +0 -98
  170. package/.claude/skills/api-documentation-patterns/SKILL.md +0 -474
  171. package/.claude/skills/chrome-mcp-reference/SKILL.md +0 -370
  172. package/.claude/skills/delegation-routing/SKILL.md +0 -202
  173. package/.claude/skills/frontend-design/SKILL.md +0 -254
  174. package/.claude/skills/hailer-activity-mover/SKILL.md +0 -213
  175. package/.claude/skills/hailer-api-client/SKILL.md +0 -518
  176. package/.claude/skills/hailer-app-builder/SKILL.md +0 -1434
  177. package/.claude/skills/hailer-apps-pictures/SKILL.md +0 -269
  178. package/.claude/skills/hailer-design-system/SKILL.md +0 -235
  179. package/.claude/skills/hailer-monolith-automations/SKILL.md +0 -686
  180. package/.claude/skills/hailer-permissions-system/SKILL.md +0 -121
  181. package/.claude/skills/hailer-project-protocol/SKILL.md +0 -488
  182. package/.claude/skills/hailer-rest-api/SKILL.md +0 -61
  183. package/.claude/skills/hailer-rest-api/hailer-activities.md +0 -184
  184. package/.claude/skills/hailer-rest-api/hailer-admin.md +0 -473
  185. package/.claude/skills/hailer-rest-api/hailer-calendar.md +0 -256
  186. package/.claude/skills/hailer-rest-api/hailer-feed.md +0 -249
  187. package/.claude/skills/hailer-rest-api/hailer-insights.md +0 -195
  188. package/.claude/skills/hailer-rest-api/hailer-messaging.md +0 -276
  189. package/.claude/skills/hailer-rest-api/hailer-workflows.md +0 -283
  190. package/.claude/skills/insight-join-patterns/SKILL.md +0 -174
  191. package/.claude/skills/integration-patterns/SKILL.md +0 -421
  192. package/.claude/skills/json-only-output/SKILL.md +0 -72
  193. package/.claude/skills/lsp-setup/SKILL.md +0 -160
  194. package/.claude/skills/mcp-direct-tools/SKILL.md +0 -153
  195. package/.claude/skills/optional-parameters/SKILL.md +0 -72
  196. package/.claude/skills/publish-hailer-app/SKILL.md +0 -244
  197. package/.claude/skills/testing-patterns/SKILL.md +0 -630
  198. package/.claude/skills/tool-builder/SKILL.md +0 -250
  199. package/.claude/skills/tool-parameter-usage/SKILL.md +0 -126
  200. package/.claude/skills/tool-response-verification/SKILL.md +0 -92
  201. package/.claude/skills/zapier-hailer-patterns/SKILL.md +0 -581
  202. package/.mcp.json +0 -13
  203. package/.opencode/agent/agent-ada-skill-builder.md +0 -35
  204. package/.opencode/agent/agent-alejandro-function-fields.md +0 -39
  205. package/.opencode/agent/agent-bjorn-config-audit.md +0 -36
  206. package/.opencode/agent/agent-builder-agent-creator.md +0 -39
  207. package/.opencode/agent/agent-code-simplifier.md +0 -31
  208. package/.opencode/agent/agent-dmitri-activity-crud.md +0 -40
  209. package/.opencode/agent/agent-giuseppe-app-builder.md +0 -37
  210. package/.opencode/agent/agent-gunther-mcp-tools.md +0 -39
  211. package/.opencode/agent/agent-helga-workflow-config.md +0 -203
  212. package/.opencode/agent/agent-igor-activity-mover-automation.md +0 -46
  213. package/.opencode/agent/agent-ingrid-doc-templates.md +0 -39
  214. package/.opencode/agent/agent-ivan-monolith.md +0 -46
  215. package/.opencode/agent/agent-kenji-data-reader.md +0 -53
  216. package/.opencode/agent/agent-lars-code-inspector.md +0 -28
  217. package/.opencode/agent/agent-marco-mockup-builder.md +0 -42
  218. package/.opencode/agent/agent-marcus-api-documenter.md +0 -53
  219. package/.opencode/agent/agent-marketplace-publisher.md +0 -44
  220. package/.opencode/agent/agent-marketplace-reviewer.md +0 -42
  221. package/.opencode/agent/agent-permissions-handler.md +0 -50
  222. package/.opencode/agent/agent-simple-writer.md +0 -45
  223. package/.opencode/agent/agent-svetlana-code-review.md +0 -39
  224. package/.opencode/agent/agent-tanya-test-runner.md +0 -57
  225. package/.opencode/agent/agent-ui-designer.md +0 -56
  226. package/.opencode/agent/agent-viktor-sql-insights.md +0 -34
  227. package/.opencode/agent/agent-web-search.md +0 -42
  228. package/.opencode/agent/agent-yevgeni-discussions.md +0 -37
  229. package/.opencode/agent/agent-zara-zapier.md +0 -53
  230. package/.opencode/commands/app-squad.md +0 -135
  231. package/.opencode/commands/audit-squad.md +0 -158
  232. package/.opencode/commands/autoplan.md +0 -563
  233. package/.opencode/commands/cleanup-squad.md +0 -98
  234. package/.opencode/commands/config-squad.md +0 -106
  235. package/.opencode/commands/crud-squad.md +0 -87
  236. package/.opencode/commands/data-squad.md +0 -97
  237. package/.opencode/commands/debug-squad.md +0 -303
  238. package/.opencode/commands/doc-squad.md +0 -65
  239. package/.opencode/commands/handoff.md +0 -137
  240. package/.opencode/commands/health.md +0 -49
  241. package/.opencode/commands/help-agents.md +0 -151
  242. package/.opencode/commands/help-commands.md +0 -32
  243. package/.opencode/commands/help-faq.md +0 -29
  244. package/.opencode/commands/help-plugins.md +0 -28
  245. package/.opencode/commands/help-skills.md +0 -7
  246. package/.opencode/commands/help-tools.md +0 -40
  247. package/.opencode/commands/help.md +0 -28
  248. package/.opencode/commands/hotfix-squad.md +0 -112
  249. package/.opencode/commands/integration-squad.md +0 -82
  250. package/.opencode/commands/janitor-squad.md +0 -167
  251. package/.opencode/commands/learn-auto.md +0 -120
  252. package/.opencode/commands/learn.md +0 -120
  253. package/.opencode/commands/mcp-list.md +0 -27
  254. package/.opencode/commands/onboard-squad.md +0 -140
  255. package/.opencode/commands/plan-workspace.md +0 -732
  256. package/.opencode/commands/prd.md +0 -131
  257. package/.opencode/commands/project-status.md +0 -82
  258. package/.opencode/commands/publish.md +0 -138
  259. package/.opencode/commands/recap.md +0 -69
  260. package/.opencode/commands/restore.md +0 -64
  261. package/.opencode/commands/review-squad.md +0 -152
  262. package/.opencode/commands/save.md +0 -24
  263. package/.opencode/commands/stats.md +0 -19
  264. package/.opencode/commands/swarm.md +0 -210
  265. package/.opencode/commands/tool-builder.md +0 -39
  266. package/.opencode/commands/ws-pull.md +0 -44
  267. package/.opencode/opencode.json +0 -28
  268. package/SESSION-HANDOFF.md +0 -68
  269. package/inbox/2026-03-04-bot-config-patterns.md +0 -24
  270. package/scripts/postinstall.cjs +0 -64
  271. 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