@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,803 @@
|
|
|
1
|
+
# Reporting Patterns — Puppeteer (Browsershot) + Laravel Excel with DTO Data Source
|
|
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 use statements and constructor injection.
|
|
6
|
+
|
|
7
|
+
The reporting feature provides:
|
|
8
|
+
- **Report Interface** for modules to define and register their own reports
|
|
9
|
+
- **Puppeteer via Browsershot** for PDF generation from Blade templates — uses headless Chrome for full CSS support including Tailwind CSS, flexbox, and grid
|
|
10
|
+
- **Laravel Excel** for XLSX and CSV export
|
|
11
|
+
- **DTO-based data source** — reports receive data via DTOs from services, never query DB directly
|
|
12
|
+
- **Multi-format export** — PDF, XLSX, CSV
|
|
13
|
+
- **Report registry** persisted in the database for discoverability
|
|
14
|
+
- **Report parameter forms** rendered via Blade for user input before generation
|
|
15
|
+
- **Blade templates with Tailwind CSS** — PDF templates use the same design system as the web application since Puppeteer renders full CSS
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Architecture Overview
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
Module Shared Reporting Infrastructure
|
|
23
|
+
┌─────────────────────┐ ┌─────────────────────────────────────────┐
|
|
24
|
+
│ StaffReport │ │ ReportService │
|
|
25
|
+
│ implements │────────►│ renderPdf(Browsershot + Blade + data)│
|
|
26
|
+
│ ReportDefinition │ │ exportExcel(Laravel Excel + data) │
|
|
27
|
+
│ │ │ exportCsv(Laravel Excel + data) │
|
|
28
|
+
│ getReportId() │ │ │
|
|
29
|
+
│ getName() │ │ ReportRegistry │
|
|
30
|
+
│ getDescription() │ │ discovers tagged ReportDefinition │
|
|
31
|
+
│ getParameters() │ │ persists to reports table │
|
|
32
|
+
│ generateData(...) │ │ provides report lookup │
|
|
33
|
+
│ │ │ │
|
|
34
|
+
│ --- Data Flow --- │ │ ReportPageController │
|
|
35
|
+
│ Service → DTO[] │ │ list all available reports │
|
|
36
|
+
│ (never Repository) │ │ render parameter form │
|
|
37
|
+
└─────────────────────┘ │ generate + download report │
|
|
38
|
+
└─────────────────────────────────────────┘
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Key design rule:** Reports obtain data through module service interfaces (which return
|
|
42
|
+
DTOs), never by injecting repositories or querying the database directly. This preserves
|
|
43
|
+
module boundaries — reports are consumers of the module's public API.
|
|
44
|
+
|
|
45
|
+
**Rendering engine:** Browsershot renders Blade templates using headless Chrome (Puppeteer).
|
|
46
|
+
Templates are self-contained HTML documents that include Tailwind CSS via CDN, enabling
|
|
47
|
+
the same utility classes used in the web application. This eliminates the CSS limitations
|
|
48
|
+
of traditional PDF libraries (no flexbox, no grid, limited font support).
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Composer Dependencies
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"require": {
|
|
57
|
+
"spatie/browsershot": "^4.0",
|
|
58
|
+
"maatwebsite/excel": "^3.1"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Additionally, install the Puppeteer Node.js package on the server:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install puppeteer
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This installs both the Puppeteer library and a compatible headless Chrome binary.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Server Requirements
|
|
74
|
+
|
|
75
|
+
Browsershot requires Node.js and a Chrome/Chromium binary to render PDFs. The setup
|
|
76
|
+
depends on the deployment environment:
|
|
77
|
+
|
|
78
|
+
**Local development:**
|
|
79
|
+
- Install Node.js (v18+) and run `npm install puppeteer` in the project root.
|
|
80
|
+
- Puppeteer automatically downloads a compatible Chrome binary into `node_modules`.
|
|
81
|
+
- Browsershot auto-detects the Node.js binary and Chrome path from `node_modules`.
|
|
82
|
+
|
|
83
|
+
**Production / Docker:**
|
|
84
|
+
- The Dockerfile must include Node.js and either install Puppeteer (which bundles Chrome)
|
|
85
|
+
or install Chromium via the system package manager.
|
|
86
|
+
- When running Chrome inside Docker, the `--no-sandbox` flag is required (Browsershot
|
|
87
|
+
applies this automatically when it detects a non-TTY environment, but it can also be
|
|
88
|
+
set explicitly).
|
|
89
|
+
- Set the environment variable `PUPPETEER_EXECUTABLE_PATH` to use a system-installed
|
|
90
|
+
Chrome instead of the one bundled with Puppeteer.
|
|
91
|
+
|
|
92
|
+
**Environment variables (optional):**
|
|
93
|
+
- `BROWSERSHOT_NODE_BINARY` — absolute path to the Node.js binary
|
|
94
|
+
- `BROWSERSHOT_NPM_BINARY` — absolute path to the npm binary
|
|
95
|
+
- `BROWSERSHOT_CHROME_PATH` — absolute path to the Chrome/Chromium binary
|
|
96
|
+
- `BROWSERSHOT_NODE_MODULES_PATH` — absolute path to the `node_modules` directory
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Package Structure
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
app/
|
|
104
|
+
├── Contracts/
|
|
105
|
+
│ └── ReportDefinition.php # Interface for module reports
|
|
106
|
+
├── Services/
|
|
107
|
+
│ └── Reporting/
|
|
108
|
+
│ ├── ReportService.php # Orchestrates render/export via Browsershot
|
|
109
|
+
│ └── ReportRegistry.php # Discovers and persists report definitions
|
|
110
|
+
├── Models/
|
|
111
|
+
│ └── Report.php # Eloquent model for reports table
|
|
112
|
+
├── Http/
|
|
113
|
+
│ └── Controllers/
|
|
114
|
+
│ └── ReportController.php # Report list, form, download endpoints
|
|
115
|
+
├── config/
|
|
116
|
+
│ └── browsershot.php # Browsershot binary path configuration
|
|
117
|
+
└── resources/
|
|
118
|
+
└── views/
|
|
119
|
+
└── reports/
|
|
120
|
+
├── index.blade.php # Report list page
|
|
121
|
+
├── parameters.blade.php # Parameter form page
|
|
122
|
+
└── templates/ # PDF Blade templates (self-contained HTML)
|
|
123
|
+
└── sample-report.blade.php
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## ReportDefinition Interface
|
|
129
|
+
|
|
130
|
+
```php
|
|
131
|
+
namespace App\Contracts;
|
|
132
|
+
|
|
133
|
+
interface ReportDefinition
|
|
134
|
+
{
|
|
135
|
+
/**
|
|
136
|
+
* Unique identifier for this report (e.g., 'staff-list-report').
|
|
137
|
+
*/
|
|
138
|
+
public function getReportId(): string;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Human-readable display name.
|
|
142
|
+
*/
|
|
143
|
+
public function getName(): string;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Description shown in the report list.
|
|
147
|
+
*/
|
|
148
|
+
public function getDescription(): string;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Parameter definitions for the report form.
|
|
152
|
+
* Each parameter: ['name' => string, 'label' => string, 'type' => string, 'required' => bool]
|
|
153
|
+
*/
|
|
154
|
+
public function getParameters(): array;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Generate report data as an array of DTOs.
|
|
158
|
+
* Parameters are the user-provided form inputs.
|
|
159
|
+
*/
|
|
160
|
+
public function generateData(array $parameters): array;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Supported export formats: ['pdf', 'xlsx', 'csv']
|
|
164
|
+
*/
|
|
165
|
+
public function getSupportedFormats(): array;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Blade template path for PDF rendering (e.g., 'reports.templates.staff-list').
|
|
169
|
+
*/
|
|
170
|
+
public function getPdfTemplate(): string;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Column headers for XLSX/CSV export.
|
|
174
|
+
*/
|
|
175
|
+
public function getExportHeaders(): array;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Whether the PDF should be rendered in landscape orientation.
|
|
179
|
+
*/
|
|
180
|
+
public function isLandscape(): bool;
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Report Model (Eloquent)
|
|
187
|
+
|
|
188
|
+
```php
|
|
189
|
+
namespace App\Models;
|
|
190
|
+
|
|
191
|
+
use Illuminate\Database\Eloquent\Model;
|
|
192
|
+
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
|
193
|
+
|
|
194
|
+
class Report extends Model
|
|
195
|
+
{
|
|
196
|
+
use HasUuids;
|
|
197
|
+
|
|
198
|
+
protected $fillable = [
|
|
199
|
+
'report_id',
|
|
200
|
+
'name',
|
|
201
|
+
'description',
|
|
202
|
+
'parameters',
|
|
203
|
+
'supported_formats',
|
|
204
|
+
'module',
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
protected $casts = [
|
|
208
|
+
'parameters' => 'array',
|
|
209
|
+
'supported_formats' => 'array',
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Migration:
|
|
215
|
+
```php
|
|
216
|
+
Schema::create('reports', function (Blueprint $table) {
|
|
217
|
+
$table->uuid('id')->primary();
|
|
218
|
+
$table->string('report_id')->unique();
|
|
219
|
+
$table->string('name');
|
|
220
|
+
$table->text('description')->nullable();
|
|
221
|
+
$table->json('parameters')->nullable();
|
|
222
|
+
$table->json('supported_formats');
|
|
223
|
+
$table->string('module')->nullable();
|
|
224
|
+
$table->timestamps();
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## ReportRegistry
|
|
231
|
+
|
|
232
|
+
Auto-discovers all `ReportDefinition` implementations and syncs to the database:
|
|
233
|
+
|
|
234
|
+
```php
|
|
235
|
+
namespace App\Services\Reporting;
|
|
236
|
+
|
|
237
|
+
use App\Contracts\ReportDefinition;
|
|
238
|
+
use App\Models\Report;
|
|
239
|
+
|
|
240
|
+
class ReportRegistry
|
|
241
|
+
{
|
|
242
|
+
/** @var array<string, ReportDefinition> */
|
|
243
|
+
private array $definitions = [];
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Register a report definition (called from service providers).
|
|
247
|
+
*/
|
|
248
|
+
public function register(ReportDefinition $definition): void
|
|
249
|
+
{
|
|
250
|
+
$this->definitions[$definition->getReportId()] = $definition;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get a report definition by ID.
|
|
255
|
+
*/
|
|
256
|
+
public function get(string $reportId): ?ReportDefinition
|
|
257
|
+
{
|
|
258
|
+
return $this->definitions[$reportId] ?? null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Sync all registered definitions to the database.
|
|
263
|
+
*/
|
|
264
|
+
public function sync(): void
|
|
265
|
+
{
|
|
266
|
+
foreach ($this->definitions as $definition) {
|
|
267
|
+
Report::updateOrCreate(
|
|
268
|
+
['report_id' => $definition->getReportId()],
|
|
269
|
+
[
|
|
270
|
+
'name' => $definition->getName(),
|
|
271
|
+
'description' => $definition->getDescription(),
|
|
272
|
+
'parameters' => $definition->getParameters(),
|
|
273
|
+
'supported_formats' => $definition->getSupportedFormats(),
|
|
274
|
+
]
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Remove reports no longer registered
|
|
279
|
+
Report::whereNotIn('report_id', array_keys($this->definitions))->delete();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get all registered definitions.
|
|
284
|
+
*/
|
|
285
|
+
public function all(): array
|
|
286
|
+
{
|
|
287
|
+
return $this->definitions;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Register as singleton in `AppServiceProvider`:
|
|
293
|
+
```php
|
|
294
|
+
$this->app->singleton(ReportRegistry::class);
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Modules register their reports in their service provider:
|
|
298
|
+
```php
|
|
299
|
+
// In Modules/Staff/Providers/StaffServiceProvider.php
|
|
300
|
+
public function boot(): void
|
|
301
|
+
{
|
|
302
|
+
$registry = app(ReportRegistry::class);
|
|
303
|
+
$registry->register(app(StaffListReport::class));
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## ReportService
|
|
310
|
+
|
|
311
|
+
Orchestrates report generation across all formats. Uses Browsershot (Puppeteer) for PDF
|
|
312
|
+
rendering — the Blade template is rendered to HTML, then Browsershot converts it to PDF
|
|
313
|
+
using headless Chrome:
|
|
314
|
+
|
|
315
|
+
```php
|
|
316
|
+
namespace App\Services\Reporting;
|
|
317
|
+
|
|
318
|
+
use App\Contracts\ReportDefinition;
|
|
319
|
+
use Illuminate\Http\Response;
|
|
320
|
+
use Maatwebsite\Excel\Facades\Excel;
|
|
321
|
+
use Spatie\Browsershot\Browsershot;
|
|
322
|
+
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
323
|
+
|
|
324
|
+
class ReportService
|
|
325
|
+
{
|
|
326
|
+
public function __construct(
|
|
327
|
+
private readonly ReportRegistry $registry,
|
|
328
|
+
) {}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Generate and download a report in the specified format.
|
|
332
|
+
*/
|
|
333
|
+
public function generate(string $reportId, array $parameters, string $format): Response|BinaryFileResponse
|
|
334
|
+
{
|
|
335
|
+
$definition = $this->registry->get($reportId)
|
|
336
|
+
?? throw new \RuntimeException("Report not found: {$reportId}");
|
|
337
|
+
|
|
338
|
+
$data = $definition->generateData($parameters);
|
|
339
|
+
|
|
340
|
+
return match ($format) {
|
|
341
|
+
'pdf' => $this->generatePdf($definition, $data, $parameters),
|
|
342
|
+
'xlsx' => $this->generateExcel($definition, $data, 'xlsx'),
|
|
343
|
+
'csv' => $this->generateExcel($definition, $data, 'csv'),
|
|
344
|
+
default => throw new \InvalidArgumentException("Unsupported format: {$format}"),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private function generatePdf(ReportDefinition $definition, array $data, array $parameters): Response
|
|
349
|
+
{
|
|
350
|
+
$html = view($definition->getPdfTemplate(), [
|
|
351
|
+
'data' => $data,
|
|
352
|
+
'parameters' => $parameters,
|
|
353
|
+
'reportName' => $definition->getName(),
|
|
354
|
+
'generatedAt' => now(),
|
|
355
|
+
])->render();
|
|
356
|
+
|
|
357
|
+
$filename = $definition->getReportId() . '-' . now()->format('Y-m-d-His') . '.pdf';
|
|
358
|
+
|
|
359
|
+
$browsershot = Browsershot::html($html)
|
|
360
|
+
->format('A4')
|
|
361
|
+
->margins(10, 10, 10, 10)
|
|
362
|
+
->showBackground()
|
|
363
|
+
->waitUntilNetworkIdle();
|
|
364
|
+
|
|
365
|
+
if ($definition->isLandscape()) {
|
|
366
|
+
$browsershot->landscape();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
$this->applyBrowsershotConfig($browsershot);
|
|
370
|
+
|
|
371
|
+
$pdfContent = $browsershot->pdf();
|
|
372
|
+
|
|
373
|
+
return response($pdfContent)
|
|
374
|
+
->header('Content-Type', 'application/pdf')
|
|
375
|
+
->header('Content-Disposition', 'attachment; filename="' . $filename . '"');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private function generateExcel(ReportDefinition $definition, array $data, string $format): BinaryFileResponse
|
|
379
|
+
{
|
|
380
|
+
$export = new GenericReportExport($definition->getExportHeaders(), $data);
|
|
381
|
+
$extension = $format === 'csv' ? 'csv' : 'xlsx';
|
|
382
|
+
$filename = $definition->getReportId() . '-' . now()->format('Y-m-d-His') . '.' . $extension;
|
|
383
|
+
|
|
384
|
+
$writerType = $format === 'csv'
|
|
385
|
+
? \Maatwebsite\Excel\Excel::CSV
|
|
386
|
+
: \Maatwebsite\Excel\Excel::XLSX;
|
|
387
|
+
|
|
388
|
+
return Excel::download($export, $filename, $writerType);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Apply optional Browsershot configuration from config/browsershot.php.
|
|
393
|
+
*/
|
|
394
|
+
private function applyBrowsershotConfig(Browsershot $browsershot): void
|
|
395
|
+
{
|
|
396
|
+
$nodeBinary = config('browsershot.node_binary');
|
|
397
|
+
$npmBinary = config('browsershot.npm_binary');
|
|
398
|
+
$chromePath = config('browsershot.chrome_path');
|
|
399
|
+
$nodeModulesPath = config('browsershot.node_modules_path');
|
|
400
|
+
|
|
401
|
+
if ($nodeBinary) {
|
|
402
|
+
$browsershot->setNodeBinary($nodeBinary);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if ($npmBinary) {
|
|
406
|
+
$browsershot->setNpmBinary($npmBinary);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if ($chromePath) {
|
|
410
|
+
$browsershot->setChromePath($chromePath);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if ($nodeModulesPath) {
|
|
414
|
+
$browsershot->setNodeModulePath($nodeModulesPath);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (config('browsershot.no_sandbox', false)) {
|
|
418
|
+
$browsershot->noSandbox();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## Generic Report Export (Laravel Excel)
|
|
427
|
+
|
|
428
|
+
```php
|
|
429
|
+
namespace App\Services\Reporting;
|
|
430
|
+
|
|
431
|
+
use Maatwebsite\Excel\Concerns\FromArray;
|
|
432
|
+
use Maatwebsite\Excel\Concerns\WithHeadings;
|
|
433
|
+
use Maatwebsite\Excel\Concerns\WithStyles;
|
|
434
|
+
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
|
435
|
+
|
|
436
|
+
class GenericReportExport implements FromArray, WithHeadings, WithStyles
|
|
437
|
+
{
|
|
438
|
+
public function __construct(
|
|
439
|
+
private readonly array $headers,
|
|
440
|
+
private readonly array $data,
|
|
441
|
+
) {}
|
|
442
|
+
|
|
443
|
+
public function headings(): array
|
|
444
|
+
{
|
|
445
|
+
return $this->headers;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
public function array(): array
|
|
449
|
+
{
|
|
450
|
+
return array_map(function ($row) {
|
|
451
|
+
if (is_object($row) && method_exists($row, 'toArray')) {
|
|
452
|
+
return $row->toArray();
|
|
453
|
+
}
|
|
454
|
+
return (array) $row;
|
|
455
|
+
}, $this->data);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
public function styles(Worksheet $sheet): array
|
|
459
|
+
{
|
|
460
|
+
return [
|
|
461
|
+
1 => ['font' => ['bold' => true]],
|
|
462
|
+
];
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
## Report Controller
|
|
470
|
+
|
|
471
|
+
```php
|
|
472
|
+
namespace App\Http\Controllers;
|
|
473
|
+
|
|
474
|
+
use App\Models\Report;
|
|
475
|
+
use App\Services\Reporting\ReportRegistry;
|
|
476
|
+
use App\Services\Reporting\ReportService;
|
|
477
|
+
use Illuminate\Http\Request;
|
|
478
|
+
use Illuminate\View\View;
|
|
479
|
+
|
|
480
|
+
class ReportController extends Controller
|
|
481
|
+
{
|
|
482
|
+
public function __construct(
|
|
483
|
+
private readonly ReportService $reportService,
|
|
484
|
+
private readonly ReportRegistry $registry,
|
|
485
|
+
) {}
|
|
486
|
+
|
|
487
|
+
public function index(): View
|
|
488
|
+
{
|
|
489
|
+
$reports = Report::orderBy('name')->paginate(20);
|
|
490
|
+
return view('reports.index', compact('reports'));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
public function parameters(string $reportId): View
|
|
494
|
+
{
|
|
495
|
+
$definition = $this->registry->get($reportId)
|
|
496
|
+
?? abort(404, "Report not found: {$reportId}");
|
|
497
|
+
|
|
498
|
+
return view('reports.parameters', [
|
|
499
|
+
'report' => $definition,
|
|
500
|
+
'reportId' => $reportId,
|
|
501
|
+
]);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
public function generate(Request $request, string $reportId)
|
|
505
|
+
{
|
|
506
|
+
$definition = $this->registry->get($reportId)
|
|
507
|
+
?? abort(404, "Report not found: {$reportId}");
|
|
508
|
+
|
|
509
|
+
$validated = $request->validate(
|
|
510
|
+
collect($definition->getParameters())
|
|
511
|
+
->mapWithKeys(fn ($param) => [
|
|
512
|
+
$param['name'] => $param['required'] ? 'required' : 'nullable',
|
|
513
|
+
])
|
|
514
|
+
->toArray()
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
$format = $request->input('format', 'pdf');
|
|
518
|
+
|
|
519
|
+
return $this->reportService->generate($reportId, $validated, $format);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## Sample Module Report Implementation
|
|
527
|
+
|
|
528
|
+
```php
|
|
529
|
+
namespace Modules\Staff\Reports;
|
|
530
|
+
|
|
531
|
+
use App\Contracts\ReportDefinition;
|
|
532
|
+
use Modules\Staff\Contracts\StaffServiceInterface;
|
|
533
|
+
|
|
534
|
+
class StaffListReport implements ReportDefinition
|
|
535
|
+
{
|
|
536
|
+
public function __construct(
|
|
537
|
+
private readonly StaffServiceInterface $staffService,
|
|
538
|
+
) {}
|
|
539
|
+
|
|
540
|
+
public function getReportId(): string
|
|
541
|
+
{
|
|
542
|
+
return 'staff-list-report';
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
public function getName(): string
|
|
546
|
+
{
|
|
547
|
+
return 'Staff List Report';
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
public function getDescription(): string
|
|
551
|
+
{
|
|
552
|
+
return 'List all staff members with their department and status.';
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
public function getParameters(): array
|
|
556
|
+
{
|
|
557
|
+
return [
|
|
558
|
+
['name' => 'department', 'label' => 'Department', 'type' => 'select', 'required' => false],
|
|
559
|
+
['name' => 'status', 'label' => 'Status', 'type' => 'select', 'required' => false],
|
|
560
|
+
];
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
public function generateData(array $parameters): array
|
|
564
|
+
{
|
|
565
|
+
// Uses the service interface (public API), NOT the repository
|
|
566
|
+
return $this->staffService->getStaffForReport(
|
|
567
|
+
department: $parameters['department'] ?? null,
|
|
568
|
+
status: $parameters['status'] ?? null,
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
public function getSupportedFormats(): array
|
|
573
|
+
{
|
|
574
|
+
return ['pdf', 'xlsx', 'csv'];
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
public function getPdfTemplate(): string
|
|
578
|
+
{
|
|
579
|
+
return 'staff::reports.staff-list';
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
public function getExportHeaders(): array
|
|
583
|
+
{
|
|
584
|
+
return ['Name', 'Email', 'Department', 'Position', 'Status', 'Joined Date'];
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
public function isLandscape(): bool
|
|
588
|
+
{
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
## PDF Blade Template
|
|
597
|
+
|
|
598
|
+
PDF templates are self-contained HTML documents. Because Browsershot uses headless Chrome,
|
|
599
|
+
templates have full CSS support — Tailwind CSS classes work exactly as they do in the
|
|
600
|
+
browser. Include the Tailwind CSS CDN in the `<head>` so the template renders correctly
|
|
601
|
+
when Browsershot processes it outside the web application context.
|
|
602
|
+
|
|
603
|
+
```blade
|
|
604
|
+
{{-- Modules/Staff/resources/views/reports/staff-list.blade.php --}}
|
|
605
|
+
<!DOCTYPE html>
|
|
606
|
+
<html>
|
|
607
|
+
<head>
|
|
608
|
+
<meta charset="utf-8">
|
|
609
|
+
<title>{{ $reportName }}</title>
|
|
610
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
611
|
+
<style>
|
|
612
|
+
@page { margin: 0; }
|
|
613
|
+
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
614
|
+
</style>
|
|
615
|
+
</head>
|
|
616
|
+
<body class="bg-white text-gray-900 text-sm p-8">
|
|
617
|
+
{{-- Report Header --}}
|
|
618
|
+
<div class="text-center mb-6">
|
|
619
|
+
<h1 class="text-xl font-bold">{{ $reportName }}</h1>
|
|
620
|
+
<p class="text-xs text-gray-500">Generated: {{ $generatedAt->format('Y-m-d H:i:s') }}</p>
|
|
621
|
+
</div>
|
|
622
|
+
|
|
623
|
+
{{-- Report Parameters Summary --}}
|
|
624
|
+
@if (!empty(array_filter($parameters)))
|
|
625
|
+
<div class="mb-4 flex flex-wrap gap-3 text-xs text-gray-600">
|
|
626
|
+
@foreach ($parameters as $key => $value)
|
|
627
|
+
@if ($value)
|
|
628
|
+
<span class="bg-gray-100 rounded px-2 py-1">
|
|
629
|
+
<span class="font-medium">{{ ucfirst(str_replace('_', ' ', $key)) }}:</span>
|
|
630
|
+
{{ $value }}
|
|
631
|
+
</span>
|
|
632
|
+
@endif
|
|
633
|
+
@endforeach
|
|
634
|
+
</div>
|
|
635
|
+
@endif
|
|
636
|
+
|
|
637
|
+
{{-- Data Table --}}
|
|
638
|
+
<table class="w-full border-collapse">
|
|
639
|
+
<thead>
|
|
640
|
+
<tr class="bg-gray-100">
|
|
641
|
+
<th class="border border-gray-300 px-3 py-2 text-left font-semibold">Name</th>
|
|
642
|
+
<th class="border border-gray-300 px-3 py-2 text-left font-semibold">Email</th>
|
|
643
|
+
<th class="border border-gray-300 px-3 py-2 text-left font-semibold">Department</th>
|
|
644
|
+
<th class="border border-gray-300 px-3 py-2 text-left font-semibold">Position</th>
|
|
645
|
+
<th class="border border-gray-300 px-3 py-2 text-left font-semibold">Status</th>
|
|
646
|
+
<th class="border border-gray-300 px-3 py-2 text-left font-semibold">Joined Date</th>
|
|
647
|
+
</tr>
|
|
648
|
+
</thead>
|
|
649
|
+
<tbody>
|
|
650
|
+
@foreach ($data as $staff)
|
|
651
|
+
<tr class="even:bg-gray-50">
|
|
652
|
+
<td class="border border-gray-300 px-3 py-2">{{ $staff->name }}</td>
|
|
653
|
+
<td class="border border-gray-300 px-3 py-2">{{ $staff->email }}</td>
|
|
654
|
+
<td class="border border-gray-300 px-3 py-2">{{ $staff->department }}</td>
|
|
655
|
+
<td class="border border-gray-300 px-3 py-2">{{ $staff->position }}</td>
|
|
656
|
+
<td class="border border-gray-300 px-3 py-2">
|
|
657
|
+
<span class="inline-block rounded-full px-2 py-0.5 text-xs font-medium
|
|
658
|
+
{{ $staff->status === 'Active' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}">
|
|
659
|
+
{{ $staff->status }}
|
|
660
|
+
</span>
|
|
661
|
+
</td>
|
|
662
|
+
<td class="border border-gray-300 px-3 py-2">{{ $staff->joined_date }}</td>
|
|
663
|
+
</tr>
|
|
664
|
+
@endforeach
|
|
665
|
+
</tbody>
|
|
666
|
+
</table>
|
|
667
|
+
|
|
668
|
+
{{-- Footer --}}
|
|
669
|
+
<div class="flex justify-between items-center mt-6 text-xs text-gray-500">
|
|
670
|
+
<p>Total records: {{ count($data) }}</p>
|
|
671
|
+
<p>Page 1</p>
|
|
672
|
+
</div>
|
|
673
|
+
</body>
|
|
674
|
+
</html>
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
---
|
|
678
|
+
|
|
679
|
+
## Browsershot Configuration
|
|
680
|
+
|
|
681
|
+
Create a configuration file at `config/browsershot.php` to allow customizing binary paths
|
|
682
|
+
per environment. This is especially useful for Docker deployments where Node.js and Chrome
|
|
683
|
+
are installed at non-standard paths:
|
|
684
|
+
|
|
685
|
+
```php
|
|
686
|
+
<?php
|
|
687
|
+
|
|
688
|
+
return [
|
|
689
|
+
|
|
690
|
+
/*
|
|
691
|
+
|--------------------------------------------------------------------------
|
|
692
|
+
| Node.js Binary Path
|
|
693
|
+
|--------------------------------------------------------------------------
|
|
694
|
+
|
|
|
695
|
+
| Absolute path to the Node.js binary. When null, Browsershot auto-detects
|
|
696
|
+
| the Node.js binary from the system PATH.
|
|
697
|
+
|
|
|
698
|
+
*/
|
|
699
|
+
'node_binary' => env('BROWSERSHOT_NODE_BINARY', null),
|
|
700
|
+
|
|
701
|
+
/*
|
|
702
|
+
|--------------------------------------------------------------------------
|
|
703
|
+
| npm Binary Path
|
|
704
|
+
|--------------------------------------------------------------------------
|
|
705
|
+
|
|
|
706
|
+
| Absolute path to the npm binary. When null, Browsershot auto-detects
|
|
707
|
+
| the npm binary from the system PATH.
|
|
708
|
+
|
|
|
709
|
+
*/
|
|
710
|
+
'npm_binary' => env('BROWSERSHOT_NPM_BINARY', null),
|
|
711
|
+
|
|
712
|
+
/*
|
|
713
|
+
|--------------------------------------------------------------------------
|
|
714
|
+
| Chrome / Chromium Binary Path
|
|
715
|
+
|--------------------------------------------------------------------------
|
|
716
|
+
|
|
|
717
|
+
| Absolute path to the Chrome or Chromium binary. When null, Browsershot
|
|
718
|
+
| uses the Chrome binary bundled with Puppeteer in node_modules.
|
|
719
|
+
|
|
|
720
|
+
*/
|
|
721
|
+
'chrome_path' => env('BROWSERSHOT_CHROME_PATH', null),
|
|
722
|
+
|
|
723
|
+
/*
|
|
724
|
+
|--------------------------------------------------------------------------
|
|
725
|
+
| node_modules Path
|
|
726
|
+
|--------------------------------------------------------------------------
|
|
727
|
+
|
|
|
728
|
+
| Absolute path to the node_modules directory containing Puppeteer.
|
|
729
|
+
| When null, Browsershot looks for node_modules in the project root.
|
|
730
|
+
|
|
|
731
|
+
*/
|
|
732
|
+
'node_modules_path' => env('BROWSERSHOT_NODE_MODULES_PATH', null),
|
|
733
|
+
|
|
734
|
+
/*
|
|
735
|
+
|--------------------------------------------------------------------------
|
|
736
|
+
| Disable Chrome Sandbox
|
|
737
|
+
|--------------------------------------------------------------------------
|
|
738
|
+
|
|
|
739
|
+
| Set to true when running inside Docker or other containerized environments
|
|
740
|
+
| where Chrome's sandbox cannot be used. This passes --no-sandbox to Chrome.
|
|
741
|
+
|
|
|
742
|
+
*/
|
|
743
|
+
'no_sandbox' => env('BROWSERSHOT_NO_SANDBOX', false),
|
|
744
|
+
|
|
745
|
+
];
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
---
|
|
749
|
+
|
|
750
|
+
## Key Design Decisions
|
|
751
|
+
|
|
752
|
+
**Why Puppeteer / Browsershot instead of DomPDF:**
|
|
753
|
+
- DomPDF has limited CSS support — no flexbox, no grid, incomplete Tailwind CSS compatibility.
|
|
754
|
+
- Puppeteer uses headless Chrome, which provides full CSS support and pixel-perfect rendering.
|
|
755
|
+
- Report Blade templates use Tailwind CSS classes, the same design system as the web application,
|
|
756
|
+
ensuring visual consistency between the UI and generated PDFs.
|
|
757
|
+
- Browsershot provides a clean PHP API around Puppeteer, handling process management and
|
|
758
|
+
binary detection automatically.
|
|
759
|
+
|
|
760
|
+
**Why DTO as data source:**
|
|
761
|
+
- Modules expose data through service interfaces that return DTOs.
|
|
762
|
+
- Reports consume the module's public API, never bypassing it to query the database directly.
|
|
763
|
+
- This preserves module boundaries — a report is a consumer of the module, not part of its internals.
|
|
764
|
+
- DTOs are easily testable (construct them in tests without database setup).
|
|
765
|
+
|
|
766
|
+
**Template management:**
|
|
767
|
+
- Each module owns its PDF Blade templates in its `resources/views/reports/` directory.
|
|
768
|
+
- Templates are self-contained HTML documents (not partials) — they include their own `<head>`,
|
|
769
|
+
Tailwind CDN, and print styles so Browsershot can render them independently.
|
|
770
|
+
- The AI coding agent writes these templates programmatically as part of module development.
|
|
771
|
+
|
|
772
|
+
---
|
|
773
|
+
|
|
774
|
+
## Operational Notes
|
|
775
|
+
|
|
776
|
+
**Chrome memory usage:**
|
|
777
|
+
- Each Browsershot PDF generation spawns a headless Chrome process. For reports with large
|
|
778
|
+
datasets (thousands of rows), Chrome may consume significant memory (200-500 MB per render).
|
|
779
|
+
- For very large reports, consider paginating the data and generating multiple pages rather
|
|
780
|
+
than rendering a single enormous table.
|
|
781
|
+
|
|
782
|
+
**Concurrent report generation:**
|
|
783
|
+
- Multiple simultaneous PDF requests each spawn their own Chrome process. On servers with
|
|
784
|
+
limited memory, consider using a queue (Laravel Queue) to serialize report generation
|
|
785
|
+
and prevent memory exhaustion.
|
|
786
|
+
- Example: dispatch a job to generate the report, then redirect the user to a download page
|
|
787
|
+
that polls for completion.
|
|
788
|
+
|
|
789
|
+
**Docker setup:**
|
|
790
|
+
- The Dockerfile must install Node.js and either Puppeteer (which bundles Chrome) or
|
|
791
|
+
a system Chromium package.
|
|
792
|
+
- Chrome inside Docker requires the `--no-sandbox` flag. Set `BROWSERSHOT_NO_SANDBOX=true`
|
|
793
|
+
in the container environment.
|
|
794
|
+
- Required system dependencies for Chrome in Debian/Ubuntu-based containers:
|
|
795
|
+
`libnss3`, `libatk1.0-0`, `libatk-bridge2.0-0`, `libcups2`, `libxcomposite1`,
|
|
796
|
+
`libxrandr2`, `libgbm1`, `libpango-1.0-0`, `libcairo2`, `libasound2`.
|
|
797
|
+
|
|
798
|
+
**Tailwind CDN in templates:**
|
|
799
|
+
- The `<script src="https://cdn.tailwindcss.com"></script>` tag in PDF templates loads
|
|
800
|
+
Tailwind CSS at render time. Browsershot's `waitUntilNetworkIdle()` ensures the script
|
|
801
|
+
is fully loaded before generating the PDF.
|
|
802
|
+
- For air-gapped environments without internet access, bundle a pre-built Tailwind CSS file
|
|
803
|
+
and reference it with an absolute `file://` path or inline the CSS directly in the template.
|