@rashidee/co2 1.2.5

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 (190) hide show
  1. package/dist/index.js +25449 -0
  2. package/drizzle/0000_remarkable_charles_xavier.sql +210 -0
  3. package/drizzle/0001_wise_wolf_cub.sql +13 -0
  4. package/drizzle/0002_secret_spyke.sql +16 -0
  5. package/drizzle/0003_workspace_config_singleton.sql +16 -0
  6. package/drizzle/0004_terminal_claim_v120.sql +11 -0
  7. package/drizzle/0005_brainstorm_v124.sql +10 -0
  8. package/drizzle/0006_project_v140.sql +6 -0
  9. package/drizzle/0007_prd_wizard_v124.sql +14 -0
  10. package/drizzle/0008_workflow_artifact_v126.sql +24 -0
  11. package/drizzle/0009_project_import_v150.sql +7 -0
  12. package/drizzle/0010_quality_v151.sql +3 -0
  13. package/drizzle/meta/0000_snapshot.json +1335 -0
  14. package/drizzle/meta/0001_snapshot.json +1416 -0
  15. package/drizzle/meta/0002_snapshot.json +1510 -0
  16. package/drizzle/meta/_journal.json +83 -0
  17. package/package.json +41 -0
  18. package/plugin/.claude-plugin/marketplace.json +36 -0
  19. package/plugin/.claude-plugin/plugin.json +22 -0
  20. package/plugin/README.md +625 -0
  21. package/plugin/SKILLS.md +350 -0
  22. package/plugin/co2.svg +5 -0
  23. package/plugin/commands/brainstorm-loop.md +129 -0
  24. package/plugin/skills/conductor-defect/SKILL.md +898 -0
  25. package/plugin/skills/conductor-feature-develop/SKILL.md +1383 -0
  26. package/plugin/skills/conductor-feature-develop/references/implementation-domain-template.md +131 -0
  27. package/plugin/skills/conductor-feature-develop/references/implementation-master-template.md +89 -0
  28. package/plugin/skills/conductor-feature-develop/references/playwright-setup.md +224 -0
  29. package/plugin/skills/conductor-feature-prepare/SKILL.md +604 -0
  30. package/plugin/skills/depgen-k8s/SKILL.md +468 -0
  31. package/plugin/skills/depgen-k8s/references/dockerfile-laravel.md +197 -0
  32. package/plugin/skills/depgen-k8s/references/dockerfile-nodejs.md +163 -0
  33. package/plugin/skills/depgen-k8s/references/dockerfile-spring-boot.md +174 -0
  34. package/plugin/skills/depgen-k8s/references/k8s-patterns.md +869 -0
  35. package/plugin/skills/mockgen-shadcn/SKILL.md +1067 -0
  36. package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +1467 -0
  37. package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +498 -0
  38. package/plugin/skills/mockgen-shadcn/references/mockup-index-template.md +216 -0
  39. package/plugin/skills/mockgen-tailwind/SKILL.md +904 -0
  40. package/plugin/skills/mockgen-tailwind/references/admin-layout-template.md +720 -0
  41. package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +498 -0
  42. package/plugin/skills/mockgen-tailwind/references/mockup-index-template.md +190 -0
  43. package/plugin/skills/modelgen-nosql/SKILL.md +617 -0
  44. package/plugin/skills/modelgen-nosql/references/model-extraction-methodology.md +497 -0
  45. package/plugin/skills/modelgen-nosql/references/nosql-design-guide.md +258 -0
  46. package/plugin/skills/modelgen-relational/SKILL.md +707 -0
  47. package/plugin/skills/modelgen-relational/references/model-extraction-methodology.md +499 -0
  48. package/plugin/skills/specgen-flutter-riverpod/SKILL.md +898 -0
  49. package/plugin/skills/specgen-flutter-riverpod/references/component-patterns.md +971 -0
  50. package/plugin/skills/specgen-flutter-riverpod/references/notification-patterns.md +457 -0
  51. package/plugin/skills/specgen-flutter-riverpod/references/routing-patterns.md +362 -0
  52. package/plugin/skills/specgen-flutter-riverpod/references/security-patterns.md +709 -0
  53. package/plugin/skills/specgen-flutter-riverpod/references/spec-template.md +1566 -0
  54. package/plugin/skills/specgen-flutter-riverpod/references/state-patterns.md +518 -0
  55. package/plugin/skills/specgen-flutter-riverpod/references/storage-patterns.md +370 -0
  56. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/SKILL.md +984 -0
  57. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/batch-patterns.md +359 -0
  58. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/messaging-patterns.md +468 -0
  59. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/modulith-patterns.md +461 -0
  60. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/reporting-patterns.md +803 -0
  61. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/security-patterns.md +501 -0
  62. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/spec-template.md +1268 -0
  63. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/web-patterns.md +301 -0
  64. package/plugin/skills/specgen-node-cli-web/SKILL.md +561 -0
  65. package/plugin/skills/specgen-node-cli-web/references/auth-patterns.md +293 -0
  66. package/plugin/skills/specgen-node-cli-web/references/cli-packaging-patterns.md +250 -0
  67. package/plugin/skills/specgen-node-cli-web/references/server-patterns.md +247 -0
  68. package/plugin/skills/specgen-node-cli-web/references/spec-template.md +401 -0
  69. package/plugin/skills/specgen-node-cli-web/references/testing-patterns.md +174 -0
  70. package/plugin/skills/specgen-react-mui/SKILL.md +789 -0
  71. package/plugin/skills/specgen-react-mui/references/component-patterns.md +1077 -0
  72. package/plugin/skills/specgen-react-mui/references/reporting-patterns.md +1998 -0
  73. package/plugin/skills/specgen-react-mui/references/routing-patterns.md +461 -0
  74. package/plugin/skills/specgen-react-mui/references/security-patterns.md +599 -0
  75. package/plugin/skills/specgen-react-mui/references/spec-template.md +1030 -0
  76. package/plugin/skills/specgen-react-mui/references/state-patterns.md +327 -0
  77. package/plugin/skills/specgen-react-tailwind/SKILL.md +859 -0
  78. package/plugin/skills/specgen-react-tailwind/references/component-patterns.md +1212 -0
  79. package/plugin/skills/specgen-react-tailwind/references/reporting-patterns.md +1529 -0
  80. package/plugin/skills/specgen-react-tailwind/references/routing-patterns.md +669 -0
  81. package/plugin/skills/specgen-react-tailwind/references/security-patterns.md +656 -0
  82. package/plugin/skills/specgen-react-tailwind/references/spec-template.md +1237 -0
  83. package/plugin/skills/specgen-react-tailwind/references/state-patterns.md +327 -0
  84. package/plugin/skills/specgen-sdk-java/SKILL.md +933 -0
  85. package/plugin/skills/specgen-sdk-java/references/api-extraction-patterns.md +247 -0
  86. package/plugin/skills/specgen-sdk-java/references/http-client-patterns.md +495 -0
  87. package/plugin/skills/specgen-sdk-java/references/packaging-patterns.md +366 -0
  88. package/plugin/skills/specgen-sdk-java/references/spec-template.md +956 -0
  89. package/plugin/skills/specgen-spring-jpa-jtehtmx/SKILL.md +1284 -0
  90. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/batch-patterns.md +1224 -0
  91. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/jasper-patterns.md +1638 -0
  92. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/messaging-patterns.md +512 -0
  93. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/modulith-patterns.md +354 -0
  94. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/security-patterns.md +551 -0
  95. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/spec-template.md +2502 -0
  96. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/web-patterns.md +503 -0
  97. package/plugin/skills/specgen-spring-jpa-restapi/SKILL.md +990 -0
  98. package/plugin/skills/specgen-spring-jpa-restapi/references/batch-patterns.md +1224 -0
  99. package/plugin/skills/specgen-spring-jpa-restapi/references/messaging-patterns.md +512 -0
  100. package/plugin/skills/specgen-spring-jpa-restapi/references/modulith-patterns.md +279 -0
  101. package/plugin/skills/specgen-spring-jpa-restapi/references/reporting-patterns.md +1536 -0
  102. package/plugin/skills/specgen-spring-jpa-restapi/references/restapi-patterns.md +722 -0
  103. package/plugin/skills/specgen-spring-jpa-restapi/references/security-patterns.md +351 -0
  104. package/plugin/skills/specgen-spring-jpa-restapi/references/spec-template.md +553 -0
  105. package/plugin/skills/specgen-ts-cli/SKILL.md +861 -0
  106. package/plugin/skills/specgen-ts-cli/references/async-patterns.md +1101 -0
  107. package/plugin/skills/specgen-ts-cli/references/command-patterns.md +383 -0
  108. package/plugin/skills/specgen-ts-cli/references/config-patterns.md +430 -0
  109. package/plugin/skills/specgen-ts-cli/references/database-patterns.md +750 -0
  110. package/plugin/skills/specgen-ts-cli/references/packaging-patterns.md +426 -0
  111. package/plugin/skills/specgen-ts-cli/references/spec-template.md +1391 -0
  112. package/plugin/skills/specgen-ts-cli/references/testing-patterns.md +909 -0
  113. package/plugin/skills/testgen-functional/SKILL.md +1381 -0
  114. package/plugin/skills/tracegen-matrix/SKILL.md +243 -0
  115. package/plugin/skills/util-preparek8senv/SKILL.md +422 -0
  116. package/plugin/skills/util-projectinit/SKILL.md +307 -0
  117. package/plugin/skills/util-projectinit/reference/brainstorm-loop.md +129 -0
  118. package/plugin/skills/util-projectinit/reference/brainstorm-protocol.md +144 -0
  119. package/plugin/skills/util-projectinit/reference/brainstorm-setup-guide.md +153 -0
  120. package/plugin/skills/util-projectinit/reference/brainstorm-template-appdev.md +87 -0
  121. package/plugin/skills/util-projectinit/reference/co2-context-generation-guide.md +761 -0
  122. package/plugin/skills/util-projectsync/SKILL.md +501 -0
  123. package/plugin/skills/util-updprd/SKILL.md +324 -0
  124. package/plugin/skills/util-usanalyzer/SKILL.md +307 -0
  125. package/plugin/skills/util-ustagger/SKILL.md +154 -0
  126. package/static/assets/abnfDiagram-VRR7QNED-DvPuwyCV.js +1 -0
  127. package/static/assets/arc-DRKw7sjs.js +1 -0
  128. package/static/assets/architectureDiagram-ZJ3FMSHR-CaNcxUbC.js +36 -0
  129. package/static/assets/blockDiagram-677ZJIJ3-Demd4AOK.js +132 -0
  130. package/static/assets/c4Diagram-LMCZKHZV-PVKUfc2x.js +10 -0
  131. package/static/assets/channel-C2nVbuwv.js +1 -0
  132. package/static/assets/chunk-2Q5K7J3B-CExf8ww_.js +1 -0
  133. package/static/assets/chunk-32BRIVSS-CFPaoONF.js +1 -0
  134. package/static/assets/chunk-5VM5RSS4-DE2esM67.js +15 -0
  135. package/static/assets/chunk-EX3LRPZG-SON_YdDG.js +231 -0
  136. package/static/assets/chunk-JWPE2WC7-Qxah8LNr.js +1 -0
  137. package/static/assets/chunk-MOJQB5TN-BcWGQV3j.js +88 -0
  138. package/static/assets/chunk-RYQCIY6F-DkDj-OR5.js +1 -0
  139. package/static/assets/chunk-V7JOEXUC-BlI1Mrah.js +206 -0
  140. package/static/assets/chunk-VR4S4FIN-Agf__duj.js +1 -0
  141. package/static/assets/chunk-XXDRQBXY-B5CfAh85.js +1 -0
  142. package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +1 -0
  143. package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +1 -0
  144. package/static/assets/cose-bilkent-JH36ORCC-DO0cQkcy.js +1 -0
  145. package/static/assets/cynefin-VYW2F7L2-CfOnsnXc.js +178 -0
  146. package/static/assets/cynefinDiagram-TSTJHNR4-BJ8tULAv.js +62 -0
  147. package/static/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
  148. package/static/assets/dagre-VKFMJZFB-DfjwzHqP.js +4 -0
  149. package/static/assets/defaultLocale-DX6XiGOO.js +1 -0
  150. package/static/assets/diagram-FQU43EPY-CndSOIIM.js +3 -0
  151. package/static/assets/diagram-G47NLZAW-Czf5hN0Z.js +24 -0
  152. package/static/assets/diagram-NH7WQ7WH-COAyojGh.js +24 -0
  153. package/static/assets/diagram-OA4YK3LP-H8PoiN2m.js +30 -0
  154. package/static/assets/diagram-WEI45ONY-DzcaVzjp.js +41 -0
  155. package/static/assets/ebnfDiagram-CCIWWBDH-DPazGYLJ.js +1 -0
  156. package/static/assets/erDiagram-Q63AITRT-Bo-fubgF.js +85 -0
  157. package/static/assets/flowDiagram-23GEKE2U-CSvxUDwh.js +156 -0
  158. package/static/assets/ganttDiagram-NO4QXBWP-DnBveWX6.js +292 -0
  159. package/static/assets/gitGraphDiagram-IHSO6WYX-B3rVL8c-.js +106 -0
  160. package/static/assets/graph-C9eacEi8.js +1 -0
  161. package/static/assets/index-LZSQkLE2.css +41 -0
  162. package/static/assets/index-y7XDSgMG.js +495 -0
  163. package/static/assets/infoDiagram-FWYZ7A6U-CvWV9yy2.js +2 -0
  164. package/static/assets/init-Gi6I4Gst.js +1 -0
  165. package/static/assets/ishikawaDiagram-FXEZZL3T-DF28a1N6.js +70 -0
  166. package/static/assets/journeyDiagram-5HDEW3XC-CB6CugYk.js +139 -0
  167. package/static/assets/kanban-definition-HUTT4EX6-ResSLF6d.js +89 -0
  168. package/static/assets/katex-C5jXJg4s.js +257 -0
  169. package/static/assets/layout-DEXfKzaS.js +1 -0
  170. package/static/assets/linear-CLO--ryk.js +1 -0
  171. package/static/assets/map-Czzmt4hB.js +1 -0
  172. package/static/assets/mindmap-definition-LN4V7U3C-BvW3EbxX.js +96 -0
  173. package/static/assets/ordinal-Cboi1Yqb.js +1 -0
  174. package/static/assets/pegDiagram-2B236MQR-BHCgqrrM.js +1 -0
  175. package/static/assets/pieDiagram-ENE6RG2P-DVmCQ5FA.js +39 -0
  176. package/static/assets/quadrantDiagram-ABIIQ3AL-B3e5KR52.js +7 -0
  177. package/static/assets/railroadDiagram-RFXS5EU6-Csv5ysLc.js +1 -0
  178. package/static/assets/requirementDiagram-TGXJPOKE-DOMVzvnB.js +84 -0
  179. package/static/assets/sankeyDiagram-HTMAVEWB-B1dcTOvy.js +40 -0
  180. package/static/assets/sequenceDiagram-DBY2YBRQ-BY414yeO.js +162 -0
  181. package/static/assets/sizeCapture-X5ZJPWSS-CuJfrfBN.js +1 -0
  182. package/static/assets/stateDiagram-2N3HPSRC-FX4rJkzV.js +1 -0
  183. package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +1 -0
  184. package/static/assets/swimlanes-5IMT3BWC-Sfy58mmv.js +2 -0
  185. package/static/assets/swimlanesDiagram-G3AALYLV-wDCDw08H.js +8 -0
  186. package/static/assets/timeline-definition-FHXFAJF6-CmV7by8p.js +120 -0
  187. package/static/assets/vennDiagram-L72KCM5P-FqIyFZFt.js +34 -0
  188. package/static/assets/wardleyDiagram-EHGQE667-BZ06O45i.js +78 -0
  189. package/static/assets/xychartDiagram-FW5EYKEG-BOkffR06.js +7 -0
  190. package/static/index.html +24 -0
