@hed-hog/core 0.0.400 → 0.0.402
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-log/access-log.interceptor.d.ts +10 -0
- package/dist/access-log/access-log.interceptor.d.ts.map +1 -1
- package/dist/access-log/access-log.interceptor.js +15 -1
- package/dist/access-log/access-log.interceptor.js.map +1 -1
- package/dist/ai/ai.controller.d.ts +2 -0
- package/dist/ai/ai.controller.d.ts.map +1 -1
- package/dist/ai/ai.service.d.ts +86 -2
- package/dist/ai/ai.service.d.ts.map +1 -1
- package/dist/ai/ai.service.js +304 -23
- package/dist/ai/ai.service.js.map +1 -1
- package/dist/ai-instruction/ai-prompt-resolver.service.d.ts +12 -0
- package/dist/ai-instruction/ai-prompt-resolver.service.d.ts.map +1 -1
- package/dist/ai-instruction/ai-prompt-resolver.service.js +26 -0
- package/dist/ai-instruction/ai-prompt-resolver.service.js.map +1 -1
- package/dist/core.module.d.ts.map +1 -1
- package/dist/core.module.js +3 -0
- package/dist/core.module.js.map +1 -1
- package/dist/document-kit/document-kit.module.d.ts +3 -0
- package/dist/document-kit/document-kit.module.d.ts.map +1 -0
- package/dist/document-kit/document-kit.module.js +24 -0
- package/dist/document-kit/document-kit.module.js.map +1 -0
- package/dist/document-kit/document-kit.registry.d.ts +55 -0
- package/dist/document-kit/document-kit.registry.d.ts.map +1 -0
- package/dist/document-kit/document-kit.registry.js +91 -0
- package/dist/document-kit/document-kit.registry.js.map +1 -0
- package/dist/document-kit/document-kit.service.d.ts +49 -0
- package/dist/document-kit/document-kit.service.d.ts.map +1 -0
- package/dist/document-kit/document-kit.service.js +135 -0
- package/dist/document-kit/document-kit.service.js.map +1 -0
- package/dist/file/file-upload-ticket.service.d.ts +1 -1
- package/dist/file/file.controller.d.ts +3 -3
- package/dist/file/file.service.d.ts +136 -13
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +297 -27
- package/dist/file/file.service.js.map +1 -1
- package/dist/file/mcp-file-source.schema.d.ts.map +1 -1
- package/dist/file/mcp-file-source.schema.js +8 -5
- package/dist/file/mcp-file-source.schema.js.map +1 -1
- package/dist/file/provider/abstract.provider.d.ts +33 -0
- package/dist/file/provider/abstract.provider.d.ts.map +1 -1
- package/dist/file/provider/abstract.provider.js +9 -0
- package/dist/file/provider/abstract.provider.js.map +1 -1
- package/dist/file/provider/s3-multipart.util.d.ts +17 -0
- package/dist/file/provider/s3-multipart.util.d.ts.map +1 -0
- package/dist/file/provider/s3-multipart.util.js +88 -0
- package/dist/file/provider/s3-multipart.util.js.map +1 -0
- package/dist/file/provider/s3.provider.d.ts +9 -1
- package/dist/file/provider/s3.provider.d.ts.map +1 -1
- package/dist/file/provider/s3.provider.js +16 -0
- package/dist/file/provider/s3.provider.js.map +1 -1
- package/dist/file/provider/spaces.provider.d.ts +10 -1
- package/dist/file/provider/spaces.provider.d.ts.map +1 -1
- package/dist/file/provider/spaces.provider.js +17 -0
- package/dist/file/provider/spaces.provider.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/integration-profile/integration-profile.controller.d.ts +1 -0
- package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.js +26 -0
- package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
- package/dist/integration-profile/integration-profile.service.d.ts +1 -0
- package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.service.js +3 -3
- package/dist/integration-profile/integration-profile.service.js.map +1 -1
- package/dist/integration-profile/testers/ai.testers.d.ts.map +1 -1
- package/dist/integration-profile/testers/ai.testers.js +46 -8
- package/dist/integration-profile/testers/ai.testers.js.map +1 -1
- package/dist/integration-profile/testers/http-probe.d.ts.map +1 -1
- package/dist/integration-profile/testers/http-probe.js +5 -3
- package/dist/integration-profile/testers/http-probe.js.map +1 -1
- package/dist/integration-profile/testers/integration-tester.types.d.ts +5 -0
- package/dist/integration-profile/testers/integration-tester.types.d.ts.map +1 -1
- package/dist/mail/mail.service.d.ts.map +1 -1
- package/dist/mail/mail.service.js +16 -1
- package/dist/mail/mail.service.js.map +1 -1
- package/dist/mcp/mcp-server.service.d.ts.map +1 -1
- package/dist/mcp/mcp-server.service.js +10 -1
- package/dist/mcp/mcp-server.service.js.map +1 -1
- package/dist/mcp/mcp.module.d.ts.map +1 -1
- package/dist/mcp/mcp.module.js +2 -0
- package/dist/mcp/mcp.module.js.map +1 -1
- package/dist/mcp/tools/core-access-log.mcp-tools.d.ts +17 -0
- package/dist/mcp/tools/core-access-log.mcp-tools.d.ts.map +1 -0
- package/dist/mcp/tools/core-access-log.mcp-tools.js +74 -0
- package/dist/mcp/tools/core-access-log.mcp-tools.js.map +1 -0
- package/dist/mcp/tools/core-files.mcp-tools.js +4 -4
- package/dist/mcp/tools/core-files.mcp-tools.js.map +1 -1
- package/dist/meeting/meeting-provider.registry.d.ts +72 -0
- package/dist/meeting/meeting-provider.registry.d.ts.map +1 -1
- package/dist/meeting/meeting-provider.registry.js.map +1 -1
- package/dist/oauth/oauth-token-broker.service.d.ts +50 -1
- package/dist/oauth/oauth-token-broker.service.d.ts.map +1 -1
- package/dist/oauth/oauth-token-broker.service.js +105 -3
- package/dist/oauth/oauth-token-broker.service.js.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +6 -0
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.js +30 -4
- package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
- package/dist/pdf/document/charts/chart-bar.d.ts +24 -0
- package/dist/pdf/document/charts/chart-bar.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-bar.js +117 -0
- package/dist/pdf/document/charts/chart-bar.js.map +1 -0
- package/dist/pdf/document/charts/chart-donut.d.ts +16 -0
- package/dist/pdf/document/charts/chart-donut.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-donut.js +89 -0
- package/dist/pdf/document/charts/chart-donut.js.map +1 -0
- package/dist/pdf/document/charts/chart-gauge.d.ts +15 -0
- package/dist/pdf/document/charts/chart-gauge.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-gauge.js +60 -0
- package/dist/pdf/document/charts/chart-gauge.js.map +1 -0
- package/dist/pdf/document/charts/chart-line.d.ts +29 -0
- package/dist/pdf/document/charts/chart-line.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-line.js +89 -0
- package/dist/pdf/document/charts/chart-line.js.map +1 -0
- package/dist/pdf/document/charts/chart-primitives.d.ts +64 -0
- package/dist/pdf/document/charts/chart-primitives.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-primitives.js +110 -0
- package/dist/pdf/document/charts/chart-primitives.js.map +1 -0
- package/dist/pdf/document/charts/chart-radar.d.ts +13 -0
- package/dist/pdf/document/charts/chart-radar.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-radar.js +83 -0
- package/dist/pdf/document/charts/chart-radar.js.map +1 -0
- package/dist/pdf/document/charts/chart-scale.d.ts +39 -0
- package/dist/pdf/document/charts/chart-scale.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-scale.js +118 -0
- package/dist/pdf/document/charts/chart-scale.js.map +1 -0
- package/dist/pdf/document/charts/index.d.ts +8 -0
- package/dist/pdf/document/charts/index.d.ts.map +1 -0
- package/dist/pdf/document/charts/index.js +24 -0
- package/dist/pdf/document/charts/index.js.map +1 -0
- package/dist/pdf/document/index.d.ts +8 -0
- package/dist/pdf/document/index.d.ts.map +1 -0
- package/dist/pdf/document/index.js +24 -0
- package/dist/pdf/document/index.js.map +1 -0
- package/dist/pdf/document/pdf-document.chrome.d.ts +20 -0
- package/dist/pdf/document/pdf-document.chrome.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.chrome.js +36 -0
- package/dist/pdf/document/pdf-document.chrome.js.map +1 -0
- package/dist/pdf/document/pdf-document.css.d.ts +20 -0
- package/dist/pdf/document/pdf-document.css.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.css.js +327 -0
- package/dist/pdf/document/pdf-document.css.js.map +1 -0
- package/dist/pdf/document/pdf-document.renderer.d.ts +10 -0
- package/dist/pdf/document/pdf-document.renderer.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.renderer.js +226 -0
- package/dist/pdf/document/pdf-document.renderer.js.map +1 -0
- package/dist/pdf/document/pdf-document.service.d.ts +27 -0
- package/dist/pdf/document/pdf-document.service.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.service.js +81 -0
- package/dist/pdf/document/pdf-document.service.js.map +1 -0
- package/dist/pdf/document/pdf-document.theme.d.ts +41 -0
- package/dist/pdf/document/pdf-document.theme.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.theme.js +105 -0
- package/dist/pdf/document/pdf-document.theme.js.map +1 -0
- package/dist/pdf/document/pdf-document.types.d.ts +164 -0
- package/dist/pdf/document/pdf-document.types.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.types.js +18 -0
- package/dist/pdf/document/pdf-document.types.js.map +1 -0
- package/dist/pdf/pdf.module.d.ts.map +1 -1
- package/dist/pdf/pdf.module.js +3 -2
- package/dist/pdf/pdf.module.js.map +1 -1
- package/dist/pdf/pdf.service.d.ts +75 -1
- package/dist/pdf/pdf.service.d.ts.map +1 -1
- package/dist/pdf/pdf.service.js +167 -63
- package/dist/pdf/pdf.service.js.map +1 -1
- package/dist/pdf/templates/commercial-proposal.template.d.ts +96 -0
- package/dist/pdf/templates/commercial-proposal.template.d.ts.map +1 -0
- package/dist/pdf/templates/commercial-proposal.template.js +276 -0
- package/dist/pdf/templates/commercial-proposal.template.js.map +1 -0
- package/dist/setting/favicon.controller.d.ts +26 -0
- package/dist/setting/favicon.controller.d.ts.map +1 -0
- package/dist/setting/favicon.controller.js +130 -0
- package/dist/setting/favicon.controller.js.map +1 -0
- package/dist/setting/setting.module.d.ts.map +1 -1
- package/dist/setting/setting.module.js +2 -1
- package/dist/setting/setting.module.js.map +1 -1
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +3 -0
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/task/task.service.d.ts +6 -0
- package/dist/task/task.service.d.ts.map +1 -1
- package/dist/task/task.service.js +44 -13
- package/dist/task/task.service.js.map +1 -1
- package/dist/whatsapp-sender/whatsapp-sender.service.d.ts +35 -2
- package/dist/whatsapp-sender/whatsapp-sender.service.d.ts.map +1 -1
- package/dist/whatsapp-sender/whatsapp-sender.service.js +107 -22
- package/dist/whatsapp-sender/whatsapp-sender.service.js.map +1 -1
- package/hedhog/data/integration_type.yaml +6 -0
- package/hedhog/data/route.yaml +8 -0
- package/hedhog/data/setting_group.yaml +52 -0
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.test.tsx.ejs +85 -2
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +576 -491
- package/hedhog/frontend/app/users/simulate-access-dialog.test.tsx.ejs +29 -50
- package/hedhog/frontend/app/users/simulate-access-dialog.tsx.ejs +146 -50
- package/hedhog/frontend/messages/en.json +81 -2
- package/hedhog/frontend/messages/pt.json +81 -2
- package/hedhog/table/file.yaml +3 -1
- package/package.json +5 -5
- package/src/access-log/access-log.interceptor.spec.ts +50 -0
- package/src/access-log/access-log.interceptor.ts +18 -0
- package/src/ai/ai-attachment.fragments.spec.ts +56 -3
- package/src/ai/ai.service.responses.spec.ts +429 -0
- package/src/ai/ai.service.ts +409 -27
- package/src/ai-instruction/ai-prompt-resolver.service.spec.ts +44 -0
- package/src/ai-instruction/ai-prompt-resolver.service.ts +29 -0
- package/src/core.module.ts +3 -0
- package/src/document-kit/document-kit.module.ts +12 -0
- package/src/document-kit/document-kit.registry.ts +125 -0
- package/src/document-kit/document-kit.service.spec.ts +156 -0
- package/src/document-kit/document-kit.service.ts +173 -0
- package/src/file/file-direct-upload.spec.ts +344 -0
- package/src/file/file.service.spec.ts +168 -1
- package/src/file/file.service.ts +435 -36
- package/src/file/mcp-file-source.schema.ts +8 -5
- package/src/file/provider/abstract.provider.ts +57 -0
- package/src/file/provider/s3-multipart.spec.ts +123 -0
- package/src/file/provider/s3-multipart.util.ts +126 -0
- package/src/file/provider/s3.provider.ts +65 -1
- package/src/file/provider/spaces.provider.ts +67 -1
- package/src/index.ts +5 -0
- package/src/integration-profile/integration-profile.secrets.ts +26 -0
- package/src/integration-profile/integration-profile.service.ts +1 -0
- package/src/integration-profile/testers/ai.testers.ts +70 -5
- package/src/integration-profile/testers/http-probe.ts +5 -3
- package/src/integration-profile/testers/integration-tester.types.ts +5 -0
- package/src/integration-profile/testers/oauth.testers.spec.ts +13 -0
- package/src/integration-profile/testers/remaining.testers.spec.ts +65 -1
- package/src/integration-profile/testers/tester-coverage.spec.ts +12 -0
- package/src/mail/mail.service.spec.ts +25 -0
- package/src/mail/mail.service.ts +18 -1
- package/src/mcp/mcp-server.service.spec.ts +16 -0
- package/src/mcp/mcp-server.service.ts +10 -1
- package/src/mcp/mcp.module.ts +2 -0
- package/src/mcp/tools/core-access-log.mcp-tools.ts +66 -0
- package/src/mcp/tools/core-files.mcp-tools.ts +4 -4
- package/src/meeting/meeting-provider.registry.ts +80 -0
- package/src/oauth/oauth-token-broker.service.spec.ts +108 -0
- package/src/oauth/oauth-token-broker.service.ts +129 -2
- package/src/oauth/providers/microsoft-entra-id.provider.spec.ts +133 -0
- package/src/oauth/providers/microsoft-entra-id.provider.ts +29 -2
- package/src/pdf/document/charts/chart-bar.ts +217 -0
- package/src/pdf/document/charts/chart-donut.ts +163 -0
- package/src/pdf/document/charts/chart-gauge.ts +94 -0
- package/src/pdf/document/charts/chart-line.ts +159 -0
- package/src/pdf/document/charts/chart-primitives.ts +188 -0
- package/src/pdf/document/charts/chart-radar.ts +136 -0
- package/src/pdf/document/charts/chart-scale.spec.ts +93 -0
- package/src/pdf/document/charts/chart-scale.ts +141 -0
- package/src/pdf/document/charts/charts.spec.ts +157 -0
- package/src/pdf/document/charts/index.ts +7 -0
- package/src/pdf/document/index.ts +7 -0
- package/src/pdf/document/pdf-document.chrome.ts +38 -0
- package/src/pdf/document/pdf-document.css.ts +326 -0
- package/src/pdf/document/pdf-document.renderer.spec.ts +172 -0
- package/src/pdf/document/pdf-document.renderer.ts +317 -0
- package/src/pdf/document/pdf-document.service.ts +77 -0
- package/src/pdf/document/pdf-document.theme.ts +146 -0
- package/src/pdf/document/pdf-document.types.ts +149 -0
- package/src/pdf/pdf.module.ts +3 -2
- package/src/pdf/pdf.service.spec.ts +131 -0
- package/src/pdf/pdf.service.ts +256 -82
- package/src/pdf/templates/commercial-proposal.template.spec.ts +177 -0
- package/src/pdf/templates/commercial-proposal.template.ts +400 -0
- package/src/setting/favicon.controller.spec.ts +92 -0
- package/src/setting/favicon.controller.ts +135 -0
- package/src/setting/setting.module.ts +2 -1
- package/src/setting/setting.service.ts +3 -0
- package/src/task/task.service.spec.ts +77 -1
- package/src/task/task.service.ts +58 -14
- package/src/whatsapp-sender/whatsapp-sender.service.spec.ts +263 -0
- package/src/whatsapp-sender/whatsapp-sender.service.ts +154 -16
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { buildDocumentCss } from './pdf-document.css';
|
|
2
|
+
import { PdfDocumentTheme } from './pdf-document.theme';
|
|
3
|
+
import {
|
|
4
|
+
PdfDocumentBlock,
|
|
5
|
+
PdfDocumentCard,
|
|
6
|
+
PdfDocumentInput,
|
|
7
|
+
PdfDocumentProgressCard,
|
|
8
|
+
PdfDocumentRow,
|
|
9
|
+
PdfDocumentSegment,
|
|
10
|
+
PdfDocumentSegmentTone,
|
|
11
|
+
PdfDocumentTableCell,
|
|
12
|
+
PdfDocumentTableColumn,
|
|
13
|
+
} from './pdf-document.types';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Único ponto por onde texto entra no HTML. Nome de aluno, título de curso e
|
|
17
|
+
* rótulo de tarefa são dados de usuário indo para um Chromium headless.
|
|
18
|
+
*/
|
|
19
|
+
export function escapeHtml(value: unknown): string {
|
|
20
|
+
if (value === null || value === undefined) return '';
|
|
21
|
+
|
|
22
|
+
return String(value)
|
|
23
|
+
.replace(/&/g, '&')
|
|
24
|
+
.replace(/</g, '<')
|
|
25
|
+
.replace(/>/g, '>')
|
|
26
|
+
.replace(/"/g, '"')
|
|
27
|
+
.replace(/'/g, ''');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const formatGeneratedAt = (date: Date, locale: string): string =>
|
|
31
|
+
new Intl.DateTimeFormat(locale, {
|
|
32
|
+
day: '2-digit',
|
|
33
|
+
month: 'long',
|
|
34
|
+
year: 'numeric',
|
|
35
|
+
hour: '2-digit',
|
|
36
|
+
minute: '2-digit',
|
|
37
|
+
}).format(date);
|
|
38
|
+
|
|
39
|
+
const toCell = (value: PdfDocumentRow[number]): PdfDocumentTableCell => {
|
|
40
|
+
if (value === null || value === undefined) return { text: '—', tone: 'muted' };
|
|
41
|
+
if (typeof value === 'object') return value;
|
|
42
|
+
return { text: String(value) };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const alignClass = (column: PdfDocumentTableColumn): string =>
|
|
46
|
+
column.align === 'right'
|
|
47
|
+
? ' class="align-right"'
|
|
48
|
+
: column.align === 'center'
|
|
49
|
+
? ' class="align-center"'
|
|
50
|
+
: '';
|
|
51
|
+
|
|
52
|
+
function renderCards(items: PdfDocumentCard[], columns: number, variant: string): string {
|
|
53
|
+
const cards = items
|
|
54
|
+
.map(
|
|
55
|
+
(item) =>
|
|
56
|
+
`<div class="card"><div class="card-label">${escapeHtml(item.label)}</div>` +
|
|
57
|
+
`<div class="card-value">${escapeHtml(item.value)}</div>` +
|
|
58
|
+
(item.hint ? `<div class="card-hint">${escapeHtml(item.hint)}</div>` : '') +
|
|
59
|
+
'</div>',
|
|
60
|
+
)
|
|
61
|
+
.join('');
|
|
62
|
+
|
|
63
|
+
return `<div class="cards cards-${columns} cards-${variant}">${cards}</div>`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function renderTable(block: Extract<PdfDocumentBlock, { kind: 'table' }>): string {
|
|
67
|
+
const {
|
|
68
|
+
columns,
|
|
69
|
+
rows,
|
|
70
|
+
numbered = false,
|
|
71
|
+
numberHeader = '#',
|
|
72
|
+
zebra = true,
|
|
73
|
+
compact = false,
|
|
74
|
+
emptyText = 'Nenhum registro no recorte selecionado.',
|
|
75
|
+
truncatedNote,
|
|
76
|
+
} = block;
|
|
77
|
+
|
|
78
|
+
if (rows.length === 0) {
|
|
79
|
+
return `<div class="table-empty">${escapeHtml(emptyText)}</div>`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const head =
|
|
83
|
+
(numbered ? `<th class="idx">${escapeHtml(numberHeader)}</th>` : '') +
|
|
84
|
+
columns
|
|
85
|
+
.map(
|
|
86
|
+
(column) =>
|
|
87
|
+
`<th${alignClass(column)}${column.width ? ` style="width:${escapeHtml(column.width)}"` : ''}>` +
|
|
88
|
+
`${escapeHtml(column.header)}</th>`,
|
|
89
|
+
)
|
|
90
|
+
.join('');
|
|
91
|
+
|
|
92
|
+
// `push` + `join` em vez de concatenar em laço: uma conta grande gera milhares
|
|
93
|
+
// de linhas, e a string intermediária de cada passo é o que faz o render
|
|
94
|
+
// arrastar.
|
|
95
|
+
const body: string[] = [];
|
|
96
|
+
|
|
97
|
+
rows.forEach((row, index) => {
|
|
98
|
+
const cells: string[] = [];
|
|
99
|
+
|
|
100
|
+
if (numbered) cells.push(`<td class="idx">${index + 1}</td>`);
|
|
101
|
+
|
|
102
|
+
columns.forEach((column, columnIndex) => {
|
|
103
|
+
const cell = toCell(row[columnIndex]);
|
|
104
|
+
const tone = cell.tone && cell.tone !== 'default' ? ` tone-${cell.tone}` : '';
|
|
105
|
+
const align =
|
|
106
|
+
column.align === 'right' ? ' align-right' : column.align === 'center' ? ' align-center' : '';
|
|
107
|
+
|
|
108
|
+
cells.push(
|
|
109
|
+
`<td class="${(tone + align).trim()}">${escapeHtml(cell.text)}` +
|
|
110
|
+
(cell.sub ? `<div class="cell-sub">${escapeHtml(cell.sub)}</div>` : '') +
|
|
111
|
+
'</td>',
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
body.push(`<tr>${cells.join('')}</tr>`);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const table =
|
|
119
|
+
`<table class="doc-table${zebra ? ' zebra' : ''}${compact ? ' compact' : ''}">` +
|
|
120
|
+
`<thead><tr>${head}</tr></thead><tbody>${body.join('')}</tbody></table>`;
|
|
121
|
+
|
|
122
|
+
return truncatedNote ? `${table}<div class="note">${escapeHtml(truncatedNote)}</div>` : table;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Barra fina empilhada. Largura em % do total; total zero vira barra vazia. */
|
|
126
|
+
function renderSegments(segments: PdfDocumentSegment[]): string {
|
|
127
|
+
const total = segments.reduce((sum, segment) => sum + Math.max(0, segment.value), 0);
|
|
128
|
+
const parts =
|
|
129
|
+
total > 0
|
|
130
|
+
? segments
|
|
131
|
+
.filter((segment) => segment.value > 0)
|
|
132
|
+
.map(
|
|
133
|
+
(segment) =>
|
|
134
|
+
`<span class="seg seg-${segment.tone}" style="width:${((segment.value / total) * 100).toFixed(2)}%"></span>`,
|
|
135
|
+
)
|
|
136
|
+
.join('')
|
|
137
|
+
: '';
|
|
138
|
+
|
|
139
|
+
return `<div class="seg-bar">${parts}</div>`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const dot = (tone: PdfDocumentSegmentTone): string => `<span class="dot dot-${tone}"></span>`;
|
|
143
|
+
|
|
144
|
+
function renderSegmentBar(block: Extract<PdfDocumentBlock, { kind: 'segmentBar' }>): string {
|
|
145
|
+
const total = block.segments.reduce((sum, segment) => sum + Math.max(0, segment.value), 0);
|
|
146
|
+
const legend = block.segments
|
|
147
|
+
.map((segment) => {
|
|
148
|
+
const pct = total > 0 ? Math.round((segment.value / total) * 100) : 0;
|
|
149
|
+
return (
|
|
150
|
+
`<span class="seg-legend-item">${dot(segment.tone)}${escapeHtml(segment.label ?? '')} ` +
|
|
151
|
+
`<strong>${segment.value} (${pct}%)</strong></span>`
|
|
152
|
+
);
|
|
153
|
+
})
|
|
154
|
+
.join('');
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
'<div class="seg-block">' +
|
|
158
|
+
(block.title ? `<div class="seg-title">${escapeHtml(block.title)}</div>` : '') +
|
|
159
|
+
renderSegments(block.segments) +
|
|
160
|
+
`<div class="seg-legend">${legend}</div>` +
|
|
161
|
+
(block.caption ? `<div class="seg-caption">${escapeHtml(block.caption)}</div>` : '') +
|
|
162
|
+
'</div>'
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function renderProgressCard(card: PdfDocumentProgressCard): string {
|
|
167
|
+
const entries = card.entries
|
|
168
|
+
.map(
|
|
169
|
+
(entry) =>
|
|
170
|
+
`<div class="pc-entry${entry.highlight ? ` pc-entry-hl pc-entry-hl-${entry.tone}` : ''}">` +
|
|
171
|
+
`<div class="pc-entry-text">${escapeHtml(entry.text)}</div>` +
|
|
172
|
+
'<div class="pc-entry-meta">' +
|
|
173
|
+
`<span class="pc-entry-status">${dot(entry.tone)}${escapeHtml(entry.status)}</span>` +
|
|
174
|
+
(entry.aside ? `<span class="pc-entry-aside">${escapeHtml(entry.aside)}</span>` : '') +
|
|
175
|
+
'</div>' +
|
|
176
|
+
(entry.sub
|
|
177
|
+
? `<div class="pc-entry-sub${entry.subTone === 'danger' ? ' pc-entry-sub-danger' : ''}">${escapeHtml(entry.sub)}</div>`
|
|
178
|
+
: '') +
|
|
179
|
+
'</div>',
|
|
180
|
+
)
|
|
181
|
+
.join('');
|
|
182
|
+
|
|
183
|
+
const body =
|
|
184
|
+
card.entries.length > 0
|
|
185
|
+
? (card.listTitle ? `<div class="pc-list-title">${escapeHtml(card.listTitle)}</div>` : '') + entries
|
|
186
|
+
: card.emptyText
|
|
187
|
+
? `<div class="pc-empty">${escapeHtml(card.emptyText)}</div>`
|
|
188
|
+
: '';
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
'<div class="pc-card">' +
|
|
192
|
+
'<div class="pc-head">' +
|
|
193
|
+
`<div class="pc-head-text"><div class="pc-title">${escapeHtml(card.title)}</div>` +
|
|
194
|
+
(card.subtitle ? `<div class="pc-subtitle">${escapeHtml(card.subtitle)}</div>` : '') +
|
|
195
|
+
'</div>' +
|
|
196
|
+
`<div class="pc-value"><div class="pc-value-num">${escapeHtml(card.value)}</div>` +
|
|
197
|
+
(card.valueHint ? `<div class="pc-value-hint">${escapeHtml(card.valueHint)}</div>` : '') +
|
|
198
|
+
'</div></div>' +
|
|
199
|
+
renderSegments(card.segments) +
|
|
200
|
+
body +
|
|
201
|
+
'</div>'
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function renderProgressCards(block: Extract<PdfDocumentBlock, { kind: 'progressCards' }>): string {
|
|
206
|
+
const legend = block.legend?.length
|
|
207
|
+
? `<div class="pc-legend">${block.legend
|
|
208
|
+
.map((item) => `<span class="seg-legend-item">${dot(item.tone)}${escapeHtml(item.label)}</span>`)
|
|
209
|
+
.join('')}</div>`
|
|
210
|
+
: '';
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
legend +
|
|
214
|
+
`<div class="pc-grid pc-grid-${block.columns ?? 2}">${block.items.map(renderProgressCard).join('')}</div>`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function renderBlock(block: PdfDocumentBlock): string {
|
|
219
|
+
switch (block.kind) {
|
|
220
|
+
case 'section':
|
|
221
|
+
return (
|
|
222
|
+
`<h2>${escapeHtml(block.title)}` +
|
|
223
|
+
(block.hint ? `<span class="section-hint">${escapeHtml(block.hint)}</span>` : '') +
|
|
224
|
+
'</h2>'
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
case 'cards':
|
|
228
|
+
return renderCards(block.items, block.columns ?? 4, block.variant ?? 'metric');
|
|
229
|
+
|
|
230
|
+
case 'table':
|
|
231
|
+
return renderTable(block);
|
|
232
|
+
|
|
233
|
+
case 'chart':
|
|
234
|
+
return (
|
|
235
|
+
'<div class="chart">' +
|
|
236
|
+
(block.title ? `<div class="chart-title">${escapeHtml(block.title)}</div>` : '') +
|
|
237
|
+
block.svg +
|
|
238
|
+
(block.caption ? `<div class="chart-caption">${escapeHtml(block.caption)}</div>` : '') +
|
|
239
|
+
'</div>'
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
case 'chartGrid':
|
|
243
|
+
return (
|
|
244
|
+
'<div class="chart-grid">' +
|
|
245
|
+
block.items
|
|
246
|
+
.map(
|
|
247
|
+
(item) =>
|
|
248
|
+
'<div class="chart">' +
|
|
249
|
+
(item.title ? `<div class="chart-title">${escapeHtml(item.title)}</div>` : '') +
|
|
250
|
+
item.svg +
|
|
251
|
+
(item.caption ? `<div class="chart-caption">${escapeHtml(item.caption)}</div>` : '') +
|
|
252
|
+
'</div>',
|
|
253
|
+
)
|
|
254
|
+
.join('') +
|
|
255
|
+
'</div>'
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
case 'segmentBar':
|
|
259
|
+
return renderSegmentBar(block);
|
|
260
|
+
|
|
261
|
+
case 'progressCards':
|
|
262
|
+
return renderProgressCards(block);
|
|
263
|
+
|
|
264
|
+
case 'note':
|
|
265
|
+
return `<div class="note${block.tone === 'accent' ? ' note-accent' : ''}">${escapeHtml(block.text)}</div>`;
|
|
266
|
+
|
|
267
|
+
case 'legend':
|
|
268
|
+
return `<div class="legend">${block.lines.map((l) => `<p>${escapeHtml(l)}</p>`).join('')}</div>`;
|
|
269
|
+
|
|
270
|
+
case 'pageBreak':
|
|
271
|
+
return '<div class="page-break"></div>';
|
|
272
|
+
|
|
273
|
+
default:
|
|
274
|
+
return '';
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function renderDocumentHtml(input: PdfDocumentInput, theme: PdfDocumentTheme): string {
|
|
279
|
+
const locale = input.locale ?? 'pt-BR';
|
|
280
|
+
const generatedAt = input.generatedAt ?? new Date();
|
|
281
|
+
const logo = input.logoDataUri || theme.logoUrl;
|
|
282
|
+
|
|
283
|
+
const meta = (input.meta ?? [])
|
|
284
|
+
.map(
|
|
285
|
+
(item) =>
|
|
286
|
+
`<div>${escapeHtml(item.label)}: <span class="meta-value">${escapeHtml(item.value)}</span></div>`,
|
|
287
|
+
)
|
|
288
|
+
.join('');
|
|
289
|
+
|
|
290
|
+
const body = input.blocks.map((block) => renderBlock(block)).join('\n');
|
|
291
|
+
|
|
292
|
+
return `<!DOCTYPE html>
|
|
293
|
+
<html lang="${escapeHtml(locale)}">
|
|
294
|
+
<head>
|
|
295
|
+
<meta charset="utf-8" />
|
|
296
|
+
<title>${escapeHtml(input.title)}</title>
|
|
297
|
+
<style>${buildDocumentCss(theme, input.landscape ?? false)}</style>
|
|
298
|
+
</head>
|
|
299
|
+
<body>
|
|
300
|
+
<header>
|
|
301
|
+
<div class="brand">
|
|
302
|
+
<img src="${escapeHtml(logo)}" alt="${escapeHtml(theme.companyName)}" />
|
|
303
|
+
<div class="brand-text">
|
|
304
|
+
${input.eyebrow ? `<div class="eyebrow">${escapeHtml(input.eyebrow)}</div>` : ''}
|
|
305
|
+
<h1>${escapeHtml(input.title)}</h1>
|
|
306
|
+
${input.subtitle ? `<div class="company-name">${escapeHtml(input.subtitle)}</div>` : ''}
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
<div class="meta-block">
|
|
310
|
+
<div>Gerado em: <span class="meta-value">${escapeHtml(formatGeneratedAt(generatedAt, locale))}</span></div>
|
|
311
|
+
${meta}
|
|
312
|
+
</div>
|
|
313
|
+
</header>
|
|
314
|
+
${body}
|
|
315
|
+
</body>
|
|
316
|
+
</html>`;
|
|
317
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Inject, Injectable, forwardRef } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import { BrandAssetService } from '../../setting/brand-asset.service';
|
|
4
|
+
import { SettingService } from '../../setting/setting.service';
|
|
5
|
+
import { PdfService } from '../pdf.service';
|
|
6
|
+
import {
|
|
7
|
+
PDF_DOCUMENT_MARGIN,
|
|
8
|
+
buildAccentHeaderTemplate,
|
|
9
|
+
buildFooterTemplate,
|
|
10
|
+
} from './pdf-document.chrome';
|
|
11
|
+
import { renderDocumentHtml } from './pdf-document.renderer';
|
|
12
|
+
import {
|
|
13
|
+
PdfDocumentTheme,
|
|
14
|
+
PdfDocumentThemeOverrides,
|
|
15
|
+
resolveDocumentTheme,
|
|
16
|
+
} from './pdf-document.theme';
|
|
17
|
+
import { PdfDocumentInput } from './pdf-document.types';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Gera o documento PDF padrão do sistema: letterhead com logo, cards, tabelas,
|
|
21
|
+
* gráficos, barra de destaque no topo de toda página e paginação "X / N".
|
|
22
|
+
*
|
|
23
|
+
* Quem chama entrega blocos de dados ({@link PdfDocumentInput}); o HTML e o
|
|
24
|
+
* Chromium são responsabilidade daqui.
|
|
25
|
+
*/
|
|
26
|
+
@Injectable()
|
|
27
|
+
export class PdfDocumentService {
|
|
28
|
+
constructor(
|
|
29
|
+
@Inject(forwardRef(() => PdfService))
|
|
30
|
+
private readonly pdf: PdfService,
|
|
31
|
+
@Inject(forwardRef(() => SettingService))
|
|
32
|
+
private readonly settingService: SettingService,
|
|
33
|
+
@Inject(forwardRef(() => BrandAssetService))
|
|
34
|
+
private readonly brandAsset: BrandAssetService,
|
|
35
|
+
) {}
|
|
36
|
+
|
|
37
|
+
loadTheme(overrides?: PdfDocumentThemeOverrides): Promise<PdfDocumentTheme> {
|
|
38
|
+
return resolveDocumentTheme(this.settingService, this.brandAsset, overrides);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** HTML puro, sem Chromium — serve aos testes e a quem quiser inspecionar. */
|
|
42
|
+
async buildHtml(input: PdfDocumentInput, theme?: PdfDocumentTheme): Promise<string> {
|
|
43
|
+
return renderDocumentHtml(input, theme ?? (await this.loadTheme()));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async render(
|
|
47
|
+
input: PdfDocumentInput,
|
|
48
|
+
options: { theme?: PdfDocumentTheme; label?: string; timeoutMs?: number } = {},
|
|
49
|
+
): Promise<Buffer> {
|
|
50
|
+
const theme = options.theme ?? (await this.loadTheme());
|
|
51
|
+
const html = renderDocumentHtml(input, theme);
|
|
52
|
+
|
|
53
|
+
const generatedAt = input.generatedAt ?? new Date();
|
|
54
|
+
const footerLeft = [
|
|
55
|
+
input.footerLeft ?? 'Documento Confidencial',
|
|
56
|
+
`Gerado em ${new Intl.DateTimeFormat(input.locale ?? 'pt-BR').format(generatedAt)}`,
|
|
57
|
+
theme.companyName,
|
|
58
|
+
]
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.join(' · ');
|
|
61
|
+
|
|
62
|
+
return this.pdf.generatePdfFromHtml(html, {
|
|
63
|
+
format: 'A4',
|
|
64
|
+
landscape: input.landscape ?? false,
|
|
65
|
+
printBackground: true,
|
|
66
|
+
margin: PDF_DOCUMENT_MARGIN,
|
|
67
|
+
displayHeaderFooter: true,
|
|
68
|
+
headerTemplate: buildAccentHeaderTemplate(theme),
|
|
69
|
+
footerTemplate: buildFooterTemplate(theme, footerLeft),
|
|
70
|
+
// O logo já vem embutido como data URI e não há fonte web: esperar por
|
|
71
|
+
// rede ociosa só adicionaria o timeout do `networkidle` ao render.
|
|
72
|
+
waitUntil: 'load',
|
|
73
|
+
timeoutMs: options.timeoutMs ?? 60_000,
|
|
74
|
+
label: options.label,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { BrandAssetService } from '../../setting/brand-asset.service';
|
|
2
|
+
import { SettingService } from '../../setting/setting.service';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tokens do documento padrão. São as mesmas settings que a proposta comercial
|
|
6
|
+
* do CRM lê, para que os dois PDFs saiam com a mesma identidade sem um copiar
|
|
7
|
+
* as cores do outro na mão.
|
|
8
|
+
*/
|
|
9
|
+
export interface PdfDocumentTheme {
|
|
10
|
+
primary: string;
|
|
11
|
+
/** Fundo suave dos cards e do cabeçalho da tabela. */
|
|
12
|
+
mutedBg: string;
|
|
13
|
+
/** Texto secundário: rótulos, legendas, rodapé. */
|
|
14
|
+
mutedFg: string;
|
|
15
|
+
/** Texto principal. */
|
|
16
|
+
ink: string;
|
|
17
|
+
/** Bordas. */
|
|
18
|
+
line: string;
|
|
19
|
+
companyName: string;
|
|
20
|
+
/** Data URI ou URL absoluta do logo do sistema. */
|
|
21
|
+
logoUrl: string;
|
|
22
|
+
fontFamily: string;
|
|
23
|
+
/** Cores categóricas dos gráficos, com a primária na frente. */
|
|
24
|
+
chartPalette: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const FALLBACK = {
|
|
28
|
+
primary: '#ff760c',
|
|
29
|
+
mutedBg: '#f4f4f5',
|
|
30
|
+
mutedFg: '#71717a',
|
|
31
|
+
ink: '#18181b',
|
|
32
|
+
line: '#e4e4e7',
|
|
33
|
+
companyName: 'HedHog',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Nenhuma fonte web: o container da API só tem `ttf-freefont`, e um `<link>`
|
|
38
|
+
* para o Google Fonts com `waitUntil: 'networkidle'` vira espera até o timeout
|
|
39
|
+
* quando o pod não tem saída para a internet.
|
|
40
|
+
*/
|
|
41
|
+
export const PDF_DOCUMENT_FONT = 'Arial, Helvetica, sans-serif';
|
|
42
|
+
|
|
43
|
+
/** Paleta print-safe: sem amarelo puro nem cinza claro, que somem no papel. */
|
|
44
|
+
const CATEGORICAL = [
|
|
45
|
+
'#2563eb',
|
|
46
|
+
'#16a34a',
|
|
47
|
+
'#9333ea',
|
|
48
|
+
'#dc2626',
|
|
49
|
+
'#0891b2',
|
|
50
|
+
'#b45309',
|
|
51
|
+
'#4f46e5',
|
|
52
|
+
'#475569',
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const text = (value: unknown): string | null => {
|
|
56
|
+
if (typeof value !== 'string') return null;
|
|
57
|
+
const trimmed = value.trim();
|
|
58
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export interface PdfDocumentThemeOverrides {
|
|
62
|
+
companyName?: string;
|
|
63
|
+
primary?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Lê as settings de marca. Qualquer falha cai nos valores de fábrica — um
|
|
68
|
+
* relatório sem cor certa ainda é um relatório; um relatório que não sai não é.
|
|
69
|
+
*/
|
|
70
|
+
export async function resolveDocumentTheme(
|
|
71
|
+
settingService: SettingService,
|
|
72
|
+
brandAsset: BrandAssetService,
|
|
73
|
+
overrides: PdfDocumentThemeOverrides = {},
|
|
74
|
+
): Promise<PdfDocumentTheme> {
|
|
75
|
+
let values: Record<string, any> = {};
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
values = await settingService.getSettingValues([
|
|
79
|
+
'theme-primary-light',
|
|
80
|
+
'theme-muted-light',
|
|
81
|
+
'theme-muted-foreground-light',
|
|
82
|
+
'theme-secondary-foreground-light',
|
|
83
|
+
'system-name',
|
|
84
|
+
'image-url',
|
|
85
|
+
'icon-url',
|
|
86
|
+
]);
|
|
87
|
+
} catch {
|
|
88
|
+
values = {};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const primary = overrides.primary ?? text(values['theme-primary-light']) ?? FALLBACK.primary;
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
primary,
|
|
95
|
+
mutedBg: text(values['theme-muted-light']) ?? FALLBACK.mutedBg,
|
|
96
|
+
mutedFg: text(values['theme-muted-foreground-light']) ?? FALLBACK.mutedFg,
|
|
97
|
+
ink: text(values['theme-secondary-foreground-light']) ?? FALLBACK.ink,
|
|
98
|
+
line: FALLBACK.line,
|
|
99
|
+
companyName: overrides.companyName ?? text(values['system-name']) ?? FALLBACK.companyName,
|
|
100
|
+
logoUrl: await resolveSystemLogo(brandAsset, values, primary),
|
|
101
|
+
fontFamily: PDF_DOCUMENT_FONT,
|
|
102
|
+
chartPalette: [primary, ...CATEGORICAL],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* O Chromium renderiza sem sessão e, dentro do cluster, pode não ter rota para
|
|
108
|
+
* o host público — por isso a imagem é baixada e embutida como data URI, com a
|
|
109
|
+
* URL absoluta como segundo recurso e um wordmark inline como último.
|
|
110
|
+
*/
|
|
111
|
+
async function resolveSystemLogo(
|
|
112
|
+
brandAsset: BrandAssetService,
|
|
113
|
+
values: Record<string, any>,
|
|
114
|
+
primary: string,
|
|
115
|
+
): Promise<string> {
|
|
116
|
+
const configured = text(values['image-url']) ?? text(values['icon-url']);
|
|
117
|
+
|
|
118
|
+
if (configured) {
|
|
119
|
+
try {
|
|
120
|
+
const absolute = await brandAsset.resolveAssetUrl(configured);
|
|
121
|
+
if (absolute) {
|
|
122
|
+
const dataUri = await brandAsset.fetchAsDataUri(absolute);
|
|
123
|
+
if (dataUri) return dataUri;
|
|
124
|
+
return absolute;
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// Cai no wordmark abaixo.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return inlineWordmark(primary);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function inlineWordmark(primary: string): string {
|
|
135
|
+
const svg = encodeURIComponent(
|
|
136
|
+
[
|
|
137
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72">',
|
|
138
|
+
'<rect width="240" height="72" rx="18" fill="#0f172a"/>',
|
|
139
|
+
`<circle cx="38" cy="36" r="18" fill="${primary}"/>`,
|
|
140
|
+
'<text x="72" y="43" fill="#ffffff" font-size="26" font-family="Arial, sans-serif" font-weight="700">HedHog</text>',
|
|
141
|
+
'</svg>',
|
|
142
|
+
].join(''),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return `data:image/svg+xml;charset=UTF-8,${svg}`;
|
|
146
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modelo de um documento PDF padrão: o chamador descreve **dados**, nunca HTML.
|
|
3
|
+
*
|
|
4
|
+
* É o que sustenta a regra de escape do renderer — se um serviço pudesse passar
|
|
5
|
+
* markup, o nome de um aluno com `<` viraria tag dentro de um Chromium headless.
|
|
6
|
+
* A única exceção é o SVG dos gráficos, que carrega o tipo {@link SafeSvg} e só
|
|
7
|
+
* pode ser produzido pelos desenhistas do próprio kit.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** SVG montado pelos helpers de gráfico do kit — a única string injetada sem escape. */
|
|
11
|
+
export type SafeSvg = string & { readonly __safeSvg: unique symbol };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Marca um SVG como seguro. Só os desenhistas de `./charts` devem chamar isto:
|
|
15
|
+
* quem passar conteúdo de usuário aqui reabre a porta que o modelo de blocos fecha.
|
|
16
|
+
*/
|
|
17
|
+
export const asSafeSvg = (svg: string): SafeSvg => svg as SafeSvg;
|
|
18
|
+
|
|
19
|
+
export interface PdfDocumentMeta {
|
|
20
|
+
label: string;
|
|
21
|
+
value: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PdfDocumentCard {
|
|
25
|
+
label: string;
|
|
26
|
+
value: string;
|
|
27
|
+
/** Linha pequena abaixo do valor, para a unidade ou a ressalva. */
|
|
28
|
+
hint?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PdfDocumentTableColumn {
|
|
32
|
+
header: string;
|
|
33
|
+
align?: 'left' | 'center' | 'right';
|
|
34
|
+
/**
|
|
35
|
+
* Largura CSS (ex.: '18%'). Sem ela o Chromium reparte pelo conteúdo e a
|
|
36
|
+
* tabela muda de forma a cada página.
|
|
37
|
+
*/
|
|
38
|
+
width?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PdfDocumentTableCell {
|
|
42
|
+
text: string;
|
|
43
|
+
/** Segunda linha menor: o e-mail sob o nome, o "Em risco" sob o status. */
|
|
44
|
+
sub?: string;
|
|
45
|
+
tone?: 'default' | 'muted' | 'bold' | 'accent' | 'warn';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type PdfDocumentRow = Array<string | number | null | undefined | PdfDocumentTableCell>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Paleta fechada das barras e marcadores — o chamador escolhe o papel, o tema
|
|
52
|
+
* decide a cor: 'ink' (feito), 'muted' e 'light' (dois cinzas), 'accent' (cor
|
|
53
|
+
* da marca) e 'danger' (atraso).
|
|
54
|
+
*/
|
|
55
|
+
export type PdfDocumentSegmentTone = 'ink' | 'muted' | 'light' | 'accent' | 'danger';
|
|
56
|
+
|
|
57
|
+
export interface PdfDocumentSegment {
|
|
58
|
+
value: number;
|
|
59
|
+
tone: PdfDocumentSegmentTone;
|
|
60
|
+
/** Só a barra avulsa (`segmentBar`) mostra rótulo, na legenda abaixo dela. */
|
|
61
|
+
label?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface PdfDocumentProgressEntry {
|
|
65
|
+
text: string;
|
|
66
|
+
/** Linha pequena com marcador da cor de `tone` (ex.: "Em revisão"). */
|
|
67
|
+
status: string;
|
|
68
|
+
tone: PdfDocumentSegmentTone;
|
|
69
|
+
/** Texto à direita da linha de status (ex.: "há 3d 4h"). */
|
|
70
|
+
aside?: string;
|
|
71
|
+
/** Terceira linha opcional (ex.: o prazo). */
|
|
72
|
+
sub?: string;
|
|
73
|
+
subTone?: 'muted' | 'danger';
|
|
74
|
+
/** Fundo tingido com a cor do `tone` — o item que pede atenção. */
|
|
75
|
+
highlight?: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface PdfDocumentProgressCard {
|
|
79
|
+
title: string;
|
|
80
|
+
subtitle?: string;
|
|
81
|
+
/** O número grande à direita (ex.: "33%"). */
|
|
82
|
+
value: string;
|
|
83
|
+
valueHint?: string;
|
|
84
|
+
segments: PdfDocumentSegment[];
|
|
85
|
+
listTitle?: string;
|
|
86
|
+
entries: PdfDocumentProgressEntry[];
|
|
87
|
+
/** Mostrado no lugar da lista quando `entries` vem vazio. */
|
|
88
|
+
emptyText?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type PdfDocumentBlock =
|
|
92
|
+
| { kind: 'section'; title: string; hint?: string }
|
|
93
|
+
| {
|
|
94
|
+
kind: 'cards';
|
|
95
|
+
items: PdfDocumentCard[];
|
|
96
|
+
/** Padrão: 4. */
|
|
97
|
+
columns?: 2 | 3 | 4 | 5;
|
|
98
|
+
/** 'metric' destaca o valor (indicador); 'filter' é o carimbo do recorte. */
|
|
99
|
+
variant?: 'metric' | 'filter';
|
|
100
|
+
}
|
|
101
|
+
| {
|
|
102
|
+
kind: 'table';
|
|
103
|
+
columns: PdfDocumentTableColumn[];
|
|
104
|
+
rows: PdfDocumentRow[];
|
|
105
|
+
/** Coluna "#" à esquerda, 1..N contínua entre páginas. */
|
|
106
|
+
numbered?: boolean;
|
|
107
|
+
numberHeader?: string;
|
|
108
|
+
zebra?: boolean;
|
|
109
|
+
/** Fonte menor, para tabela com muitas colunas. */
|
|
110
|
+
compact?: boolean;
|
|
111
|
+
emptyText?: string;
|
|
112
|
+
/** Aviso de corte quando a origem truncou as linhas. */
|
|
113
|
+
truncatedNote?: string;
|
|
114
|
+
}
|
|
115
|
+
| { kind: 'chart'; title?: string; svg: SafeSvg; caption?: string }
|
|
116
|
+
| { kind: 'chartGrid'; items: Array<{ title?: string; svg: SafeSvg; caption?: string }> }
|
|
117
|
+
/** Barra empilhada com legenda "rótulo N (x%)" — a composição de um total. */
|
|
118
|
+
| { kind: 'segmentBar'; title?: string; segments: PdfDocumentSegment[]; caption?: string }
|
|
119
|
+
/** Grade de cartões com %, barra e lista de itens — um por pessoa, por exemplo. */
|
|
120
|
+
| {
|
|
121
|
+
kind: 'progressCards';
|
|
122
|
+
items: PdfDocumentProgressCard[];
|
|
123
|
+
/** Legenda dos marcadores, acima da grade. */
|
|
124
|
+
legend?: Array<{ label: string; tone: PdfDocumentSegmentTone }>;
|
|
125
|
+
/** Padrão: 2. */
|
|
126
|
+
columns?: 1 | 2;
|
|
127
|
+
}
|
|
128
|
+
| { kind: 'note'; text: string; tone?: 'muted' | 'accent' }
|
|
129
|
+
| { kind: 'legend'; lines: string[] }
|
|
130
|
+
| { kind: 'pageBreak' };
|
|
131
|
+
|
|
132
|
+
export interface PdfDocumentInput {
|
|
133
|
+
/** O `h1` do letterhead. */
|
|
134
|
+
title: string;
|
|
135
|
+
/** Sobretítulo laranja, em caixa alta (ex.: "Relatório de Alunos"). */
|
|
136
|
+
eyebrow?: string;
|
|
137
|
+
/** Linha cinza abaixo do título — normalmente o nome da empresa. */
|
|
138
|
+
subtitle?: string;
|
|
139
|
+
/** Bloco alinhado à direita do letterhead. */
|
|
140
|
+
meta?: PdfDocumentMeta[];
|
|
141
|
+
blocks: PdfDocumentBlock[];
|
|
142
|
+
/** Data URI do logo; sem ele o kit usa o logo do sistema. */
|
|
143
|
+
logoDataUri?: string | null;
|
|
144
|
+
generatedAt?: Date;
|
|
145
|
+
locale?: string;
|
|
146
|
+
landscape?: boolean;
|
|
147
|
+
/** Padrão: 'Documento Confidencial'. */
|
|
148
|
+
footerLeft?: string;
|
|
149
|
+
}
|