@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.
Files changed (274) hide show
  1. package/dist/access-log/access-log.interceptor.d.ts +10 -0
  2. package/dist/access-log/access-log.interceptor.d.ts.map +1 -1
  3. package/dist/access-log/access-log.interceptor.js +15 -1
  4. package/dist/access-log/access-log.interceptor.js.map +1 -1
  5. package/dist/ai/ai.controller.d.ts +2 -0
  6. package/dist/ai/ai.controller.d.ts.map +1 -1
  7. package/dist/ai/ai.service.d.ts +86 -2
  8. package/dist/ai/ai.service.d.ts.map +1 -1
  9. package/dist/ai/ai.service.js +304 -23
  10. package/dist/ai/ai.service.js.map +1 -1
  11. package/dist/ai-instruction/ai-prompt-resolver.service.d.ts +12 -0
  12. package/dist/ai-instruction/ai-prompt-resolver.service.d.ts.map +1 -1
  13. package/dist/ai-instruction/ai-prompt-resolver.service.js +26 -0
  14. package/dist/ai-instruction/ai-prompt-resolver.service.js.map +1 -1
  15. package/dist/core.module.d.ts.map +1 -1
  16. package/dist/core.module.js +3 -0
  17. package/dist/core.module.js.map +1 -1
  18. package/dist/document-kit/document-kit.module.d.ts +3 -0
  19. package/dist/document-kit/document-kit.module.d.ts.map +1 -0
  20. package/dist/document-kit/document-kit.module.js +24 -0
  21. package/dist/document-kit/document-kit.module.js.map +1 -0
  22. package/dist/document-kit/document-kit.registry.d.ts +55 -0
  23. package/dist/document-kit/document-kit.registry.d.ts.map +1 -0
  24. package/dist/document-kit/document-kit.registry.js +91 -0
  25. package/dist/document-kit/document-kit.registry.js.map +1 -0
  26. package/dist/document-kit/document-kit.service.d.ts +49 -0
  27. package/dist/document-kit/document-kit.service.d.ts.map +1 -0
  28. package/dist/document-kit/document-kit.service.js +135 -0
  29. package/dist/document-kit/document-kit.service.js.map +1 -0
  30. package/dist/file/file-upload-ticket.service.d.ts +1 -1
  31. package/dist/file/file.controller.d.ts +3 -3
  32. package/dist/file/file.service.d.ts +136 -13
  33. package/dist/file/file.service.d.ts.map +1 -1
  34. package/dist/file/file.service.js +297 -27
  35. package/dist/file/file.service.js.map +1 -1
  36. package/dist/file/mcp-file-source.schema.d.ts.map +1 -1
  37. package/dist/file/mcp-file-source.schema.js +8 -5
  38. package/dist/file/mcp-file-source.schema.js.map +1 -1
  39. package/dist/file/provider/abstract.provider.d.ts +33 -0
  40. package/dist/file/provider/abstract.provider.d.ts.map +1 -1
  41. package/dist/file/provider/abstract.provider.js +9 -0
  42. package/dist/file/provider/abstract.provider.js.map +1 -1
  43. package/dist/file/provider/s3-multipart.util.d.ts +17 -0
  44. package/dist/file/provider/s3-multipart.util.d.ts.map +1 -0
  45. package/dist/file/provider/s3-multipart.util.js +88 -0
  46. package/dist/file/provider/s3-multipart.util.js.map +1 -0
  47. package/dist/file/provider/s3.provider.d.ts +9 -1
  48. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  49. package/dist/file/provider/s3.provider.js +16 -0
  50. package/dist/file/provider/s3.provider.js.map +1 -1
  51. package/dist/file/provider/spaces.provider.d.ts +10 -1
  52. package/dist/file/provider/spaces.provider.d.ts.map +1 -1
  53. package/dist/file/provider/spaces.provider.js +17 -0
  54. package/dist/file/provider/spaces.provider.js.map +1 -1
  55. package/dist/index.d.ts +5 -0
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +5 -0
  58. package/dist/index.js.map +1 -1
  59. package/dist/integration-profile/integration-profile.controller.d.ts +1 -0
  60. package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
  61. package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
  62. package/dist/integration-profile/integration-profile.secrets.js +26 -0
  63. package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
  64. package/dist/integration-profile/integration-profile.service.d.ts +1 -0
  65. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
  66. package/dist/integration-profile/integration-profile.service.js +3 -3
  67. package/dist/integration-profile/integration-profile.service.js.map +1 -1
  68. package/dist/integration-profile/testers/ai.testers.d.ts.map +1 -1
  69. package/dist/integration-profile/testers/ai.testers.js +46 -8
  70. package/dist/integration-profile/testers/ai.testers.js.map +1 -1
  71. package/dist/integration-profile/testers/http-probe.d.ts.map +1 -1
  72. package/dist/integration-profile/testers/http-probe.js +5 -3
  73. package/dist/integration-profile/testers/http-probe.js.map +1 -1
  74. package/dist/integration-profile/testers/integration-tester.types.d.ts +5 -0
  75. package/dist/integration-profile/testers/integration-tester.types.d.ts.map +1 -1
  76. package/dist/mail/mail.service.d.ts.map +1 -1
  77. package/dist/mail/mail.service.js +16 -1
  78. package/dist/mail/mail.service.js.map +1 -1
  79. package/dist/mcp/mcp-server.service.d.ts.map +1 -1
  80. package/dist/mcp/mcp-server.service.js +10 -1
  81. package/dist/mcp/mcp-server.service.js.map +1 -1
  82. package/dist/mcp/mcp.module.d.ts.map +1 -1
  83. package/dist/mcp/mcp.module.js +2 -0
  84. package/dist/mcp/mcp.module.js.map +1 -1
  85. package/dist/mcp/tools/core-access-log.mcp-tools.d.ts +17 -0
  86. package/dist/mcp/tools/core-access-log.mcp-tools.d.ts.map +1 -0
  87. package/dist/mcp/tools/core-access-log.mcp-tools.js +74 -0
  88. package/dist/mcp/tools/core-access-log.mcp-tools.js.map +1 -0
  89. package/dist/mcp/tools/core-files.mcp-tools.js +4 -4
  90. package/dist/mcp/tools/core-files.mcp-tools.js.map +1 -1
  91. package/dist/meeting/meeting-provider.registry.d.ts +72 -0
  92. package/dist/meeting/meeting-provider.registry.d.ts.map +1 -1
  93. package/dist/meeting/meeting-provider.registry.js.map +1 -1
  94. package/dist/oauth/oauth-token-broker.service.d.ts +50 -1
  95. package/dist/oauth/oauth-token-broker.service.d.ts.map +1 -1
  96. package/dist/oauth/oauth-token-broker.service.js +105 -3
  97. package/dist/oauth/oauth-token-broker.service.js.map +1 -1
  98. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +6 -0
  99. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
  100. package/dist/oauth/providers/microsoft-entra-id.provider.js +30 -4
  101. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
  102. package/dist/pdf/document/charts/chart-bar.d.ts +24 -0
  103. package/dist/pdf/document/charts/chart-bar.d.ts.map +1 -0
  104. package/dist/pdf/document/charts/chart-bar.js +117 -0
  105. package/dist/pdf/document/charts/chart-bar.js.map +1 -0
  106. package/dist/pdf/document/charts/chart-donut.d.ts +16 -0
  107. package/dist/pdf/document/charts/chart-donut.d.ts.map +1 -0
  108. package/dist/pdf/document/charts/chart-donut.js +89 -0
  109. package/dist/pdf/document/charts/chart-donut.js.map +1 -0
  110. package/dist/pdf/document/charts/chart-gauge.d.ts +15 -0
  111. package/dist/pdf/document/charts/chart-gauge.d.ts.map +1 -0
  112. package/dist/pdf/document/charts/chart-gauge.js +60 -0
  113. package/dist/pdf/document/charts/chart-gauge.js.map +1 -0
  114. package/dist/pdf/document/charts/chart-line.d.ts +29 -0
  115. package/dist/pdf/document/charts/chart-line.d.ts.map +1 -0
  116. package/dist/pdf/document/charts/chart-line.js +89 -0
  117. package/dist/pdf/document/charts/chart-line.js.map +1 -0
  118. package/dist/pdf/document/charts/chart-primitives.d.ts +64 -0
  119. package/dist/pdf/document/charts/chart-primitives.d.ts.map +1 -0
  120. package/dist/pdf/document/charts/chart-primitives.js +110 -0
  121. package/dist/pdf/document/charts/chart-primitives.js.map +1 -0
  122. package/dist/pdf/document/charts/chart-radar.d.ts +13 -0
  123. package/dist/pdf/document/charts/chart-radar.d.ts.map +1 -0
  124. package/dist/pdf/document/charts/chart-radar.js +83 -0
  125. package/dist/pdf/document/charts/chart-radar.js.map +1 -0
  126. package/dist/pdf/document/charts/chart-scale.d.ts +39 -0
  127. package/dist/pdf/document/charts/chart-scale.d.ts.map +1 -0
  128. package/dist/pdf/document/charts/chart-scale.js +118 -0
  129. package/dist/pdf/document/charts/chart-scale.js.map +1 -0
  130. package/dist/pdf/document/charts/index.d.ts +8 -0
  131. package/dist/pdf/document/charts/index.d.ts.map +1 -0
  132. package/dist/pdf/document/charts/index.js +24 -0
  133. package/dist/pdf/document/charts/index.js.map +1 -0
  134. package/dist/pdf/document/index.d.ts +8 -0
  135. package/dist/pdf/document/index.d.ts.map +1 -0
  136. package/dist/pdf/document/index.js +24 -0
  137. package/dist/pdf/document/index.js.map +1 -0
  138. package/dist/pdf/document/pdf-document.chrome.d.ts +20 -0
  139. package/dist/pdf/document/pdf-document.chrome.d.ts.map +1 -0
  140. package/dist/pdf/document/pdf-document.chrome.js +36 -0
  141. package/dist/pdf/document/pdf-document.chrome.js.map +1 -0
  142. package/dist/pdf/document/pdf-document.css.d.ts +20 -0
  143. package/dist/pdf/document/pdf-document.css.d.ts.map +1 -0
  144. package/dist/pdf/document/pdf-document.css.js +327 -0
  145. package/dist/pdf/document/pdf-document.css.js.map +1 -0
  146. package/dist/pdf/document/pdf-document.renderer.d.ts +10 -0
  147. package/dist/pdf/document/pdf-document.renderer.d.ts.map +1 -0
  148. package/dist/pdf/document/pdf-document.renderer.js +226 -0
  149. package/dist/pdf/document/pdf-document.renderer.js.map +1 -0
  150. package/dist/pdf/document/pdf-document.service.d.ts +27 -0
  151. package/dist/pdf/document/pdf-document.service.d.ts.map +1 -0
  152. package/dist/pdf/document/pdf-document.service.js +81 -0
  153. package/dist/pdf/document/pdf-document.service.js.map +1 -0
  154. package/dist/pdf/document/pdf-document.theme.d.ts +41 -0
  155. package/dist/pdf/document/pdf-document.theme.d.ts.map +1 -0
  156. package/dist/pdf/document/pdf-document.theme.js +105 -0
  157. package/dist/pdf/document/pdf-document.theme.js.map +1 -0
  158. package/dist/pdf/document/pdf-document.types.d.ts +164 -0
  159. package/dist/pdf/document/pdf-document.types.d.ts.map +1 -0
  160. package/dist/pdf/document/pdf-document.types.js +18 -0
  161. package/dist/pdf/document/pdf-document.types.js.map +1 -0
  162. package/dist/pdf/pdf.module.d.ts.map +1 -1
  163. package/dist/pdf/pdf.module.js +3 -2
  164. package/dist/pdf/pdf.module.js.map +1 -1
  165. package/dist/pdf/pdf.service.d.ts +75 -1
  166. package/dist/pdf/pdf.service.d.ts.map +1 -1
  167. package/dist/pdf/pdf.service.js +167 -63
  168. package/dist/pdf/pdf.service.js.map +1 -1
  169. package/dist/pdf/templates/commercial-proposal.template.d.ts +96 -0
  170. package/dist/pdf/templates/commercial-proposal.template.d.ts.map +1 -0
  171. package/dist/pdf/templates/commercial-proposal.template.js +276 -0
  172. package/dist/pdf/templates/commercial-proposal.template.js.map +1 -0
  173. package/dist/setting/favicon.controller.d.ts +26 -0
  174. package/dist/setting/favicon.controller.d.ts.map +1 -0
  175. package/dist/setting/favicon.controller.js +130 -0
  176. package/dist/setting/favicon.controller.js.map +1 -0
  177. package/dist/setting/setting.module.d.ts.map +1 -1
  178. package/dist/setting/setting.module.js +2 -1
  179. package/dist/setting/setting.module.js.map +1 -1
  180. package/dist/setting/setting.service.d.ts.map +1 -1
  181. package/dist/setting/setting.service.js +3 -0
  182. package/dist/setting/setting.service.js.map +1 -1
  183. package/dist/task/task.service.d.ts +6 -0
  184. package/dist/task/task.service.d.ts.map +1 -1
  185. package/dist/task/task.service.js +44 -13
  186. package/dist/task/task.service.js.map +1 -1
  187. package/dist/whatsapp-sender/whatsapp-sender.service.d.ts +35 -2
  188. package/dist/whatsapp-sender/whatsapp-sender.service.d.ts.map +1 -1
  189. package/dist/whatsapp-sender/whatsapp-sender.service.js +107 -22
  190. package/dist/whatsapp-sender/whatsapp-sender.service.js.map +1 -1
  191. package/hedhog/data/integration_type.yaml +6 -0
  192. package/hedhog/data/route.yaml +8 -0
  193. package/hedhog/data/setting_group.yaml +52 -0
  194. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.test.tsx.ejs +85 -2
  195. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +576 -491
  196. package/hedhog/frontend/app/users/simulate-access-dialog.test.tsx.ejs +29 -50
  197. package/hedhog/frontend/app/users/simulate-access-dialog.tsx.ejs +146 -50
  198. package/hedhog/frontend/messages/en.json +81 -2
  199. package/hedhog/frontend/messages/pt.json +81 -2
  200. package/hedhog/table/file.yaml +3 -1
  201. package/package.json +5 -5
  202. package/src/access-log/access-log.interceptor.spec.ts +50 -0
  203. package/src/access-log/access-log.interceptor.ts +18 -0
  204. package/src/ai/ai-attachment.fragments.spec.ts +56 -3
  205. package/src/ai/ai.service.responses.spec.ts +429 -0
  206. package/src/ai/ai.service.ts +409 -27
  207. package/src/ai-instruction/ai-prompt-resolver.service.spec.ts +44 -0
  208. package/src/ai-instruction/ai-prompt-resolver.service.ts +29 -0
  209. package/src/core.module.ts +3 -0
  210. package/src/document-kit/document-kit.module.ts +12 -0
  211. package/src/document-kit/document-kit.registry.ts +125 -0
  212. package/src/document-kit/document-kit.service.spec.ts +156 -0
  213. package/src/document-kit/document-kit.service.ts +173 -0
  214. package/src/file/file-direct-upload.spec.ts +344 -0
  215. package/src/file/file.service.spec.ts +168 -1
  216. package/src/file/file.service.ts +435 -36
  217. package/src/file/mcp-file-source.schema.ts +8 -5
  218. package/src/file/provider/abstract.provider.ts +57 -0
  219. package/src/file/provider/s3-multipart.spec.ts +123 -0
  220. package/src/file/provider/s3-multipart.util.ts +126 -0
  221. package/src/file/provider/s3.provider.ts +65 -1
  222. package/src/file/provider/spaces.provider.ts +67 -1
  223. package/src/index.ts +5 -0
  224. package/src/integration-profile/integration-profile.secrets.ts +26 -0
  225. package/src/integration-profile/integration-profile.service.ts +1 -0
  226. package/src/integration-profile/testers/ai.testers.ts +70 -5
  227. package/src/integration-profile/testers/http-probe.ts +5 -3
  228. package/src/integration-profile/testers/integration-tester.types.ts +5 -0
  229. package/src/integration-profile/testers/oauth.testers.spec.ts +13 -0
  230. package/src/integration-profile/testers/remaining.testers.spec.ts +65 -1
  231. package/src/integration-profile/testers/tester-coverage.spec.ts +12 -0
  232. package/src/mail/mail.service.spec.ts +25 -0
  233. package/src/mail/mail.service.ts +18 -1
  234. package/src/mcp/mcp-server.service.spec.ts +16 -0
  235. package/src/mcp/mcp-server.service.ts +10 -1
  236. package/src/mcp/mcp.module.ts +2 -0
  237. package/src/mcp/tools/core-access-log.mcp-tools.ts +66 -0
  238. package/src/mcp/tools/core-files.mcp-tools.ts +4 -4
  239. package/src/meeting/meeting-provider.registry.ts +80 -0
  240. package/src/oauth/oauth-token-broker.service.spec.ts +108 -0
  241. package/src/oauth/oauth-token-broker.service.ts +129 -2
  242. package/src/oauth/providers/microsoft-entra-id.provider.spec.ts +133 -0
  243. package/src/oauth/providers/microsoft-entra-id.provider.ts +29 -2
  244. package/src/pdf/document/charts/chart-bar.ts +217 -0
  245. package/src/pdf/document/charts/chart-donut.ts +163 -0
  246. package/src/pdf/document/charts/chart-gauge.ts +94 -0
  247. package/src/pdf/document/charts/chart-line.ts +159 -0
  248. package/src/pdf/document/charts/chart-primitives.ts +188 -0
  249. package/src/pdf/document/charts/chart-radar.ts +136 -0
  250. package/src/pdf/document/charts/chart-scale.spec.ts +93 -0
  251. package/src/pdf/document/charts/chart-scale.ts +141 -0
  252. package/src/pdf/document/charts/charts.spec.ts +157 -0
  253. package/src/pdf/document/charts/index.ts +7 -0
  254. package/src/pdf/document/index.ts +7 -0
  255. package/src/pdf/document/pdf-document.chrome.ts +38 -0
  256. package/src/pdf/document/pdf-document.css.ts +326 -0
  257. package/src/pdf/document/pdf-document.renderer.spec.ts +172 -0
  258. package/src/pdf/document/pdf-document.renderer.ts +317 -0
  259. package/src/pdf/document/pdf-document.service.ts +77 -0
  260. package/src/pdf/document/pdf-document.theme.ts +146 -0
  261. package/src/pdf/document/pdf-document.types.ts +149 -0
  262. package/src/pdf/pdf.module.ts +3 -2
  263. package/src/pdf/pdf.service.spec.ts +131 -0
  264. package/src/pdf/pdf.service.ts +256 -82
  265. package/src/pdf/templates/commercial-proposal.template.spec.ts +177 -0
  266. package/src/pdf/templates/commercial-proposal.template.ts +400 -0
  267. package/src/setting/favicon.controller.spec.ts +92 -0
  268. package/src/setting/favicon.controller.ts +135 -0
  269. package/src/setting/setting.module.ts +2 -1
  270. package/src/setting/setting.service.ts +3 -0
  271. package/src/task/task.service.spec.ts +77 -1
  272. package/src/task/task.service.ts +58 -14
  273. package/src/whatsapp-sender/whatsapp-sender.service.spec.ts +263 -0
  274. package/src/whatsapp-sender/whatsapp-sender.service.ts +154 -16