@@ -0,0 +1,1638 @@
1
+ # Reporting Patterns — JasperReports with JRDesign API (Programmatic Layout)
2
+
3
+ This reference describes the reporting infrastructure for the spec. Include this content
4
+ in the Reporting section of the generated specification. All code samples must be reproduced
5
+ in the generated spec with full import statements and constructor injection.
6
+
7
+ The reporting feature provides:
8
+ - **Report Interface** for modules to define and register their own reports
9
+ - **JasperReports JRDesign API** for building report layouts entirely in Java code — no `.jrxml` templates
10
+ - **DTO-based data source** via `JRBeanCollectionDataSource` — no direct DB queries in reports
11
+ - **Multi-format export** — PDF, XLSX, CSV
12
+ - **Report registry** persisted in the database for discoverability
13
+ - **Report parameter forms** rendered via JTE for user input before generation
14
+ - **ReportDesignHelper** utility with static builders for common layout patterns
15
+
16
+ ---
17
+
18
+ ## Architecture Overview
19
+
20
+ ```
21
+ Module Shared Reporting Infrastructure
22
+ ┌─────────────────────┐ ┌─────────────────────────────────────────┐
23
+ │ StaffReport │ │ ReportService │
24
+ │ implements │────────►│ compile(JasperDesign → JasperReport) │
25
+ │ ReportDefinition │ │ fill(JasperReport + DTO Collection) │
26
+ │ │ │ export(JasperPrint → PDF/XLSX/CSV) │
27
+ │ getReportId() │ │ │
28
+ │ getName() │ │ ReportDesignHelper │
29
+ │ getDescription() │ │ createA4PortraitDesign() │
30
+ │ getParameters() │ │ createA4LandscapeDesign() │
31
+ │ buildDesign(...) │ │ addField(), createTitleBand(), etc. │
32
+ │ generateData(...) │ │ │
33
+ │ │ │ ReportRegistry │
34
+ │ DTO: StaffReportDTO│ │ discovers @Component ReportDefinition│
35
+ │ (data source bean) │ │ persists to report table │
36
+ │ │ │ provides report lookup │
37
+ └─────────────────────┘ │ │
38
+ │ ReportController │
39
+ │ GET /reports — list all reports │
40
+ │ GET /reports/{id} — parameter form │
41
+ │ POST /reports/{id}/generate — export │
42
+ └─────────────────────────────────────────┘
43
+ ```
44
+
45
+ **Data flow:**
46
+ 1. User selects a report from the report list page
47
+ 2. Report parameter form is rendered (date range, filters, etc.)
48
+ 3. User submits parameters → `ReportController` dispatches to the matching `ReportDefinition`
49
+ 4. `ReportDefinition.generateData(params)` calls module services to produce a `List<DTO>`
50
+ 5. `ReportDefinition.buildDesign(params)` builds a `JasperDesign` programmatically via JRDesign API
51
+ 6. `ReportService` compiles `JasperDesign` → `JasperReport`, wraps DTOs in `JRBeanCollectionDataSource`, fills, and exports
52
+ 7. The exported file (PDF/XLSX/CSV) is returned as a download response
53
+
54
+ ---
55
+
56
+ ## Maven Dependencies
57
+
58
+ ```xml
59
+ <!-- Reporting (included when Reporting = yes) -->
60
+ <dependency>
61
+ <groupId>net.sf.jasperreports</groupId>
62
+ <artifactId>jasperreports</artifactId>
63
+ <version>7.0.3</version>
64
+ </dependency>
65
+ <dependency>
66
+ <groupId>net.sf.jasperreports</groupId>
67
+ <artifactId>jasperreports-fonts</artifactId>
68
+ <version>7.0.3</version>
69
+ </dependency>
70
+ <!-- PDF export via OpenPDF (JasperReports 7.x default) -->
71
+ <dependency>
72
+ <groupId>com.github.librepdf</groupId>
73
+ <artifactId>openpdf</artifactId>
74
+ <version>2.0.4</version>
75
+ </dependency>
76
+ <!-- XLSX export -->
77
+ <dependency>
78
+ <groupId>org.apache.poi</groupId>
79
+ <artifactId>poi-ooxml</artifactId>
80
+ <version>5.4.1</version>
81
+ </dependency>
82
+ ```
83
+
84
+ > **Note:** JasperReports 7.x uses OpenPDF instead of iText for PDF generation. The
85
+ > `jasperreports` artifact pulls in transitive dependencies, but `openpdf` and `poi-ooxml`
86
+ > should be declared explicitly for version control.
87
+
88
+ ---
89
+
90
+ ## Application Configuration
91
+
92
+ ```yaml
93
+ # [If Reporting = yes] add to application.yml:
94
+ app:
95
+ reporting:
96
+ temp-path: ${java.io.tmpdir}/reports # temp directory for export files
97
+ default-format: PDF # PDF, XLSX, CSV
98
+ ```
99
+
100
+ ### application-prod.yml overrides
101
+
102
+ ```yaml
103
+ app:
104
+ reporting:
105
+ temp-path: ${REPORT_TEMP_PATH:/tmp/reports}
106
+ default-format: PDF
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Package Structure
112
+
113
+ ```
114
+ shared/
115
+ └── reporting/
116
+ ├── ReportDefinition.java # Interface for modules to implement
117
+ ├── ReportParameter.java # Report parameter descriptor record
118
+ ├── ReportFormat.java # Enum: PDF, XLSX, CSV
119
+ ├── ReportResult.java # Holds generated bytes + metadata
120
+ ├── ReportDesignHelper.java # Utility for common JRDesign layout patterns
121
+ ├── ReportService.java # Compile, fill, export orchestrator
122
+ ├── ReportRegistry.java # Auto-discovers and persists report definitions
123
+ ├── ReportConfig.java # Configuration properties binding
124
+ ├── ReportGenerationException.java # Runtime exception for report failures
125
+ ├── ReportEntity.java # [If JPA] Database entity for report registry
126
+ ├── ReportDocument.java # [If MongoDB] Document for report registry
127
+ ├── ReportRepository.java # [If JPA/MongoDB] Spring Data repository
128
+ ├── page/
129
+ │ └── ReportPageController.java # Page controller for report UI
130
+ ├── fragment/
131
+ │ └── ReportFragmentController.java # Fragment controller for htmx updates
132
+ └── view/
133
+ ├── ReportListView.java # View model for report list
134
+ └── ReportGenerateView.java # View model for parameter form
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Report Definition Interface
140
+
141
+ Modules implement this interface to define their reports. Each implementation is
142
+ a Spring `@Component` automatically discovered by the `ReportRegistry`.
143
+
144
+ ```java
145
+ package {{BASE_PACKAGE}}.shared.reporting;
146
+
147
+ import net.sf.jasperreports.engine.design.JasperDesign;
148
+
149
+ import java.util.Collection;
150
+ import java.util.List;
151
+ import java.util.Map;
152
+
153
+ /**
154
+ * Contract for domain-defined reports. Each domain module creates one or more
155
+ * implementations as Spring @Component beans. The ReportRegistry auto-discovers
156
+ * them at startup and persists their metadata to the database.
157
+ */
158
+ public interface ReportDefinition {
159
+
160
+ /**
161
+ * Unique identifier for this report (e.g., "staff-allocation-summary").
162
+ * Used as the report ID in URLs and database records.
163
+ */
164
+ String getReportId();
165
+
166
+ /**
167
+ * Human-readable report name displayed in the report list.
168
+ */
169
+ String getName();
170
+
171
+ /**
172
+ * Brief description of what this report contains.
173
+ */
174
+ String getDescription();
175
+
176
+ /**
177
+ * The domain this report belongs to (e.g., "Staff", "Project").
178
+ * Used for grouping reports in the UI.
179
+ */
180
+ String getDomain();
181
+
182
+ /**
183
+ * Defines the parameters this report accepts (date range, filters, etc.).
184
+ * The ReportController renders a form based on these parameter descriptors.
185
+ * Return an empty list if the report takes no parameters.
186
+ */
187
+ List<ReportParameter> getParameters();
188
+
189
+ /**
190
+ * Builds the report layout programmatically using the JRDesign API.
191
+ * This method constructs a JasperDesign with all bands, fields, styles,
192
+ * and expressions needed for the report. The design is compiled into a
193
+ * JasperReport by the ReportService.
194
+ *
195
+ * @param parameters user-supplied parameter values — allows dynamic layout
196
+ * adjustments based on user input (e.g., hiding columns)
197
+ * @return a fully configured JasperDesign ready for compilation
198
+ */
199
+ JasperDesign buildDesign(Map<String, Object> parameters);
200
+
201
+ /**
202
+ * Executes domain-specific logic to produce the report data as a collection
203
+ * of DTOs. The returned collection is wrapped in JRBeanCollectionDataSource
204
+ * by the ReportService.
205
+ *
206
+ * @param parameters user-supplied parameter values keyed by parameter name
207
+ * @return collection of DTO beans to be used as the Jasper data source
208
+ */
209
+ Collection<?> generateData(Map<String, Object> parameters);
210
+ }
211
+ ```
212
+
213
+ ---
214
+
215
+ ## Report Parameter Descriptor
216
+
217
+ ```java
218
+ package {{BASE_PACKAGE}}.shared.reporting;
219
+
220
+ import java.util.List;
221
+
222
+ /**
223
+ * Describes a single parameter that a report accepts.
224
+ * Used by the ReportController to render the parameter input form.
225
+ *
226
+ * @param name parameter key (e.g., "startDate", "departmentId")
227
+ * @param label human-readable label for the form field
228
+ * @param type input type: TEXT, DATE, NUMBER, SELECT, BOOLEAN
229
+ * @param required whether the parameter is mandatory
230
+ * @param options for SELECT type: list of allowed values (label-value pairs)
231
+ * @param defaultValue default value if user doesn't provide one
232
+ */
233
+ public record ReportParameter(
234
+ String name,
235
+ String label,
236
+ ParameterType type,
237
+ boolean required,
238
+ List<Option> options,
239
+ String defaultValue
240
+ ) {
241
+ public enum ParameterType {
242
+ TEXT, DATE, NUMBER, SELECT, BOOLEAN
243
+ }
244
+
245
+ public record Option(String label, String value) {}
246
+
247
+ public static ReportParameter text(String name, String label, boolean required) {
248
+ return new ReportParameter(name, label, ParameterType.TEXT, required, List.of(), null);
249
+ }
250
+
251
+ public static ReportParameter date(String name, String label, boolean required) {
252
+ return new ReportParameter(name, label, ParameterType.DATE, required, List.of(), null);
253
+ }
254
+
255
+ public static ReportParameter number(String name, String label, boolean required) {
256
+ return new ReportParameter(name, label, ParameterType.NUMBER, required, List.of(), null);
257
+ }
258
+
259
+ public static ReportParameter select(String name, String label, boolean required, List<Option> options) {
260
+ return new ReportParameter(name, label, ParameterType.SELECT, required, options, null);
261
+ }
262
+
263
+ public static ReportParameter bool(String name, String label, String defaultValue) {
264
+ return new ReportParameter(name, label, ParameterType.BOOLEAN, false, List.of(), defaultValue);
265
+ }
266
+ }
267
+ ```
268
+
269
+ ---
270
+
271
+ ## Report Format Enum
272
+
273
+ ```java
274
+ package {{BASE_PACKAGE}}.shared.reporting;
275
+
276
+ public enum ReportFormat {
277
+ PDF("application/pdf", ".pdf"),
278
+ XLSX("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".xlsx"),
279
+ CSV("text/csv", ".csv");
280
+
281
+ private final String contentType;
282
+ private final String extension;
283
+
284
+ ReportFormat(String contentType, String extension) {
285
+ this.contentType = contentType;
286
+ this.extension = extension;
287
+ }
288
+
289
+ public String getContentType() { return contentType; }
290
+ public String getExtension() { return extension; }
291
+ }
292
+ ```
293
+
294
+ ---
295
+
296
+ ## Report Result Record
297
+
298
+ ```java
299
+ package {{BASE_PACKAGE}}.shared.reporting;
300
+
301
+ /**
302
+ * Holds the generated report output.
303
+ *
304
+ * @param data the exported report bytes
305
+ * @param format the export format used
306
+ * @param fileName suggested file name for download
307
+ * @param contentType MIME type for the HTTP response
308
+ */
309
+ public record ReportResult(
310
+ byte[] data,
311
+ ReportFormat format,
312
+ String fileName,
313
+ String contentType
314
+ ) {
315
+ public static ReportResult of(byte[] data, ReportFormat format, String reportId) {
316
+ String fileName = reportId + format.getExtension();
317
+ return new ReportResult(data, format, fileName, format.getContentType());
318
+ }
319
+ }
320
+ ```
321
+
322
+ ---
323
+
324
+ ## Report Design Helper
325
+
326
+ Utility class with static builder methods for common JRDesign layout patterns. Module
327
+ report implementations use these helpers to construct their `JasperDesign` without
328
+ repeating boilerplate for page setup, fonts, bands, and element placement.
329
+
330
+ ```java
331
+ package {{BASE_PACKAGE}}.shared.reporting;
332
+
333
+ import net.sf.jasperreports.engine.design.JRDesignBand;
334
+ import net.sf.jasperreports.engine.design.JRDesignExpression;
335
+ import net.sf.jasperreports.engine.design.JRDesignField;
336
+ import net.sf.jasperreports.engine.design.JRDesignLine;
337
+ import net.sf.jasperreports.engine.design.JRDesignStaticText;
338
+ import net.sf.jasperreports.engine.design.JRDesignStyle;
339
+ import net.sf.jasperreports.engine.design.JRDesignTextField;
340
+ import net.sf.jasperreports.engine.design.JRDesignVariable;
341
+ import net.sf.jasperreports.engine.design.JasperDesign;
342
+ import net.sf.jasperreports.engine.type.EvaluationTimeType;
343
+ import net.sf.jasperreports.engine.type.HorizontalTextAlignEnum;
344
+ import net.sf.jasperreports.engine.type.VerticalTextAlignEnum;
345
+
346
+ import java.awt.Color;
347
+ import java.util.List;
348
+
349
+ /**
350
+ * Static utility methods for building JasperDesign objects programmatically.
351
+ * Provides convenient builders for page setup, fields, bands, and styles
352
+ * so that module-level report implementations stay concise and consistent.
353
+ */
354
+ public final class ReportDesignHelper {
355
+
356
+ private ReportDesignHelper() {
357
+ // utility class — no instantiation
358
+ }
359
+
360
+ /**
361
+ * Column definition used by header and detail band builders.
362
+ *
363
+ * @param label display label for the column header
364
+ * @param fieldName DTO field name (must match the JRDesignField name)
365
+ * @param width column width in pixels
366
+ */
367
+ public record ColumnDef(String label, String fieldName, int width) {}
368
+
369
+ // ── Page Setup ──────────────────────────────────────────────────────
370
+
371
+ /**
372
+ * Create an A4 portrait JasperDesign (595 x 842 pt) with 20pt margins.
373
+ * Column width = 595 - 20 - 20 = 555.
374
+ *
375
+ * @param name report name (used internally by JasperReports)
376
+ * @return a JasperDesign with portrait page dimensions configured
377
+ */
378
+ public static JasperDesign createA4PortraitDesign(String name) {
379
+ JasperDesign design = new JasperDesign();
380
+ design.setName(name);
381
+ design.setPageWidth(595);
382
+ design.setPageHeight(842);
383
+ design.setLeftMargin(20);
384
+ design.setRightMargin(20);
385
+ design.setTopMargin(20);
386
+ design.setBottomMargin(20);
387
+ design.setColumnWidth(555);
388
+ return design;
389
+ }
390
+
391
+ /**
392
+ * Create an A4 landscape JasperDesign (842 x 595 pt) with 20pt margins.
393
+ * Column width = 842 - 20 - 20 = 802.
394
+ *
395
+ * @param name report name (used internally by JasperReports)
396
+ * @return a JasperDesign with landscape page dimensions configured
397
+ */
398
+ public static JasperDesign createA4LandscapeDesign(String name) {
399
+ JasperDesign design = new JasperDesign();
400
+ design.setName(name);
401
+ design.setPageWidth(842);
402
+ design.setPageHeight(595);
403
+ design.setLeftMargin(20);
404
+ design.setRightMargin(20);
405
+ design.setTopMargin(20);
406
+ design.setBottomMargin(20);
407
+ design.setColumnWidth(802);
408
+ return design;
409
+ }
410
+
411
+ // ── Fields ──────────────────────────────────────────────────────────
412
+
413
+ /**
414
+ * Add a field declaration to the design. Field names must match the getter
415
+ * names on the DTO (JavaBean convention: field "staffNumber" maps to
416
+ * getStaffNumber()).
417
+ *
418
+ * @param design the JasperDesign to add the field to
419
+ * @param name field name matching the DTO property
420
+ * @param type Java class of the field (e.g., String.class, Integer.class)
421
+ */
422
+ public static void addField(JasperDesign design, String name, Class<?> type) {
423
+ try {
424
+ JRDesignField field = new JRDesignField();
425
+ field.setName(name);
426
+ field.setValueClass(type);
427
+ design.addField(field);
428
+ } catch (Exception e) {
429
+ throw new ReportGenerationException("Failed to add field: " + name, e);
430
+ }
431
+ }
432
+
433
+ // ── Styles ──────────────────────────────────────────────────────────
434
+
435
+ /**
436
+ * Create a named style and add it to the design.
437
+ *
438
+ * @param design the JasperDesign to register the style with
439
+ * @param name unique style name (referenced by elements)
440
+ * @param fontName font family name (e.g., "SansSerif", "DejaVu Sans")
441
+ * @param fontSize font size in points
442
+ * @param bold whether the font is bold
443
+ * @return the created JRDesignStyle for further customization if needed
444
+ */
445
+ public static JRDesignStyle createStyle(JasperDesign design, String name,
446
+ String fontName, int fontSize, boolean bold) {
447
+ try {
448
+ JRDesignStyle style = new JRDesignStyle();
449
+ style.setName(name);
450
+ style.setFontName(fontName);
451
+ style.setFontSize((float) fontSize);
452
+ style.setBold(bold);
453
+ design.addStyle(style);
454
+ return style;
455
+ } catch (Exception e) {
456
+ throw new ReportGenerationException("Failed to create style: " + name, e);
457
+ }
458
+ }
459
+
460
+ // ── Title Band ──────────────────────────────────────────────────────
461
+
462
+ /**
463
+ * Create a title band with a centered title label.
464
+ *
465
+ * @param title the report title text
466
+ * @param bandHeight height of the title band in pixels
467
+ * @param columnWidth full usable column width of the page
468
+ * @return a JRDesignBand configured as the title band
469
+ */
470
+ public static JRDesignBand createTitleBand(String title, int bandHeight, int columnWidth) {
471
+ JRDesignBand band = new JRDesignBand();
472
+ band.setHeight(bandHeight);
473
+
474
+ JRDesignStaticText titleText = new JRDesignStaticText();
475
+ titleText.setX(0);
476
+ titleText.setY(0);
477
+ titleText.setWidth(columnWidth);
478
+ titleText.setHeight(bandHeight - 10);
479
+ titleText.setText(title);
480
+ titleText.setFontSize(18f);
481
+ titleText.setBold(true);
482
+ titleText.setHorizontalTextAlign(HorizontalTextAlignEnum.CENTER);
483
+ titleText.setVerticalTextAlign(VerticalTextAlignEnum.MIDDLE);
484
+ band.addElement(titleText);
485
+
486
+ // Separator line below title
487
+ JRDesignLine line = new JRDesignLine();
488
+ line.setX(0);
489
+ line.setY(bandHeight - 5);
490
+ line.setWidth(columnWidth);
491
+ line.setHeight(1);
492
+ line.setForecolor(Color.GRAY);
493
+ band.addElement(line);
494
+
495
+ return band;
496
+ }
497
+
498
+ // ── Column Header Band ──────────────────────────────────────────────
499
+
500
+ /**
501
+ * Create a column header band with static text labels for each column.
502
+ * Headers are rendered with a light gray background and bold font.
503
+ *
504
+ * @param columns list of column definitions (label, fieldName, width)
505
+ * @return a JRDesignBand configured as the column header band
506
+ */
507
+ public static JRDesignBand createColumnHeaderBand(List<ColumnDef> columns) {
508
+ int bandHeight = 30;
509
+ JRDesignBand band = new JRDesignBand();
510
+ band.setHeight(bandHeight);
511
+
512
+ int xOffset = 0;
513
+ for (ColumnDef col : columns) {
514
+ JRDesignStaticText header = new JRDesignStaticText();
515
+ header.setX(xOffset);
516
+ header.setY(0);
517
+ header.setWidth(col.width());
518
+ header.setHeight(bandHeight);
519
+ header.setText(col.label());
520
+ header.setFontSize(10f);
521
+ header.setBold(true);
522
+ header.setVerticalTextAlign(VerticalTextAlignEnum.MIDDLE);
523
+ header.setMode(net.sf.jasperreports.engine.type.ModeEnum.OPAQUE);
524
+ header.setBackcolor(new Color(0xE8, 0xE8, 0xE8));
525
+ band.addElement(header);
526
+
527
+ xOffset += col.width();
528
+ }
529
+
530
+ return band;
531
+ }
532
+
533
+ // ── Detail Band ─────────────────────────────────────────────────────
534
+
535
+ /**
536
+ * Create a detail band with text fields bound to DTO properties.
537
+ * Each column renders a $F{fieldName} expression matching the DTO getter.
538
+ *
539
+ * @param columns list of column definitions — fieldName must match an addField() call
540
+ * @return a JRDesignBand configured as the detail band
541
+ */
542
+ public static JRDesignBand createDetailBand(List<ColumnDef> columns) {
543
+ int bandHeight = 25;
544
+ JRDesignBand band = new JRDesignBand();
545
+ band.setHeight(bandHeight);
546
+
547
+ int xOffset = 0;
548
+ for (ColumnDef col : columns) {
549
+ JRDesignTextField textField = new JRDesignTextField();
550
+ textField.setX(xOffset);
551
+ textField.setY(0);
552
+ textField.setWidth(col.width());
553
+ textField.setHeight(bandHeight);
554
+ textField.setVerticalTextAlign(VerticalTextAlignEnum.MIDDLE);
555
+ textField.setBlankWhenNull(true);
556
+
557
+ JRDesignExpression expression = new JRDesignExpression();
558
+ expression.setText("$F{" + col.fieldName() + "}");
559
+ textField.setExpression(expression);
560
+
561
+ band.addElement(textField);
562
+ xOffset += col.width();
563
+ }
564
+
565
+ return band;
566
+ }
567
+
568
+ // ── Page Footer Band ────────────────────────────────────────────────
569
+
570
+ /**
571
+ * Create a page footer band with "Page X of Y" text.
572
+ * Uses the built-in PAGE_NUMBER variable with evaluation-time splitting
573
+ * to display total page count.
574
+ *
575
+ * @param columnWidth full usable column width of the page
576
+ * @return a JRDesignBand configured as the page footer band
577
+ */
578
+ public static JRDesignBand createPageFooterBand(int columnWidth) {
579
+ int bandHeight = 30;
580
+ JRDesignBand band = new JRDesignBand();
581
+ band.setHeight(bandHeight);
582
+
583
+ // Separator line above footer
584
+ JRDesignLine line = new JRDesignLine();
585
+ line.setX(0);
586
+ line.setY(0);
587
+ line.setWidth(columnWidth);
588
+ line.setHeight(1);
589
+ line.setForecolor(Color.LIGHT_GRAY);
590
+ band.addElement(line);
591
+
592
+ // "Page X" — evaluated per page
593
+ JRDesignTextField pageCurrentField = new JRDesignTextField();
594
+ pageCurrentField.setX(0);
595
+ pageCurrentField.setY(5);
596
+ pageCurrentField.setWidth(100);
597
+ pageCurrentField.setHeight(20);
598
+ pageCurrentField.setVerticalTextAlign(VerticalTextAlignEnum.MIDDLE);
599
+ pageCurrentField.setFontSize(8f);
600
+
601
+ JRDesignExpression currentExpr = new JRDesignExpression();
602
+ currentExpr.setText("\"Page \" + $V{PAGE_NUMBER}");
603
+ pageCurrentField.setExpression(currentExpr);
604
+ band.addElement(pageCurrentField);
605
+
606
+ // " of Y" — evaluated at report level (after all pages rendered)
607
+ JRDesignTextField pageTotalField = new JRDesignTextField();
608
+ pageTotalField.setX(100);
609
+ pageTotalField.setY(5);
610
+ pageTotalField.setWidth(100);
611
+ pageTotalField.setHeight(20);
612
+ pageTotalField.setVerticalTextAlign(VerticalTextAlignEnum.MIDDLE);
613
+ pageTotalField.setFontSize(8f);
614
+ pageTotalField.setEvaluationTime(EvaluationTimeType.REPORT);
615
+
616
+ JRDesignExpression totalExpr = new JRDesignExpression();
617
+ totalExpr.setText("\" of \" + $V{PAGE_NUMBER}");
618
+ pageTotalField.setExpression(totalExpr);
619
+ band.addElement(pageTotalField);
620
+
621
+ return band;
622
+ }
623
+ }
624
+ ```
625
+
626
+ ---
627
+
628
+ ## Report Configuration
629
+
630
+ ```java
631
+ package {{BASE_PACKAGE}}.shared.reporting;
632
+
633
+ import lombok.Getter;
634
+ import lombok.Setter;
635
+ import org.springframework.boot.context.properties.ConfigurationProperties;
636
+ import org.springframework.context.annotation.Configuration;
637
+
638
+ @Configuration
639
+ @ConfigurationProperties(prefix = "app.reporting")
640
+ @Getter
641
+ @Setter
642
+ public class ReportConfig {
643
+
644
+ private String tempPath = System.getProperty("java.io.tmpdir") + "/reports";
645
+ private ReportFormat defaultFormat = ReportFormat.PDF;
646
+ }
647
+ ```
648
+
649
+ ---
650
+
651
+ ## Report Service
652
+
653
+ The central orchestrator that compiles `JasperDesign` objects returned by report
654
+ definitions, fills them with DTO collections via `JRBeanCollectionDataSource`, and
655
+ exports to the requested format.
656
+
657
+ ```java
658
+ package {{BASE_PACKAGE}}.shared.reporting;
659
+
660
+ import lombok.RequiredArgsConstructor;
661
+ import lombok.extern.slf4j.Slf4j;
662
+ import net.sf.jasperreports.engine.JRException;
663
+ import net.sf.jasperreports.engine.JasperCompileManager;
664
+ import net.sf.jasperreports.engine.JasperFillManager;
665
+ import net.sf.jasperreports.engine.JasperPrint;
666
+ import net.sf.jasperreports.engine.JasperReport;
667
+ import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;
668
+ import net.sf.jasperreports.engine.design.JasperDesign;
669
+ import net.sf.jasperreports.engine.export.JRCsvExporter;
670
+ import net.sf.jasperreports.engine.export.ooxml.JRXlsxExporter;
671
+ import net.sf.jasperreports.export.SimpleCsvExporterConfiguration;
672
+ import net.sf.jasperreports.export.SimpleExporterInput;
673
+ import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput;
674
+ import net.sf.jasperreports.export.SimpleXlsxReportConfiguration;
675
+ import net.sf.jasperreports.pdf.JRPdfExporter;
676
+ import org.springframework.stereotype.Service;
677
+
678
+ import java.io.ByteArrayOutputStream;
679
+ import java.util.Collection;
680
+ import java.util.HashMap;
681
+ import java.util.Map;
682
+ import java.util.concurrent.ConcurrentHashMap;
683
+
684
+ @Service
685
+ @RequiredArgsConstructor
686
+ @Slf4j
687
+ public class ReportService {
688
+
689
+ private final ReportConfig config;
690
+
691
+ // Cache compiled JasperReport objects by reportId to avoid re-compilation on every request
692
+ private final Map<String, JasperReport> compiledCache = new ConcurrentHashMap<>();
693
+
694
+ /**
695
+ * Generate a report using the specified ReportDefinition, user parameters, and format.
696
+ *
697
+ * @param definition the report definition (builds design and generates data)
698
+ * @param parameters user-supplied parameter values
699
+ * @param format desired export format (PDF, XLSX, CSV)
700
+ * @return ReportResult containing the exported bytes and metadata
701
+ */
702
+ public ReportResult generate(ReportDefinition definition,
703
+ Map<String, Object> parameters,
704
+ ReportFormat format) {
705
+ log.info("Generating report [{}] in format [{}]", definition.getReportId(), format);
706
+
707
+ // 1. Generate the data from the domain module
708
+ Collection<?> data = definition.generateData(parameters);
709
+ log.debug("Report [{}] produced {} data records", definition.getReportId(), data.size());
710
+
711
+ // 2. Compile the JasperDesign (cached by reportId)
712
+ JasperReport jasperReport = compileDesign(definition, parameters);
713
+
714
+ // 3. Create the DTO-based data source
715
+ JRBeanCollectionDataSource dataSource = new JRBeanCollectionDataSource(data);
716
+
717
+ // 4. Build Jasper parameters (pass user params + any report-level params)
718
+ Map<String, Object> jasperParams = new HashMap<>(parameters);
719
+
720
+ // 5. Fill the report
721
+ JasperPrint jasperPrint = fillReport(jasperReport, jasperParams, dataSource);
722
+
723
+ // 6. Export to the requested format
724
+ byte[] exportedBytes = exportReport(jasperPrint, format);
725
+
726
+ log.info("Report [{}] generated successfully: {} bytes", definition.getReportId(), exportedBytes.length);
727
+ return ReportResult.of(exportedBytes, format, definition.getReportId());
728
+ }
729
+
730
+ /**
731
+ * Compile a JasperDesign into a JasperReport. Results are cached by reportId.
732
+ * The design is built programmatically by the ReportDefinition — no XML files involved.
733
+ */
734
+ private JasperReport compileDesign(ReportDefinition definition,
735
+ Map<String, Object> parameters) {
736
+ return compiledCache.computeIfAbsent(definition.getReportId(), id -> {
737
+ try {
738
+ log.debug("Building and compiling design for report: {}", id);
739
+ JasperDesign design = definition.buildDesign(parameters);
740
+ return JasperCompileManager.compileReport(design);
741
+ } catch (JRException e) {
742
+ throw new ReportGenerationException("Failed to compile design for report: " + id, e);
743
+ }
744
+ });
745
+ }
746
+
747
+ /**
748
+ * Fill a compiled JasperReport with parameters and data source.
749
+ */
750
+ private JasperPrint fillReport(JasperReport report,
751
+ Map<String, Object> parameters,
752
+ JRBeanCollectionDataSource dataSource) {
753
+ try {
754
+ return JasperFillManager.fillReport(report, parameters, dataSource);
755
+ } catch (JRException e) {
756
+ throw new ReportGenerationException("Failed to fill report", e);
757
+ }
758
+ }
759
+
760
+ /**
761
+ * Export JasperPrint to the specified format.
762
+ */
763
+ private byte[] exportReport(JasperPrint jasperPrint, ReportFormat format) {
764
+ try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
765
+ switch (format) {
766
+ case PDF -> exportToPdf(jasperPrint, outputStream);
767
+ case XLSX -> exportToXlsx(jasperPrint, outputStream);
768
+ case CSV -> exportToCsv(jasperPrint, outputStream);
769
+ }
770
+ return outputStream.toByteArray();
771
+ } catch (Exception e) {
772
+ throw new ReportGenerationException("Failed to export report as " + format, e);
773
+ }
774
+ }
775
+
776
+ private void exportToPdf(JasperPrint print, ByteArrayOutputStream out) throws JRException {
777
+ JRPdfExporter exporter = new JRPdfExporter();
778
+ exporter.setExporterInput(new SimpleExporterInput(print));
779
+ exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(out));
780
+ exporter.exportReport();
781
+ }
782
+
783
+ private void exportToXlsx(JasperPrint print, ByteArrayOutputStream out) throws JRException {
784
+ JRXlsxExporter exporter = new JRXlsxExporter();
785
+ exporter.setExporterInput(new SimpleExporterInput(print));
786
+ exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(out));
787
+
788
+ SimpleXlsxReportConfiguration xlsxConfig = new SimpleXlsxReportConfiguration();
789
+ xlsxConfig.setOnePagePerSheet(false);
790
+ xlsxConfig.setRemoveEmptySpaceBetweenRows(true);
791
+ xlsxConfig.setDetectCellType(true);
792
+ xlsxConfig.setWhitePageBackground(false);
793
+ exporter.setConfiguration(xlsxConfig);
794
+
795
+ exporter.exportReport();
796
+ }
797
+
798
+ private void exportToCsv(JasperPrint print, ByteArrayOutputStream out) throws JRException {
799
+ JRCsvExporter exporter = new JRCsvExporter();
800
+ exporter.setExporterInput(new SimpleExporterInput(print));
801
+ exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(out));
802
+
803
+ SimpleCsvExporterConfiguration csvConfig = new SimpleCsvExporterConfiguration();
804
+ csvConfig.setFieldDelimiter(",");
805
+ exporter.setConfiguration(csvConfig);
806
+
807
+ exporter.exportReport();
808
+ }
809
+
810
+ /**
811
+ * Evict a cached compiled report by its reportId. Call this when the
812
+ * buildDesign() logic changes and the cached JasperReport must be rebuilt.
813
+ * Typically used during development or after deploying updated report logic.
814
+ *
815
+ * @param reportId the report identifier to evict from the cache
816
+ */
817
+ public void evictReport(String reportId) {
818
+ compiledCache.remove(reportId);
819
+ log.info("Evicted cached compiled report: {}", reportId);
820
+ }
821
+ }
822
+ ```
823
+
824
+ ---
825
+
826
+ ## Report Generation Exception
827
+
828
+ ```java
829
+ package {{BASE_PACKAGE}}.shared.reporting;
830
+
831
+ public class ReportGenerationException extends RuntimeException {
832
+
833
+ public ReportGenerationException(String message, Throwable cause) {
834
+ super(message, cause);
835
+ }
836
+
837
+ public ReportGenerationException(String message) {
838
+ super(message);
839
+ }
840
+ }
841
+ ```
842
+
843
+ ---
844
+
845
+ ## Report Registry
846
+
847
+ The registry auto-discovers all `ReportDefinition` beans at startup and persists their
848
+ metadata to the database. This allows the report list page to show all available reports
849
+ without scanning components at runtime.
850
+
851
+ ### [If PostgreSQL/MySQL] Report Entity
852
+
853
+ ```java
854
+ package {{BASE_PACKAGE}}.shared.reporting;
855
+
856
+ import jakarta.persistence.Column;
857
+ import jakarta.persistence.Entity;
858
+ import jakarta.persistence.Id;
859
+ import jakarta.persistence.Table;
860
+ import lombok.Getter;
861
+ import lombok.Setter;
862
+
863
+ @Entity
864
+ @Table(name = "report")
865
+ @Getter
866
+ @Setter
867
+ public class ReportEntity {
868
+
869
+ @Id
870
+ @Column(name = "report_id", length = 100)
871
+ private String reportId;
872
+
873
+ @Column(name = "name", nullable = false, length = 255)
874
+ private String name;
875
+
876
+ @Column(name = "description", length = 500)
877
+ private String description;
878
+
879
+ @Column(name = "domain", nullable = false, length = 100)
880
+ private String domain;
881
+
882
+ @Column(name = "active", nullable = false)
883
+ private boolean active = true;
884
+ }
885
+ ```
886
+
887
+ ### [If PostgreSQL/MySQL] Report Repository
888
+
889
+ ```java
890
+ package {{BASE_PACKAGE}}.shared.reporting;
891
+
892
+ import org.springframework.data.jpa.repository.JpaRepository;
893
+
894
+ import java.util.List;
895
+
896
+ public interface ReportRepository extends JpaRepository<ReportEntity, String> {
897
+
898
+ List<ReportEntity> findByActiveTrue();
899
+
900
+ List<ReportEntity> findByDomainAndActiveTrue(String domain);
901
+ }
902
+ ```
903
+
904
+ ### [If PostgreSQL/MySQL] Flyway Migration
905
+
906
+ ```sql
907
+ -- V{N}__create_report_table.sql
908
+ CREATE TABLE report (
909
+ report_id VARCHAR(100) PRIMARY KEY,
910
+ name VARCHAR(255) NOT NULL,
911
+ description VARCHAR(500),
912
+ domain VARCHAR(100) NOT NULL,
913
+ active BOOLEAN NOT NULL DEFAULT TRUE
914
+ );
915
+ ```
916
+
917
+ ### [If MongoDB] Report Document
918
+
919
+ ```java
920
+ package {{BASE_PACKAGE}}.shared.reporting;
921
+
922
+ import lombok.Getter;
923
+ import lombok.Setter;
924
+ import org.springframework.data.annotation.Id;
925
+ import org.springframework.data.mongodb.core.mapping.Document;
926
+
927
+ @Document(collection = "report")
928
+ @Getter
929
+ @Setter
930
+ public class ReportDocument {
931
+
932
+ @Id
933
+ private String reportId;
934
+ private String name;
935
+ private String description;
936
+ private String domain;
937
+ private boolean active = true;
938
+ }
939
+ ```
940
+
941
+ ### [If MongoDB] Report Repository
942
+
943
+ ```java
944
+ package {{BASE_PACKAGE}}.shared.reporting;
945
+
946
+ import org.springframework.data.mongodb.repository.MongoRepository;
947
+
948
+ import java.util.List;
949
+
950
+ public interface ReportRepository extends MongoRepository<ReportDocument, String> {
951
+
952
+ List<ReportDocument> findByActiveTrue();
953
+
954
+ List<ReportDocument> findByDomainAndActiveTrue(String domain);
955
+ }
956
+ ```
957
+
958
+ ### Registry Service
959
+
960
+ ```java
961
+ package {{BASE_PACKAGE}}.shared.reporting;
962
+
963
+ import lombok.RequiredArgsConstructor;
964
+ import lombok.extern.slf4j.Slf4j;
965
+ import org.springframework.boot.context.event.ApplicationReadyEvent;
966
+ import org.springframework.context.event.EventListener;
967
+ import org.springframework.stereotype.Service;
968
+
969
+ import java.util.List;
970
+ import java.util.Map;
971
+ import java.util.Optional;
972
+ import java.util.concurrent.ConcurrentHashMap;
973
+
974
+ @Service
975
+ @RequiredArgsConstructor
976
+ @Slf4j
977
+ public class ReportRegistry {
978
+
979
+ private final List<ReportDefinition> reportDefinitions;
980
+ private final ReportRepository reportRepository;
981
+
982
+ // In-memory lookup for fast dispatch during report generation
983
+ private final Map<String, ReportDefinition> definitionMap = new ConcurrentHashMap<>();
984
+
985
+ /**
986
+ * On application startup, register all ReportDefinition beans in the database
987
+ * and build the in-memory lookup map.
988
+ */
989
+ @EventListener(ApplicationReadyEvent.class)
990
+ public void registerReports() {
991
+ log.info("Registering {} report definitions", reportDefinitions.size());
992
+
993
+ for (ReportDefinition definition : reportDefinitions) {
994
+ definitionMap.put(definition.getReportId(), definition);
995
+
996
+ // Upsert to database — [If PostgreSQL/MySQL] use ReportEntity,
997
+ // [If MongoDB] use ReportDocument
998
+ ReportEntity entity = reportRepository.findById(definition.getReportId())
999
+ .orElseGet(ReportEntity::new);
1000
+ entity.setReportId(definition.getReportId());
1001
+ entity.setName(definition.getName());
1002
+ entity.setDescription(definition.getDescription());
1003
+ entity.setDomain(definition.getDomain());
1004
+ entity.setActive(true);
1005
+ reportRepository.save(entity);
1006
+
1007
+ log.info("Registered report: [{}] {} ({})",
1008
+ definition.getReportId(), definition.getName(), definition.getDomain());
1009
+ }
1010
+
1011
+ // Deactivate reports that no longer have a definition
1012
+ List<ReportEntity> allReports = reportRepository.findAll();
1013
+ for (ReportEntity entity : allReports) {
1014
+ if (!definitionMap.containsKey(entity.getReportId())) {
1015
+ entity.setActive(false);
1016
+ reportRepository.save(entity);
1017
+ log.info("Deactivated report: [{}] (no definition found)", entity.getReportId());
1018
+ }
1019
+ }
1020
+ }
1021
+
1022
+ /**
1023
+ * Get a ReportDefinition by its ID for report generation.
1024
+ */
1025
+ public Optional<ReportDefinition> getDefinition(String reportId) {
1026
+ return Optional.ofNullable(definitionMap.get(reportId));
1027
+ }
1028
+
1029
+ /**
1030
+ * List all active reports from the database.
1031
+ */
1032
+ public List<ReportEntity> listActiveReports() {
1033
+ return reportRepository.findByActiveTrue();
1034
+ }
1035
+
1036
+ /**
1037
+ * List active reports for a specific domain.
1038
+ */
1039
+ public List<ReportEntity> listReportsByDomain(String domain) {
1040
+ return reportRepository.findByDomainAndActiveTrue(domain);
1041
+ }
1042
+ }
1043
+ ```
1044
+
1045
+ ---
1046
+
1047
+ ## Report Page Controller
1048
+
1049
+ ```java
1050
+ package {{BASE_PACKAGE}}.shared.reporting.page;
1051
+
1052
+ import {{BASE_PACKAGE}}.shared.reporting.ReportDefinition;
1053
+ import {{BASE_PACKAGE}}.shared.reporting.ReportFormat;
1054
+ import {{BASE_PACKAGE}}.shared.reporting.ReportRegistry;
1055
+ import {{BASE_PACKAGE}}.shared.reporting.ReportResult;
1056
+ import {{BASE_PACKAGE}}.shared.reporting.ReportService;
1057
+ import {{BASE_PACKAGE}}.shared.reporting.view.ReportGenerateView;
1058
+ import {{BASE_PACKAGE}}.shared.reporting.view.ReportListView;
1059
+ import lombok.RequiredArgsConstructor;
1060
+ import lombok.extern.slf4j.Slf4j;
1061
+ import org.springframework.http.HttpHeaders;
1062
+ import org.springframework.http.MediaType;
1063
+ import org.springframework.http.ResponseEntity;
1064
+ import org.springframework.security.access.prepost.PreAuthorize;
1065
+ import org.springframework.stereotype.Controller;
1066
+ import org.springframework.web.bind.annotation.GetMapping;
1067
+ import org.springframework.web.bind.annotation.PathVariable;
1068
+ import org.springframework.web.bind.annotation.PostMapping;
1069
+ import org.springframework.web.bind.annotation.RequestMapping;
1070
+ import org.springframework.web.bind.annotation.RequestParam;
1071
+ import org.springframework.web.servlet.ModelAndView;
1072
+
1073
+ import java.util.Map;
1074
+
1075
+ @Controller
1076
+ @RequestMapping("/reports")
1077
+ @PreAuthorize("isAuthenticated()")
1078
+ @RequiredArgsConstructor
1079
+ @Slf4j
1080
+ public class ReportPageController {
1081
+
1082
+ private final ReportRegistry reportRegistry;
1083
+ private final ReportService reportService;
1084
+
1085
+ /**
1086
+ * List all available reports grouped by domain.
1087
+ */
1088
+ @GetMapping
1089
+ public ModelAndView listReports() {
1090
+ var reports = reportRegistry.listActiveReports();
1091
+ var view = ReportListView.from(reports);
1092
+ return new ModelAndView("shared/reporting/page/list", Map.of("view", view));
1093
+ }
1094
+
1095
+ /**
1096
+ * Show the parameter form for a specific report.
1097
+ */
1098
+ @GetMapping("/{reportId}")
1099
+ public ModelAndView showReportForm(@PathVariable String reportId) {
1100
+ var definition = reportRegistry.getDefinition(reportId)
1101
+ .orElseThrow(() -> new IllegalArgumentException("Report not found: " + reportId));
1102
+ var view = ReportGenerateView.from(definition);
1103
+ return new ModelAndView("shared/reporting/page/generate", Map.of("view", view));
1104
+ }
1105
+
1106
+ /**
1107
+ * Generate and download the report.
1108
+ */
1109
+ @PostMapping("/{reportId}/generate")
1110
+ public ResponseEntity<byte[]> generateReport(
1111
+ @PathVariable String reportId,
1112
+ @RequestParam Map<String, Object> parameters,
1113
+ @RequestParam(defaultValue = "PDF") ReportFormat format) {
1114
+
1115
+ var definition = reportRegistry.getDefinition(reportId)
1116
+ .orElseThrow(() -> new IllegalArgumentException("Report not found: " + reportId));
1117
+
1118
+ ReportResult result = reportService.generate(definition, parameters, format);
1119
+
1120
+ return ResponseEntity.ok()
1121
+ .contentType(MediaType.parseMediaType(result.contentType()))
1122
+ .header(HttpHeaders.CONTENT_DISPOSITION,
1123
+ "attachment; filename=\"" + result.fileName() + "\"")
1124
+ .body(result.data());
1125
+ }
1126
+ }
1127
+ ```
1128
+
1129
+ ---
1130
+
1131
+ ## Report Fragment Controller
1132
+
1133
+ ```java
1134
+ package {{BASE_PACKAGE}}.shared.reporting.fragment;
1135
+
1136
+ import {{BASE_PACKAGE}}.shared.reporting.ReportRegistry;
1137
+ import {{BASE_PACKAGE}}.shared.reporting.view.ReportGenerateView;
1138
+ import {{BASE_PACKAGE}}.shared.reporting.view.ReportListView;
1139
+ import lombok.RequiredArgsConstructor;
1140
+ import lombok.extern.slf4j.Slf4j;
1141
+ import org.springframework.security.access.prepost.PreAuthorize;
1142
+ import org.springframework.stereotype.Controller;
1143
+ import org.springframework.web.bind.annotation.GetMapping;
1144
+ import org.springframework.web.bind.annotation.PathVariable;
1145
+ import org.springframework.web.bind.annotation.RequestMapping;
1146
+ import org.springframework.web.servlet.ModelAndView;
1147
+
1148
+ import java.util.Map;
1149
+
1150
+ @Controller
1151
+ @RequestMapping("/reports/fragments")
1152
+ @PreAuthorize("isAuthenticated()")
1153
+ @RequiredArgsConstructor
1154
+ @Slf4j
1155
+ public class ReportFragmentController {
1156
+
1157
+ private final ReportRegistry reportRegistry;
1158
+
1159
+ /**
1160
+ * htmx fragment: report list content (replaces #content-area without full page load).
1161
+ */
1162
+ @GetMapping("/list")
1163
+ public ModelAndView listReportsFragment() {
1164
+ var reports = reportRegistry.listActiveReports();
1165
+ var view = ReportListView.from(reports);
1166
+ return new ModelAndView("shared/reporting/fragment/list", Map.of("view", view));
1167
+ }
1168
+
1169
+ /**
1170
+ * htmx fragment: report parameter form (replaces #content-area without full page load).
1171
+ */
1172
+ @GetMapping("/{reportId}")
1173
+ public ModelAndView showReportFormFragment(@PathVariable String reportId) {
1174
+ var definition = reportRegistry.getDefinition(reportId)
1175
+ .orElseThrow(() -> new IllegalArgumentException("Report not found: " + reportId));
1176
+ var view = ReportGenerateView.from(definition);
1177
+ return new ModelAndView("shared/reporting/fragment/generate", Map.of("view", view));
1178
+ }
1179
+ }
1180
+ ```
1181
+
1182
+ ---
1183
+
1184
+ ## Report View Models
1185
+
1186
+ ```java
1187
+ package {{BASE_PACKAGE}}.shared.reporting.view;
1188
+
1189
+ import {{BASE_PACKAGE}}.shared.reporting.ReportEntity;
1190
+ import lombok.Getter;
1191
+ import lombok.Setter;
1192
+
1193
+ import java.util.List;
1194
+ import java.util.Map;
1195
+ import java.util.stream.Collectors;
1196
+
1197
+ @Getter
1198
+ @Setter
1199
+ public class ReportListView {
1200
+
1201
+ private Map<String, List<ReportEntry>> reportsByDomain;
1202
+ private int totalReports;
1203
+
1204
+ public record ReportEntry(String reportId, String name, String description, String domain) {}
1205
+
1206
+ public static ReportListView from(List<ReportEntity> entities) {
1207
+ var view = new ReportListView();
1208
+ view.totalReports = entities.size();
1209
+ view.reportsByDomain = entities.stream()
1210
+ .map(e -> new ReportEntry(e.getReportId(), e.getName(), e.getDescription(), e.getDomain()))
1211
+ .collect(Collectors.groupingBy(ReportEntry::domain));
1212
+ return view;
1213
+ }
1214
+ }
1215
+ ```
1216
+
1217
+ ```java
1218
+ package {{BASE_PACKAGE}}.shared.reporting.view;
1219
+
1220
+ import {{BASE_PACKAGE}}.shared.reporting.ReportDefinition;
1221
+ import {{BASE_PACKAGE}}.shared.reporting.ReportParameter;
1222
+ import lombok.Getter;
1223
+ import lombok.Setter;
1224
+
1225
+ import java.util.List;
1226
+
1227
+ @Getter
1228
+ @Setter
1229
+ public class ReportGenerateView {
1230
+
1231
+ private String reportId;
1232
+ private String name;
1233
+ private String description;
1234
+ private String domain;
1235
+ private List<ReportParameter> parameters;
1236
+
1237
+ public static ReportGenerateView from(ReportDefinition definition) {
1238
+ var view = new ReportGenerateView();
1239
+ view.reportId = definition.getReportId();
1240
+ view.name = definition.getName();
1241
+ view.description = definition.getDescription();
1242
+ view.domain = definition.getDomain();
1243
+ view.parameters = definition.getParameters();
1244
+ return view;
1245
+ }
1246
+ }
1247
+ ```
1248
+
1249
+ ---
1250
+
1251
+ ## JTE Templates
1252
+
1253
+ ### Report List Page
1254
+
1255
+ ```html
1256
+ @import {{BASE_PACKAGE}}.shared.reporting.view.ReportListView
1257
+ @import {{BASE_PACKAGE}}.shared.reporting.view.ReportListView.ReportEntry
1258
+
1259
+ @param ReportListView view
1260
+
1261
+ <!-- Breadcrumb -->
1262
+ <div class="mb-6">
1263
+ <nav class="flex" aria-label="Breadcrumb">
1264
+ <ol class="inline-flex items-center space-x-1">
1265
+ <li><a href="/home" hx-get="/home/fragments/content" hx-target="#content-area"
1266
+ class="text-gray-500 hover:text-gray-700">Home</a></li>
1267
+ <li class="text-gray-400">/</li>
1268
+ <li class="text-gray-900 font-medium">Reports</li>
1269
+ </ol>
1270
+ </nav>
1271
+ </div>
1272
+
1273
+ <h1 class="text-2xl font-bold mb-6">Reports (${view.getTotalReports()})</h1>
1274
+
1275
+ @for(var entry : view.getReportsByDomain().entrySet())
1276
+ <div class="mb-8">
1277
+ <h2 class="text-lg font-semibold text-gray-700 mb-4">${entry.getKey()}</h2>
1278
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
1279
+ @for(ReportEntry report : entry.getValue())
1280
+ <div class="bg-white rounded-lg shadow p-6 hover:shadow-md transition-shadow">
1281
+ <h3 class="text-base font-semibold text-gray-900 mb-2">${report.name()}</h3>
1282
+ <p class="text-sm text-gray-600 mb-4">${report.description()}</p>
1283
+ <a href="/reports/${report.reportId()}"
1284
+ hx-get="/reports/fragments/${report.reportId()}"
1285
+ hx-target="#content-area"
1286
+ class="inline-flex items-center text-sm font-medium text-indigo-600 hover:text-indigo-800">
1287
+ Generate Report →
1288
+ </a>
1289
+ </div>
1290
+ @endfor
1291
+ </div>
1292
+ </div>
1293
+ @endfor
1294
+ ```
1295
+
1296
+ ### Report Parameter Form Page
1297
+
1298
+ ```html
1299
+ @import {{BASE_PACKAGE}}.shared.reporting.view.ReportGenerateView
1300
+ @import {{BASE_PACKAGE}}.shared.reporting.ReportParameter
1301
+
1302
+ @param ReportGenerateView view
1303
+
1304
+ <!-- Breadcrumb -->
1305
+ <div class="mb-6">
1306
+ <nav class="flex" aria-label="Breadcrumb">
1307
+ <ol class="inline-flex items-center space-x-1">
1308
+ <li><a href="/home" hx-get="/home/fragments/content" hx-target="#content-area"
1309
+ class="text-gray-500 hover:text-gray-700">Home</a></li>
1310
+ <li class="text-gray-400">/</li>
1311
+ <li><a href="/reports" hx-get="/reports/fragments/list" hx-target="#content-area"
1312
+ class="text-gray-500 hover:text-gray-700">Reports</a></li>
1313
+ <li class="text-gray-400">/</li>
1314
+ <li class="text-gray-900 font-medium">${view.getName()}</li>
1315
+ </ol>
1316
+ </nav>
1317
+ </div>
1318
+
1319
+ <div class="max-w-2xl">
1320
+ <h1 class="text-2xl font-bold mb-2">${view.getName()}</h1>
1321
+ <p class="text-gray-600 mb-6">${view.getDescription()}</p>
1322
+
1323
+ <form action="/reports/${view.getReportId()}/generate" method="post"
1324
+ class="bg-white rounded-lg shadow p-6 space-y-6">
1325
+
1326
+ @for(ReportParameter param : view.getParameters())
1327
+ <div>
1328
+ <label for="${param.name()}"
1329
+ class="block text-sm font-medium text-gray-700 mb-1">
1330
+ ${param.label()}
1331
+ @if(param.required())
1332
+ <span class="text-red-500">*</span>
1333
+ @endif
1334
+ </label>
1335
+
1336
+ @if(param.type() == ReportParameter.ParameterType.TEXT)
1337
+ <input type="text" id="${param.name()}" name="${param.name()}"
1338
+ value="${param.defaultValue() != null ? param.defaultValue() : ""}"
1339
+ class="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
1340
+ ${ param.required() ? "required" : "" }>
1341
+ @elseif(param.type() == ReportParameter.ParameterType.DATE)
1342
+ <input type="date" id="${param.name()}" name="${param.name()}"
1343
+ value="${param.defaultValue() != null ? param.defaultValue() : ""}"
1344
+ class="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
1345
+ ${ param.required() ? "required" : "" }>
1346
+ @elseif(param.type() == ReportParameter.ParameterType.NUMBER)
1347
+ <input type="number" id="${param.name()}" name="${param.name()}"
1348
+ value="${param.defaultValue() != null ? param.defaultValue() : ""}"
1349
+ class="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
1350
+ ${ param.required() ? "required" : "" }>
1351
+ @elseif(param.type() == ReportParameter.ParameterType.SELECT)
1352
+ <select id="${param.name()}" name="${param.name()}"
1353
+ class="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
1354
+ ${ param.required() ? "required" : "" }>
1355
+ <option value="">-- Select --</option>
1356
+ @for(var option : param.options())
1357
+ <option value="${option.value()}">${option.label()}</option>
1358
+ @endfor
1359
+ </select>
1360
+ @elseif(param.type() == ReportParameter.ParameterType.BOOLEAN)
1361
+ <div class="flex items-center" x-data="{ checked: ${param.defaultValue() != null ? param.defaultValue() : "false"} }">
1362
+ <input type="hidden" name="${param.name()}" :value="checked">
1363
+ <button type="button" @click="checked = !checked"
1364
+ :class="checked ? 'bg-indigo-600' : 'bg-gray-200'"
1365
+ class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors">
1366
+ <span :class="checked ? 'translate-x-6' : 'translate-x-1'"
1367
+ class="inline-block h-4 w-4 rounded-full bg-white transition-transform"></span>
1368
+ </button>
1369
+ </div>
1370
+ @endif
1371
+ </div>
1372
+ @endfor
1373
+
1374
+ <!-- Export Format Selection -->
1375
+ <div>
1376
+ <label class="block text-sm font-medium text-gray-700 mb-1">Export Format</label>
1377
+ <div class="flex space-x-4">
1378
+ <label class="inline-flex items-center">
1379
+ <input type="radio" name="format" value="PDF" checked
1380
+ class="text-indigo-600 focus:ring-indigo-500">
1381
+ <span class="ml-2 text-sm text-gray-700">PDF</span>
1382
+ </label>
1383
+ <label class="inline-flex items-center">
1384
+ <input type="radio" name="format" value="XLSX"
1385
+ class="text-indigo-600 focus:ring-indigo-500">
1386
+ <span class="ml-2 text-sm text-gray-700">Excel (XLSX)</span>
1387
+ </label>
1388
+ <label class="inline-flex items-center">
1389
+ <input type="radio" name="format" value="CSV"
1390
+ class="text-indigo-600 focus:ring-indigo-500">
1391
+ <span class="ml-2 text-sm text-gray-700">CSV</span>
1392
+ </label>
1393
+ </div>
1394
+ </div>
1395
+
1396
+ <!-- Action Buttons -->
1397
+ <div class="flex justify-end space-x-3 pt-4 border-t">
1398
+ <a href="/reports"
1399
+ hx-get="/reports/fragments/list"
1400
+ hx-target="#content-area"
1401
+ class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200">
1402
+ Cancel
1403
+ </a>
1404
+ <button type="submit"
1405
+ class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
1406
+ Generate Report
1407
+ </button>
1408
+ </div>
1409
+ </form>
1410
+ </div>
1411
+ ```
1412
+
1413
+ ---
1414
+
1415
+ ## Module Integration — Sample Report Implementation
1416
+
1417
+ Each module creates `@Component` classes implementing `ReportDefinition` to register
1418
+ their reports. The implementation calls module services (never repositories directly) to
1419
+ produce DTO collections, and builds the report layout programmatically using the
1420
+ JRDesign API (with help from `ReportDesignHelper`).
1421
+
1422
+ ### Sample: Staff Allocation Summary Report
1423
+
1424
+ ```java
1425
+ package {{BASE_PACKAGE}}.staff.internal;
1426
+
1427
+ import {{BASE_PACKAGE}}.shared.reporting.ReportDefinition;
1428
+ import {{BASE_PACKAGE}}.shared.reporting.ReportDesignHelper;
1429
+ import {{BASE_PACKAGE}}.shared.reporting.ReportDesignHelper.ColumnDef;
1430
+ import {{BASE_PACKAGE}}.shared.reporting.ReportGenerationException;
1431
+ import {{BASE_PACKAGE}}.shared.reporting.ReportParameter;
1432
+ import {{BASE_PACKAGE}}.staff.StaffService;
1433
+ import lombok.RequiredArgsConstructor;
1434
+ import net.sf.jasperreports.engine.design.JasperDesign;
1435
+ import org.springframework.stereotype.Component;
1436
+
1437
+ import java.util.Collection;
1438
+ import java.util.List;
1439
+ import java.util.Map;
1440
+
1441
+ /**
1442
+ * Staff Allocation Summary report — lists all staff with their current
1443
+ * allocation status, department, and position.
1444
+ *
1445
+ * Data source: StaffReportDTO (flat DTO projected from Staff entity/document).
1446
+ * Layout: built programmatically via JRDesign API — no .jrxml template.
1447
+ */
1448
+ @Component
1449
+ @RequiredArgsConstructor
1450
+ public class StaffAllocationSummaryReport implements ReportDefinition {
1451
+
1452
+ private final StaffService staffService;
1453
+
1454
+ @Override
1455
+ public String getReportId() {
1456
+ return "staff-allocation-summary";
1457
+ }
1458
+
1459
+ @Override
1460
+ public String getName() {
1461
+ return "Staff Allocation Summary";
1462
+ }
1463
+
1464
+ @Override
1465
+ public String getDescription() {
1466
+ return "Lists all staff members with their department, position, and current allocation status.";
1467
+ }
1468
+
1469
+ @Override
1470
+ public String getDomain() {
1471
+ return "Staff";
1472
+ }
1473
+
1474
+ @Override
1475
+ public List<ReportParameter> getParameters() {
1476
+ return List.of(
1477
+ ReportParameter.select("departmentId", "Department", false,
1478
+ List.of(
1479
+ new ReportParameter.Option("All Departments", ""),
1480
+ new ReportParameter.Option("Software Engineering", "dept-se"),
1481
+ new ReportParameter.Option("Quality Assurance", "dept-qa")
1482
+ )),
1483
+ ReportParameter.select("allocationStatus", "Allocation Status", false,
1484
+ List.of(
1485
+ new ReportParameter.Option("All", ""),
1486
+ new ReportParameter.Option("Available", "AVAILABLE"),
1487
+ new ReportParameter.Option("Allocated", "ALLOCATED")
1488
+ ))
1489
+ );
1490
+ }
1491
+
1492
+ @Override
1493
+ public JasperDesign buildDesign(Map<String, Object> parameters) {
1494
+ try {
1495
+ // 1. Create landscape A4 page
1496
+ JasperDesign design = ReportDesignHelper.createA4LandscapeDesign("staff-allocation-summary");
1497
+
1498
+ // 2. Declare fields — names must match StaffReportDTO getter names
1499
+ ReportDesignHelper.addField(design, "staffNumber", String.class);
1500
+ ReportDesignHelper.addField(design, "fullName", String.class);
1501
+ ReportDesignHelper.addField(design, "email", String.class);
1502
+ ReportDesignHelper.addField(design, "departmentName", String.class);
1503
+ ReportDesignHelper.addField(design, "positionName", String.class);
1504
+ ReportDesignHelper.addField(design, "allocationStatus", String.class);
1505
+ ReportDesignHelper.addField(design, "allocationPercentage", String.class);
1506
+
1507
+ // 3. Define column layout
1508
+ List<ColumnDef> columns = List.of(
1509
+ new ColumnDef("Staff No.", "staffNumber", 80),
1510
+ new ColumnDef("Full Name", "fullName", 150),
1511
+ new ColumnDef("Email", "email", 170),
1512
+ new ColumnDef("Department", "departmentName", 130),
1513
+ new ColumnDef("Position", "positionName", 100),
1514
+ new ColumnDef("Status", "allocationStatus", 80),
1515
+ new ColumnDef("Allocation %", "allocationPercentage", 92)
1516
+ );
1517
+
1518
+ // 4. Build title band
1519
+ design.setTitle(ReportDesignHelper.createTitleBand(
1520
+ "Staff Allocation Summary", 50, design.getColumnWidth()));
1521
+
1522
+ // 5. Build column header band
1523
+ design.setColumnHeader(ReportDesignHelper.createColumnHeaderBand(columns));
1524
+
1525
+ // 6. Build detail band
1526
+ design.setDetail(ReportDesignHelper.createDetailBand(columns));
1527
+
1528
+ // 7. Build page footer with "Page X of Y"
1529
+ design.setPageFooter(ReportDesignHelper.createPageFooterBand(design.getColumnWidth()));
1530
+
1531
+ return design;
1532
+ } catch (Exception e) {
1533
+ throw new ReportGenerationException(
1534
+ "Failed to build design for report: staff-allocation-summary", e);
1535
+ }
1536
+ }
1537
+
1538
+ @Override
1539
+ public Collection<?> generateData(Map<String, Object> parameters) {
1540
+ String departmentId = (String) parameters.getOrDefault("departmentId", "");
1541
+ String allocationStatus = (String) parameters.getOrDefault("allocationStatus", "");
1542
+
1543
+ // Call domain service to get filtered data as DTOs
1544
+ // The service returns List<StaffReportDTO> — flat DTO suitable for Jasper
1545
+ return staffService.getStaffForReport(departmentId, allocationStatus);
1546
+ }
1547
+ }
1548
+ ```
1549
+
1550
+ ---
1551
+
1552
+ ## Sample Report DTO
1553
+
1554
+ ```java
1555
+ package {{BASE_PACKAGE}}.staff.internal;
1556
+
1557
+ import lombok.Getter;
1558
+ import lombok.Setter;
1559
+
1560
+ /**
1561
+ * Flat DTO used as the data source bean for JasperReports.
1562
+ * Each field maps to a JRDesignField declared in the buildDesign() method.
1563
+ * Field names must match exactly between this DTO's getter names and the
1564
+ * JRDesignField names declared via ReportDesignHelper.addField().
1565
+ */
1566
+ @Getter
1567
+ @Setter
1568
+ public class StaffReportDTO {
1569
+
1570
+ private String staffNumber;
1571
+ private String fullName;
1572
+ private String email;
1573
+ private String departmentName;
1574
+ private String positionName;
1575
+ private String allocationStatus;
1576
+ private String allocationPercentage;
1577
+ }
1578
+ ```
1579
+
1580
+ ---
1581
+
1582
+ ## Key Design Decisions
1583
+
1584
+ ### Why Programmatic Design (JRDesign API)?
1585
+
1586
+ 1. **No XML files to maintain** — the report layout is Java code, versioned with the
1587
+ application in the same repository. There are no `.jrxml` files to keep in sync.
1588
+ 2. **Fully AI-agent-friendly** — coding agents write Java, not XML. The JRDesign API
1589
+ produces the same result as `.jrxml` but is expressed as regular Java method calls.
1590
+ 3. **Compile-time safety** — typos in field names, incorrect types, and missing elements
1591
+ surface as Java compilation errors or obvious runtime exceptions in the `buildDesign()`
1592
+ method, not as opaque Jasper XML parsing failures.
1593
+ 4. **Dynamic layouts** — report structure can adapt based on parameters or data. For
1594
+ example, columns can be conditionally included or excluded based on user selections
1595
+ without maintaining multiple template variants.
1596
+ 5. **No template/code synchronization** — the design and the data source DTO live in the
1597
+ same codebase. When a DTO field is renamed, the `buildDesign()` method is updated in
1598
+ the same commit. There is no separate XML file that can drift out of sync.
1599
+
1600
+ ### Why DTO as Data Source (not direct DB query)?
1601
+
1602
+ 1. **Separation of concerns**: Report definitions call module services, not repositories.
1603
+ This preserves the module boundary enforced by Spring Modulith.
1604
+ 2. **Testability**: Report data generation can be unit-tested by mocking the module service.
1605
+ 3. **Flexibility**: The DTO can aggregate data from multiple sources (entities, computed
1606
+ fields, cross-module lookups via public APIs) before feeding it to Jasper.
1607
+ 4. **Type safety**: `JRBeanCollectionDataSource` reads Java getter methods, so field
1608
+ mismatches between the DTO and `JRDesignField` declarations are caught early.
1609
+ 5. **No SQL in reports**: Jasper reports contain only layout logic — no `<queryString>` SQL.
1610
+ All data access goes through the standard service layer.
1611
+
1612
+ ### Report DTO Field Naming Convention
1613
+
1614
+ - DTO field names use **camelCase** (Java standard)
1615
+ - `JRDesignField` names added via `ReportDesignHelper.addField()` must match the DTO
1616
+ getter name exactly (e.g., field `staffNumber` maps to getter `getStaffNumber()`)
1617
+ - For nested objects, flatten them into the report DTO
1618
+ (e.g., `staff.department.name` becomes `departmentName` in the DTO)
1619
+
1620
+ ---
1621
+
1622
+ ## Operational Notes
1623
+
1624
+ - **Memory**: Large reports can consume significant memory during filling and export.
1625
+ Consider streaming exports for very large data sets (JasperReports supports
1626
+ `JRVirtualizer` for swapping pages to disk).
1627
+ - **Concurrency**: The `ReportService` is thread-safe. Compiled `JasperReport` objects are
1628
+ cached in a `ConcurrentHashMap`. Each `generate()` call creates its own `JasperPrint`.
1629
+ - **Timeouts**: For long-running reports, consider executing generation asynchronously
1630
+ via `@Async` or Spring Modulith events, with the result stored temporarily for download.
1631
+ - **Font embedding**: For PDF export, ensure fonts are embedded via the
1632
+ `jasperreports-fonts` dependency. Configure `jasperreports.properties` or
1633
+ `jasperreports_extension.properties` for custom font families if needed.
1634
+ - **Cache invalidation**: After deploying updated `buildDesign()` logic, call
1635
+ `ReportService.evictReport(reportId)` to force recompilation from the new design.
1636
+ In practice, a fresh deployment restarts the JVM and clears the in-memory cache
1637
+ automatically. The `evictReport()` method is primarily useful during development
1638
+ or in hot-reload scenarios.