@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.
- package/dist/index.js +25449 -0
- package/drizzle/0000_remarkable_charles_xavier.sql +210 -0
- package/drizzle/0001_wise_wolf_cub.sql +13 -0
- package/drizzle/0002_secret_spyke.sql +16 -0
- package/drizzle/0003_workspace_config_singleton.sql +16 -0
- package/drizzle/0004_terminal_claim_v120.sql +11 -0
- package/drizzle/0005_brainstorm_v124.sql +10 -0
- package/drizzle/0006_project_v140.sql +6 -0
- package/drizzle/0007_prd_wizard_v124.sql +14 -0
- package/drizzle/0008_workflow_artifact_v126.sql +24 -0
- package/drizzle/0009_project_import_v150.sql +7 -0
- package/drizzle/0010_quality_v151.sql +3 -0
- package/drizzle/meta/0000_snapshot.json +1335 -0
- package/drizzle/meta/0001_snapshot.json +1416 -0
- package/drizzle/meta/0002_snapshot.json +1510 -0
- package/drizzle/meta/_journal.json +83 -0
- package/package.json +41 -0
- package/plugin/.claude-plugin/marketplace.json +36 -0
- package/plugin/.claude-plugin/plugin.json +22 -0
- package/plugin/README.md +625 -0
- package/plugin/SKILLS.md +350 -0
- package/plugin/co2.svg +5 -0
- package/plugin/commands/brainstorm-loop.md +129 -0
- package/plugin/skills/conductor-defect/SKILL.md +898 -0
- package/plugin/skills/conductor-feature-develop/SKILL.md +1383 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-domain-template.md +131 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-master-template.md +89 -0
- package/plugin/skills/conductor-feature-develop/references/playwright-setup.md +224 -0
- package/plugin/skills/conductor-feature-prepare/SKILL.md +604 -0
- package/plugin/skills/depgen-k8s/SKILL.md +468 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-laravel.md +197 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-nodejs.md +163 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-spring-boot.md +174 -0
- package/plugin/skills/depgen-k8s/references/k8s-patterns.md +869 -0
- package/plugin/skills/mockgen-shadcn/SKILL.md +1067 -0
- package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +1467 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-index-template.md +216 -0
- package/plugin/skills/mockgen-tailwind/SKILL.md +904 -0
- package/plugin/skills/mockgen-tailwind/references/admin-layout-template.md +720 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-index-template.md +190 -0
- package/plugin/skills/modelgen-nosql/SKILL.md +617 -0
- package/plugin/skills/modelgen-nosql/references/model-extraction-methodology.md +497 -0
- package/plugin/skills/modelgen-nosql/references/nosql-design-guide.md +258 -0
- package/plugin/skills/modelgen-relational/SKILL.md +707 -0
- package/plugin/skills/modelgen-relational/references/model-extraction-methodology.md +499 -0
- package/plugin/skills/specgen-flutter-riverpod/SKILL.md +898 -0
- package/plugin/skills/specgen-flutter-riverpod/references/component-patterns.md +971 -0
- package/plugin/skills/specgen-flutter-riverpod/references/notification-patterns.md +457 -0
- package/plugin/skills/specgen-flutter-riverpod/references/routing-patterns.md +362 -0
- package/plugin/skills/specgen-flutter-riverpod/references/security-patterns.md +709 -0
- package/plugin/skills/specgen-flutter-riverpod/references/spec-template.md +1566 -0
- package/plugin/skills/specgen-flutter-riverpod/references/state-patterns.md +518 -0
- package/plugin/skills/specgen-flutter-riverpod/references/storage-patterns.md +370 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/SKILL.md +984 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/batch-patterns.md +359 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/messaging-patterns.md +468 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/modulith-patterns.md +461 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/reporting-patterns.md +803 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/security-patterns.md +501 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/spec-template.md +1268 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/web-patterns.md +301 -0
- package/plugin/skills/specgen-node-cli-web/SKILL.md +561 -0
- package/plugin/skills/specgen-node-cli-web/references/auth-patterns.md +293 -0
- package/plugin/skills/specgen-node-cli-web/references/cli-packaging-patterns.md +250 -0
- package/plugin/skills/specgen-node-cli-web/references/server-patterns.md +247 -0
- package/plugin/skills/specgen-node-cli-web/references/spec-template.md +401 -0
- package/plugin/skills/specgen-node-cli-web/references/testing-patterns.md +174 -0
- package/plugin/skills/specgen-react-mui/SKILL.md +789 -0
- package/plugin/skills/specgen-react-mui/references/component-patterns.md +1077 -0
- package/plugin/skills/specgen-react-mui/references/reporting-patterns.md +1998 -0
- package/plugin/skills/specgen-react-mui/references/routing-patterns.md +461 -0
- package/plugin/skills/specgen-react-mui/references/security-patterns.md +599 -0
- package/plugin/skills/specgen-react-mui/references/spec-template.md +1030 -0
- package/plugin/skills/specgen-react-mui/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-react-tailwind/SKILL.md +859 -0
- package/plugin/skills/specgen-react-tailwind/references/component-patterns.md +1212 -0
- package/plugin/skills/specgen-react-tailwind/references/reporting-patterns.md +1529 -0
- package/plugin/skills/specgen-react-tailwind/references/routing-patterns.md +669 -0
- package/plugin/skills/specgen-react-tailwind/references/security-patterns.md +656 -0
- package/plugin/skills/specgen-react-tailwind/references/spec-template.md +1237 -0
- package/plugin/skills/specgen-react-tailwind/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-sdk-java/SKILL.md +933 -0
- package/plugin/skills/specgen-sdk-java/references/api-extraction-patterns.md +247 -0
- package/plugin/skills/specgen-sdk-java/references/http-client-patterns.md +495 -0
- package/plugin/skills/specgen-sdk-java/references/packaging-patterns.md +366 -0
- package/plugin/skills/specgen-sdk-java/references/spec-template.md +956 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/SKILL.md +1284 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/jasper-patterns.md +1638 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/modulith-patterns.md +354 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/security-patterns.md +551 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/spec-template.md +2502 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/web-patterns.md +503 -0
- package/plugin/skills/specgen-spring-jpa-restapi/SKILL.md +990 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/modulith-patterns.md +279 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/reporting-patterns.md +1536 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/restapi-patterns.md +722 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/security-patterns.md +351 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/spec-template.md +553 -0
- package/plugin/skills/specgen-ts-cli/SKILL.md +861 -0
- package/plugin/skills/specgen-ts-cli/references/async-patterns.md +1101 -0
- package/plugin/skills/specgen-ts-cli/references/command-patterns.md +383 -0
- package/plugin/skills/specgen-ts-cli/references/config-patterns.md +430 -0
- package/plugin/skills/specgen-ts-cli/references/database-patterns.md +750 -0
- package/plugin/skills/specgen-ts-cli/references/packaging-patterns.md +426 -0
- package/plugin/skills/specgen-ts-cli/references/spec-template.md +1391 -0
- package/plugin/skills/specgen-ts-cli/references/testing-patterns.md +909 -0
- package/plugin/skills/testgen-functional/SKILL.md +1381 -0
- package/plugin/skills/tracegen-matrix/SKILL.md +243 -0
- package/plugin/skills/util-preparek8senv/SKILL.md +422 -0
- package/plugin/skills/util-projectinit/SKILL.md +307 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-loop.md +129 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-protocol.md +144 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-setup-guide.md +153 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-template-appdev.md +87 -0
- package/plugin/skills/util-projectinit/reference/co2-context-generation-guide.md +761 -0
- package/plugin/skills/util-projectsync/SKILL.md +501 -0
- package/plugin/skills/util-updprd/SKILL.md +324 -0
- package/plugin/skills/util-usanalyzer/SKILL.md +307 -0
- package/plugin/skills/util-ustagger/SKILL.md +154 -0
- package/static/assets/abnfDiagram-VRR7QNED-DvPuwyCV.js +1 -0
- package/static/assets/arc-DRKw7sjs.js +1 -0
- package/static/assets/architectureDiagram-ZJ3FMSHR-CaNcxUbC.js +36 -0
- package/static/assets/blockDiagram-677ZJIJ3-Demd4AOK.js +132 -0
- package/static/assets/c4Diagram-LMCZKHZV-PVKUfc2x.js +10 -0
- package/static/assets/channel-C2nVbuwv.js +1 -0
- package/static/assets/chunk-2Q5K7J3B-CExf8ww_.js +1 -0
- package/static/assets/chunk-32BRIVSS-CFPaoONF.js +1 -0
- package/static/assets/chunk-5VM5RSS4-DE2esM67.js +15 -0
- package/static/assets/chunk-EX3LRPZG-SON_YdDG.js +231 -0
- package/static/assets/chunk-JWPE2WC7-Qxah8LNr.js +1 -0
- package/static/assets/chunk-MOJQB5TN-BcWGQV3j.js +88 -0
- package/static/assets/chunk-RYQCIY6F-DkDj-OR5.js +1 -0
- package/static/assets/chunk-V7JOEXUC-BlI1Mrah.js +206 -0
- package/static/assets/chunk-VR4S4FIN-Agf__duj.js +1 -0
- package/static/assets/chunk-XXDRQBXY-B5CfAh85.js +1 -0
- package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +1 -0
- package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +1 -0
- package/static/assets/cose-bilkent-JH36ORCC-DO0cQkcy.js +1 -0
- package/static/assets/cynefin-VYW2F7L2-CfOnsnXc.js +178 -0
- package/static/assets/cynefinDiagram-TSTJHNR4-BJ8tULAv.js +62 -0
- package/static/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
- package/static/assets/dagre-VKFMJZFB-DfjwzHqP.js +4 -0
- package/static/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/static/assets/diagram-FQU43EPY-CndSOIIM.js +3 -0
- package/static/assets/diagram-G47NLZAW-Czf5hN0Z.js +24 -0
- package/static/assets/diagram-NH7WQ7WH-COAyojGh.js +24 -0
- package/static/assets/diagram-OA4YK3LP-H8PoiN2m.js +30 -0
- package/static/assets/diagram-WEI45ONY-DzcaVzjp.js +41 -0
- package/static/assets/ebnfDiagram-CCIWWBDH-DPazGYLJ.js +1 -0
- package/static/assets/erDiagram-Q63AITRT-Bo-fubgF.js +85 -0
- package/static/assets/flowDiagram-23GEKE2U-CSvxUDwh.js +156 -0
- package/static/assets/ganttDiagram-NO4QXBWP-DnBveWX6.js +292 -0
- package/static/assets/gitGraphDiagram-IHSO6WYX-B3rVL8c-.js +106 -0
- package/static/assets/graph-C9eacEi8.js +1 -0
- package/static/assets/index-LZSQkLE2.css +41 -0
- package/static/assets/index-y7XDSgMG.js +495 -0
- package/static/assets/infoDiagram-FWYZ7A6U-CvWV9yy2.js +2 -0
- package/static/assets/init-Gi6I4Gst.js +1 -0
- package/static/assets/ishikawaDiagram-FXEZZL3T-DF28a1N6.js +70 -0
- package/static/assets/journeyDiagram-5HDEW3XC-CB6CugYk.js +139 -0
- package/static/assets/kanban-definition-HUTT4EX6-ResSLF6d.js +89 -0
- package/static/assets/katex-C5jXJg4s.js +257 -0
- package/static/assets/layout-DEXfKzaS.js +1 -0
- package/static/assets/linear-CLO--ryk.js +1 -0
- package/static/assets/map-Czzmt4hB.js +1 -0
- package/static/assets/mindmap-definition-LN4V7U3C-BvW3EbxX.js +96 -0
- package/static/assets/ordinal-Cboi1Yqb.js +1 -0
- package/static/assets/pegDiagram-2B236MQR-BHCgqrrM.js +1 -0
- package/static/assets/pieDiagram-ENE6RG2P-DVmCQ5FA.js +39 -0
- package/static/assets/quadrantDiagram-ABIIQ3AL-B3e5KR52.js +7 -0
- package/static/assets/railroadDiagram-RFXS5EU6-Csv5ysLc.js +1 -0
- package/static/assets/requirementDiagram-TGXJPOKE-DOMVzvnB.js +84 -0
- package/static/assets/sankeyDiagram-HTMAVEWB-B1dcTOvy.js +40 -0
- package/static/assets/sequenceDiagram-DBY2YBRQ-BY414yeO.js +162 -0
- package/static/assets/sizeCapture-X5ZJPWSS-CuJfrfBN.js +1 -0
- package/static/assets/stateDiagram-2N3HPSRC-FX4rJkzV.js +1 -0
- package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +1 -0
- package/static/assets/swimlanes-5IMT3BWC-Sfy58mmv.js +2 -0
- package/static/assets/swimlanesDiagram-G3AALYLV-wDCDw08H.js +8 -0
- package/static/assets/timeline-definition-FHXFAJF6-CmV7by8p.js +120 -0
- package/static/assets/vennDiagram-L72KCM5P-FqIyFZFt.js +34 -0
- package/static/assets/wardleyDiagram-EHGQE667-BZ06O45i.js +78 -0
- package/static/assets/xychartDiagram-FW5EYKEG-BOkffR06.js +7 -0
- 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.
|