@@ -0,0 +1,136 @@
1
+ import { SafeSvg } from '../pdf-document.types';
2
+ import {
3
+ CHART_DEFAULT_HEIGHT,
4
+ CHART_FONT,
5
+ CHART_FULL_WIDTH,
6
+ ChartBaseOptions,
7
+ ChartSeries,
8
+ chartColor,
9
+ emptyChart,
10
+ legendRows,
11
+ svgRoot,
12
+ text,
13
+ } from './chart-primitives';
14
+ import { formatNumber, niceScale, truncateToWidth, withAlpha } from './chart-scale';
15
+
16
+ /**
17
+ * Radar de eixos comparáveis — a forma da tela para "nota média por tópico",
18
+ * onde todos os eixos compartilham a mesma escala (0 a 5).
19
+ */
20
+ export function radarChartSvg(
21
+ options: ChartBaseOptions & {
22
+ axes: string[];
23
+ series: ChartSeries[];
24
+ max?: number;
25
+ rings?: number;
26
+ },
27
+ ): SafeSvg {
28
+ const {
29
+ theme,
30
+ axes,
31
+ series,
32
+ width = CHART_FULL_WIDTH,
33
+ height = CHART_DEFAULT_HEIGHT,
34
+ rings = 4,
35
+ valueFormat = (value: number) => formatNumber(value),
36
+ } = options;
37
+
38
+ const values = series
39
+ .flatMap((s) => s.values)
40
+ .filter((v): v is number => typeof v === 'number' && Number.isFinite(v));
41
+
42
+ // Abaixo de 3 eixos o radar vira uma linha ou um triângulo degenerado, que
43
+ // engana mais do que informa.
44
+ if (axes.length < 3 || values.length === 0) return emptyChart(options);
45
+
46
+ const max = options.max ?? niceScale(0, Math.max(...values), rings).max;
47
+ const showLegend = series.length > 1;
48
+ const legendHeight = showLegend ? 16 : 0;
49
+
50
+ const cx = width / 2;
51
+ const cy = (height - legendHeight) / 2;
52
+ const radius = Math.min(cx, cy) - 26;
53
+
54
+ const angleOf = (index: number) => (index / axes.length) * Math.PI * 2 - Math.PI / 2;
55
+ const point = (index: number, value: number) => {
56
+ const ratio = max === 0 ? 0 : Math.min(Math.max(value / max, 0), 1);
57
+ const angle = angleOf(index);
58
+ return { x: cx + radius * ratio * Math.cos(angle), y: cy + radius * ratio * Math.sin(angle) };
59
+ };
60
+
61
+ const parts: string[] = [];
62
+
63
+ for (let ring = 1; ring <= rings; ring += 1) {
64
+ const r = (radius * ring) / rings;
65
+ const polygon = axes
66
+ .map((_, index) => {
67
+ const angle = angleOf(index);
68
+ return `${(cx + r * Math.cos(angle)).toFixed(1)},${(cy + r * Math.sin(angle)).toFixed(1)}`;
69
+ })
70
+ .join(' ');
71
+
72
+ parts.push(`<polygon points="${polygon}" fill="none" stroke="${theme.line}" stroke-width="1" />`);
73
+ }
74
+
75
+ axes.forEach((axis, index) => {
76
+ const angle = angleOf(index);
77
+ const edgeX = cx + radius * Math.cos(angle);
78
+ const edgeY = cy + radius * Math.sin(angle);
79
+
80
+ parts.push(
81
+ `<line x1="${cx}" y1="${cy}" x2="${edgeX.toFixed(1)}" y2="${edgeY.toFixed(1)}" stroke="${theme.line}" />`,
82
+ );
83
+
84
+ const labelX = cx + (radius + 12) * Math.cos(angle);
85
+ const labelY = cy + (radius + 12) * Math.sin(angle) + 3;
86
+ const anchor = Math.abs(Math.cos(angle)) < 0.3 ? 'middle' : Math.cos(angle) > 0 ? 'start' : 'end';
87
+
88
+ parts.push(
89
+ text(truncateToWidth(axis, CHART_FONT.axis, 90), labelX, labelY, {
90
+ size: CHART_FONT.axis,
91
+ fill: theme.mutedFg,
92
+ anchor,
93
+ }),
94
+ );
95
+ });
96
+
97
+ series.forEach((serie, serieIndex) => {
98
+ const color = chartColor(theme, serieIndex, serie.color);
99
+ const points = axes.map((_, index) => {
100
+ const raw = serie.values[index];
101
+ return point(index, typeof raw === 'number' && Number.isFinite(raw) ? raw : 0);
102
+ });
103
+
104
+ parts.push(
105
+ `<polygon points="${points.map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ')}" ` +
106
+ `fill="${withAlpha(color, 0.18)}" stroke="${color}" stroke-width="1.8" stroke-linejoin="round" />`,
107
+ );
108
+
109
+ points.forEach((p) => {
110
+ parts.push(`<circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="2" fill="${color}" />`);
111
+ });
112
+ });
113
+
114
+ parts.push(
115
+ text(`Escala 0 a ${valueFormat(max)}`, width, height - legendHeight - 2, {
116
+ size: CHART_FONT.axis,
117
+ fill: theme.mutedFg,
118
+ anchor: 'end',
119
+ }),
120
+ );
121
+
122
+ if (showLegend) {
123
+ const legend = legendRows(
124
+ series.map((serie, index) => ({
125
+ label: serie.name,
126
+ color: chartColor(theme, index, serie.color),
127
+ })),
128
+ width,
129
+ height - 4,
130
+ theme,
131
+ );
132
+ parts.push(legend.markup);
133
+ }
134
+
135
+ return svgRoot(width, height, parts.join(''), options.ariaLabel);
136
+ }
@@ -0,0 +1,93 @@
1
+ import { describe, expect, it } from '@jest/globals';
2
+
3
+ import {
4
+ axisLabelPlan,
5
+ bandScale,
6
+ estimateTextWidth,
7
+ linearScale,
8
+ niceScale,
9
+ truncateToWidth,
10
+ withAlpha,
11
+ } from './chart-scale';
12
+
13
+ describe('niceScale', () => {
14
+ it('arredonda o topo para um valor redondo', () => {
15
+ const scale = niceScale(0, 97, 4);
16
+
17
+ expect(scale.max).toBe(100);
18
+ expect(scale.step).toBe(25);
19
+ expect(scale.ticks).toEqual([0, 25, 50, 75, 100]);
20
+ });
21
+
22
+ // Uma série constante (ou toda zero) não tem amplitude: sem este caso a
23
+ // divisão pelo intervalo daria NaN e o gráfico sairia vazio.
24
+ it('sobrevive a série sem amplitude', () => {
25
+ expect(niceScale(0, 0).ticks.length).toBeGreaterThan(1);
26
+ expect(niceScale(7, 7).max).toBe(7);
27
+ expect(niceScale(7, 7).ticks.every((t) => Number.isFinite(t))).toBe(true);
28
+ });
29
+
30
+ it('inclui o último tick apesar do erro de ponto flutuante', () => {
31
+ const scale = niceScale(0, 0.3, 3);
32
+
33
+ expect(scale.ticks[scale.ticks.length - 1]).toBeGreaterThanOrEqual(0.3);
34
+ });
35
+ });
36
+
37
+ describe('linearScale', () => {
38
+ it('mapeia o domínio no intervalo de pixels, invertendo o eixo Y', () => {
39
+ const y = linearScale([0, 100], [200, 0]);
40
+
41
+ expect(y(0)).toBe(200);
42
+ expect(y(100)).toBe(0);
43
+ expect(y(50)).toBe(100);
44
+ });
45
+
46
+ it('não divide por zero quando o domínio é degenerado', () => {
47
+ expect(Number.isFinite(linearScale([5, 5], [0, 10])(5))).toBe(true);
48
+ });
49
+ });
50
+
51
+ describe('bandScale', () => {
52
+ it('distribui as faixas com folga entre elas', () => {
53
+ const band = bandScale(4, [0, 400], 0.2);
54
+
55
+ expect(band.bandwidth).toBe(80);
56
+ expect(band.center(0)).toBe(50);
57
+ expect(band.center(3)).toBe(350);
58
+ });
59
+ });
60
+
61
+ describe('medidas de texto', () => {
62
+ it('corta com reticências quando não cabe', () => {
63
+ const long = 'Curso de Introdução à Inteligência Artificial';
64
+
65
+ expect(truncateToWidth(long, 8, 40)).toMatch(/…$/);
66
+ expect(estimateTextWidth(truncateToWidth(long, 8, 40), 8)).toBeLessThanOrEqual(44);
67
+ expect(truncateToWidth('Curto', 8, 200)).toBe('Curto');
68
+ });
69
+
70
+ it('deita o rótulo quando cabe e gira quando não cabe', () => {
71
+ expect(axisLabelPlan(['jan', 'fev'], 40, 8).rotate).toBe(0);
72
+ expect(axisLabelPlan(['Janeiro de 2026', 'Fevereiro de 2026'], 20, 8).rotate).toBe(-35);
73
+ });
74
+
75
+ it('pula rótulos quando nem girado cabe', () => {
76
+ const plan = axisLabelPlan(
77
+ Array.from({ length: 40 }, (_, i) => `Semana ${i + 1} de 2026`),
78
+ 6,
79
+ 8,
80
+ );
81
+
82
+ expect(plan.skipEvery).toBeGreaterThan(1);
83
+ });
84
+ });
85
+
86
+ describe('withAlpha', () => {
87
+ it.each([
88
+ ['#ff760c', 'rgba(255,118,12,0.18)'],
89
+ ['#FFF', 'rgba(255,255,255,0.18)'],
90
+ ])('converte %s', (color, expected) => {
91
+ expect(withAlpha(color, 0.18)).toBe(expected);
92
+ });
93
+ });
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Escalas, medidas e formatação dos gráficos do documento PDF.
3
+ *
4
+ * Tudo aqui é função pura e sem dependência: o gráfico é desenhado como SVG no
5
+ * servidor, onde não há canvas para medir texto nem biblioteca de plotagem.
6
+ */
7
+
8
+ /** Domínio arredondado para valores "redondos" (1, 2, 5 × 10^k) e os ticks dele. */
9
+ export function niceScale(
10
+ min: number,
11
+ max: number,
12
+ tickCount = 4,
13
+ ): { min: number; max: number; step: number; ticks: number[] } {
14
+ const safeMin = Number.isFinite(min) ? min : 0;
15
+ const safeMax = Number.isFinite(max) ? max : 0;
16
+
17
+ // Série toda igual (ou toda zero) não tem amplitude para dividir: um passo de
18
+ // 1 mantém o eixo legível em vez de gerar divisão por zero.
19
+ if (safeMax === safeMin) {
20
+ const value = safeMax === 0 ? 1 : Math.abs(safeMax);
21
+ const step = value / tickCount || 1;
22
+ return {
23
+ min: Math.min(0, safeMin),
24
+ max: safeMax === 0 ? 1 : safeMax,
25
+ step,
26
+ ticks: Array.from({ length: tickCount + 1 }, (_, i) => Math.min(0, safeMin) + step * i),
27
+ };
28
+ }
29
+
30
+ const rawStep = (safeMax - safeMin) / Math.max(tickCount, 1);
31
+ const magnitude = 10 ** Math.floor(Math.log10(rawStep));
32
+ const normalized = rawStep / magnitude;
33
+ // O 2,5 está na escada de propósito: sem ele, um eixo de 0 a 97 pedindo 4
34
+ // divisões cai no passo 50 e imprime só 0/50/100 — informação de menos para
35
+ // um relatório que se lê no papel, sem passar o mouse em nada.
36
+ const niceNormalized =
37
+ normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 2.5 ? 2.5 : normalized <= 5 ? 5 : 10;
38
+ const step = niceNormalized * magnitude;
39
+
40
+ const niceMin = Math.floor(safeMin / step) * step;
41
+ const niceMax = Math.ceil(safeMax / step) * step;
42
+
43
+ const ticks: number[] = [];
44
+ // Tolerância pelo passo: somar 0.1+0.2 em ponto flutuante erraria o último tick.
45
+ for (let value = niceMin; value <= niceMax + step / 1000; value += step) {
46
+ ticks.push(Number(value.toFixed(10)));
47
+ }
48
+
49
+ return { min: niceMin, max: niceMax, step, ticks };
50
+ }
51
+
52
+ /** Converte valor do domínio em pixel. */
53
+ export function linearScale(
54
+ domain: [number, number],
55
+ range: [number, number],
56
+ ): (value: number) => number {
57
+ const [d0, d1] = domain;
58
+ const [r0, r1] = range;
59
+ const span = d1 - d0 || 1;
60
+
61
+ return (value: number) => r0 + ((value - d0) / span) * (r1 - r0);
62
+ }
63
+
64
+ /** Divide um intervalo em faixas de mesma largura, uma por categoria. */
65
+ export function bandScale(
66
+ count: number,
67
+ range: [number, number],
68
+ paddingRatio = 0.2,
69
+ ): { band: (index: number) => number; center: (index: number) => number; bandwidth: number } {
70
+ const [r0, r1] = range;
71
+ const total = r1 - r0;
72
+ const slots = Math.max(count, 1);
73
+ const slot = total / slots;
74
+ const bandwidth = slot * (1 - paddingRatio);
75
+ const offset = (slot - bandwidth) / 2;
76
+
77
+ return {
78
+ band: (index: number) => r0 + slot * index + offset,
79
+ center: (index: number) => r0 + slot * index + slot / 2,
80
+ bandwidth,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Largura aproximada de um texto. Sem canvas no servidor, 0,55 × tamanho da
86
+ * fonte por caractere erra pouco para Arial/FreeSans e é o bastante para
87
+ * decidir corte e rotação de rótulo.
88
+ */
89
+ export function estimateTextWidth(text: string, fontSize: number): number {
90
+ return text.length * fontSize * 0.55;
91
+ }
92
+
93
+ /** Corta o texto no limite de pixels, com reticências. */
94
+ export function truncateToWidth(text: string, fontSize: number, maxWidth: number): string {
95
+ if (estimateTextWidth(text, fontSize) <= maxWidth) return text;
96
+
97
+ const maxChars = Math.max(Math.floor(maxWidth / (fontSize * 0.55)) - 1, 1);
98
+ return `${text.slice(0, maxChars).trimEnd()}…`;
99
+ }
100
+
101
+ /** Decide se os rótulos do eixo X cabem deitados e quantos pular. */
102
+ export function axisLabelPlan(
103
+ labels: string[],
104
+ bandwidth: number,
105
+ fontSize: number,
106
+ ): { rotate: 0 | -35; skipEvery: number } {
107
+ const widest = labels.reduce((max, label) => Math.max(max, estimateTextWidth(label, fontSize)), 0);
108
+
109
+ if (widest <= bandwidth) return { rotate: 0, skipEvery: 1 };
110
+ // Girar resolve até certo ponto; passado ele, é melhor mostrar menos rótulos
111
+ // do que empilhar texto ilegível.
112
+ if (widest <= bandwidth * 3) return { rotate: -35, skipEvery: 1 };
113
+
114
+ return { rotate: -35, skipEvery: Math.ceil(widest / (bandwidth * 3)) };
115
+ }
116
+
117
+ /** `#ff760c` + alpha → `rgba(255,118,12,0.18)`, para o preenchimento de área. */
118
+ export function withAlpha(hexColor: string, alpha: number): string {
119
+ const hex = hexColor.replace('#', '');
120
+ const full =
121
+ hex.length === 3
122
+ ? hex
123
+ .split('')
124
+ .map((char) => char + char)
125
+ .join('')
126
+ : hex;
127
+
128
+ const value = Number.parseInt(full.slice(0, 6), 16);
129
+ if (!Number.isFinite(value)) return hexColor;
130
+
131
+ const r = (value >> 16) & 255;
132
+ const g = (value >> 8) & 255;
133
+ const b = value & 255;
134
+
135
+ return `rgba(${r},${g},${b},${alpha})`;
136
+ }
137
+
138
+ /** Número no formato pt-BR, sem casas decimais desnecessárias. */
139
+ export function formatNumber(value: number, maximumFractionDigits = 1): string {
140
+ return new Intl.NumberFormat('pt-BR', { maximumFractionDigits }).format(value);
141
+ }
@@ -0,0 +1,157 @@
1
+ import { describe, expect, it } from '@jest/globals';
2
+
3
+ import { PdfDocumentTheme } from '../pdf-document.theme';
4
+ import {
5
+ areaChartSvg,
6
+ barChartSvg,
7
+ donutChartSvg,
8
+ gaugeChartSvg,
9
+ horizontalBarChartSvg,
10
+ lineChartSvg,
11
+ radarChartSvg,
12
+ } from './index';
13
+
14
+ const theme: PdfDocumentTheme = {
15
+ primary: '#ff760c',
16
+ mutedBg: '#f4f4f5',
17
+ mutedFg: '#71717a',
18
+ ink: '#18181b',
19
+ line: '#e4e4e7',
20
+ companyName: 'HedHog',
21
+ logoUrl: '',
22
+ fontFamily: 'Arial, Helvetica, sans-serif',
23
+ chartPalette: ['#ff760c', '#2563eb', '#16a34a'],
24
+ };
25
+
26
+ const meses = ['Jan', 'Fev', 'Mar', 'Abr'];
27
+
28
+ describe('desenhistas de gráfico', () => {
29
+ it('produzem SVG válido com viewBox', () => {
30
+ const svgs = [
31
+ barChartSvg({ theme, categories: meses, series: [{ name: 'Matrículas', values: [3, 8, 5, 9] }] }),
32
+ horizontalBarChartSvg({
33
+ theme,
34
+ categories: ['Curso A', 'Curso B'],
35
+ series: [{ name: 'Conclusão', values: [80, 45] }],
36
+ }),
37
+ lineChartSvg({ theme, categories: meses, series: [{ name: 'Frequência', values: [70, 75, 80, 90] }] }),
38
+ donutChartSvg({
39
+ theme,
40
+ slices: [
41
+ { label: 'Ativos', value: 30 },
42
+ { label: 'Em risco', value: 10 },
43
+ ],
44
+ }),
45
+ radarChartSvg({
46
+ theme,
47
+ axes: ['Conteúdo', 'Didática', 'Ritmo'],
48
+ series: [{ name: 'Média', values: [4.5, 3.8, 4.1] }],
49
+ max: 5,
50
+ }),
51
+ gaugeChartSvg({ theme, items: [{ label: 'Curso A', pct: 62 }] }),
52
+ ];
53
+
54
+ for (const svg of svgs) {
55
+ expect(svg).toMatch(/^<svg viewBox="0 0 \d+ \d+"/);
56
+ expect(svg.endsWith('</svg>')).toBe(true);
57
+ // Tag desbalanceada só apareceria no papel, e tarde demais.
58
+ expect(svg.split('<').length).toBe(svg.split('>').length);
59
+ }
60
+ });
61
+
62
+ // Rótulo de curso é dado de usuário indo para dentro de um Chromium headless.
63
+ it('escapam o texto que vem dos dados', () => {
64
+ const svg = horizontalBarChartSvg({
65
+ theme,
66
+ categories: ['<script>alert(1)</script>'],
67
+ series: [{ name: 'x', values: [10] }],
68
+ });
69
+
70
+ expect(svg).not.toContain('<script>');
71
+ expect(svg).toContain('&lt;script&gt;');
72
+ });
73
+
74
+ it('caem no aviso de vazio em vez de desenhar uma moldura sem nada', () => {
75
+ const semDados = barChartSvg({ theme, categories: [], series: [] });
76
+ const soNulos = lineChartSvg({ theme, categories: meses, series: [{ name: 'x', values: [null, null, null, null] }] });
77
+ const roscaZerada = donutChartSvg({ theme, slices: [{ label: 'Nada', value: 0 }] });
78
+
79
+ for (const svg of [semDados, soNulos, roscaZerada]) {
80
+ expect(svg).toContain('Sem dados no período');
81
+ }
82
+ });
83
+
84
+ it('respeitam o texto de vazio escolhido pelo chamador', () => {
85
+ expect(
86
+ barChartSvg({ theme, categories: [], series: [], emptyText: 'Nenhuma turma no recorte' }),
87
+ ).toContain('Nenhuma turma no recorte');
88
+ });
89
+
90
+ // Buraco na série é ausência de dado, não zero: emendar a linha inventaria
91
+ // uma queda que não aconteceu.
92
+ it('interrompem a linha no buraco da série', () => {
93
+ const comBuraco = lineChartSvg({
94
+ theme,
95
+ categories: meses,
96
+ series: [{ name: 'Nota', values: [4, null, 3, 5] }],
97
+ });
98
+
99
+ expect((comBuraco.match(/<path d="M/g) ?? []).length).toBeGreaterThan(1);
100
+ });
101
+
102
+ it('desenham a linha de meta quando pedida', () => {
103
+ const svg = areaChartSvg({
104
+ theme,
105
+ categories: meses,
106
+ series: [{ name: 'Frequência', values: [70, 75, 80, 90] }],
107
+ referenceLine: { value: 80, label: 'Meta 80%' },
108
+ });
109
+
110
+ expect(svg).toContain('stroke-dasharray="4 3"');
111
+ expect(svg).toContain('Meta 80%');
112
+ });
113
+
114
+ it('a área preenche sob a linha e a linha simples não', () => {
115
+ const base = { theme, categories: meses, series: [{ name: 'x', values: [1, 2, 3, 4] }] };
116
+
117
+ expect(areaChartSvg(base)).toContain('rgba(255,118,12,0.18)');
118
+ expect(lineChartSvg(base)).not.toContain('rgba(255,118,12,0.18)');
119
+ });
120
+
121
+ it('a rosca mostra o percentual de cada fatia', () => {
122
+ const svg = donutChartSvg({
123
+ theme,
124
+ slices: [
125
+ { label: 'Ativos', value: 75 },
126
+ { label: 'Inativos', value: 25 },
127
+ ],
128
+ });
129
+
130
+ expect(svg).toContain('75%');
131
+ expect(svg).toContain('25%');
132
+ });
133
+
134
+ // A legenda era dimensionada por subtração e o epsilon de ponto flutuante
135
+ // cortava o rótulo que cabia exatamente: "Em risco" saía "Em ris…".
136
+ it('a legenda não corta o rótulo que cabe', () => {
137
+ const svg = donutChartSvg({
138
+ theme,
139
+ width: 330,
140
+ height: 200,
141
+ slices: [
142
+ { label: 'Ativos', value: 318 },
143
+ { label: 'Em risco', value: 87 },
144
+ { label: 'Inativos', value: 16 },
145
+ ],
146
+ });
147
+
148
+ expect(svg).toContain('Em risco');
149
+ expect(svg).not.toContain('…');
150
+ });
151
+
152
+ it('o radar exige três eixos para não virar um triângulo degenerado', () => {
153
+ expect(
154
+ radarChartSvg({ theme, axes: ['A', 'B'], series: [{ name: 'x', values: [1, 2] }] }),
155
+ ).toContain('Sem dados no período');
156
+ });
157
+ });
@@ -0,0 +1,7 @@
1
+ export * from './chart-bar';
2
+ export * from './chart-donut';
3
+ export * from './chart-gauge';
4
+ export * from './chart-line';
5
+ export * from './chart-primitives';
6
+ export * from './chart-radar';
7
+ export * from './chart-scale';
@@ -0,0 +1,7 @@
1
+ export * from './charts';
2
+ export * from './pdf-document.chrome';
3
+ export * from './pdf-document.css';
4
+ export * from './pdf-document.renderer';
5
+ export * from './pdf-document.service';
6
+ export * from './pdf-document.theme';
7
+ export * from './pdf-document.types';
@@ -0,0 +1,38 @@
1
+ import { escapeHtml } from './pdf-document.renderer';
2
+ import { PdfDocumentTheme } from './pdf-document.theme';
3
+
4
+ /**
5
+ * Cabeçalho e rodapé nativos do Chromium — a barra laranja que repete em toda
6
+ * página e o "3 / 15" que só o renderizador sabe calcular.
7
+ *
8
+ * Eles são mini-documentos, não parte da página: começam em `font-size: 0`,
9
+ * precisam do próprio `print-color-adjust` para manter fundo, e só
10
+ * `<span class="pageNumber">` e `<span class="totalPages">` são substituídos.
11
+ * As margens abaixo reservam o espaço deles — encolher `top`/`bottom` faz o
12
+ * conteúdo imprimir por cima.
13
+ */
14
+ export const PDF_DOCUMENT_MARGIN = {
15
+ top: '72px',
16
+ right: '56px',
17
+ bottom: '56px',
18
+ left: '56px',
19
+ };
20
+
21
+ export function buildAccentHeaderTemplate(theme: PdfDocumentTheme): string {
22
+ return (
23
+ '<style>*{margin:0;padding:0;box-sizing:border-box;}html,body{margin:0;padding:0;}</style>' +
24
+ '<div style="position:absolute;top:0;left:0;right:0;width:100%;height:8px;' +
25
+ `background:${theme.primary};-webkit-print-color-adjust:exact;print-color-adjust:exact;"></div>`
26
+ );
27
+ }
28
+
29
+ export function buildFooterTemplate(theme: PdfDocumentTheme, left: string): string {
30
+ return (
31
+ '<div style="width:100%;padding:0 56px;display:flex;justify-content:space-between;' +
32
+ `align-items:center;font-family:${theme.fontFamily};font-size:8px;color:${theme.mutedFg};` +
33
+ '-webkit-print-color-adjust:exact;print-color-adjust:exact;">' +
34
+ `<span>${escapeHtml(left)}</span>` +
35
+ '<span><span class="pageNumber"></span> / <span class="totalPages"></span></span>' +
36
+ '</div>'
37
+ );
38
+ }