Add reporting features with new report engine and bookmark management

- Introduced a comprehensive report engine for generating and managing reports, including sales and logistics reports.
- Added new API endpoints for retrieving report configurations, data, and metadata, ensuring secure access with privilege checks.
- Implemented a report bookmarks system to allow users to save and manage report filters and configurations.
- Created database migrations for the `report_bookmarks` table and updated the schema to support new report functionalities.
- Developed services and controllers for handling report queries and bookmarks, including CRUD operations for bookmarks.
- Enhanced API documentation to reflect the new reporting features and endpoints.
- Added unit and integration tests to validate the new functionalities and ensure data integrity across report operations.
This commit is contained in:
shancheas
2026-09-01 08:45:52 +07:00
parent 5579cf6566
commit 2955b974d2
43 changed files with 3257 additions and 1 deletions
+30
View File
@@ -83,3 +83,33 @@ Bootstrap: first user is draft until `UPDATE users SET status = 'active'`.
## Employees
Create/update optional `userId` (assign an existing login user) or nested `user` (`id?`, `username?`, `password?`). Nested `user` without `id` creates a login user (`username` + `password` required) or updates the currently linked username. `user.id` / `userId` links an existing user; `username` may be updated, but `password` is rejected (use `PATCH /users/:id`). Nested `user` cannot set `privilegeId`. `user: null` or `userId: null` unlinks. Users link the other way with `employeeId`. DTO nests `user: { id, username } | null`. List filter `userId`. List filter `position` as one or more of `sales` | `driver` | `crew` (`?position=sales&position=driver`). CSV optional `userId`. Unique assigned user → `409`.
## Reports
Privilege keys: `SALES.REPORT`, `LOGISTICS.REPORT` (seeded in migration `0013_reports`).
### `GET /reports/config` — bearer — `200`
Query: `groupNames` (e.g. `sales_report`). Returns report configs visible to the caller, each with optional `activeFilter` and `activeTableConfig` bookmarks.
### `POST /reports/data` — bearer — `200`
Body: `{ groupName, uniqueName, queryModel }`. Returns row array keyed by column id.
### `POST /reports/meta` — bearer — `200`
Same body as data. Returns `{ totalRow, limit, offset }`.
### Report bookmarks
| Method | Path | Notes |
| --- | --- | --- |
| `GET` | `/report-bookmarks` | List for current user (`@Pagination()`) |
| `GET` | `/report-bookmarks/label-history` | Distinct labels |
| `GET` | `/report-bookmarks/applied` | Query: `groupName`, `uniqueName`, `type` |
| `POST` | `/report-bookmarks` | Create (`201`) |
| `PUT` | `/report-bookmarks/applied/:id` | Apply (unapplies siblings) |
| `PUT` | `/report-bookmarks/unapplied/:id` | Clear applied |
| `DELETE` | `/report-bookmarks/:id` | `204` |
Bookmark `type`: `FILTER_TABLE` | `TABLE_CONFIG`. `configuration` is opaque JSON (filter form values or AG Grid column state).
+52
View File
@@ -0,0 +1,52 @@
# TrackGo Report Engine
Config-driven reporting for `trackgo-be` (`src/modules/reports`) and `trackgo-fe` (`apps/web/src/core/report`).
## Architecture
1. **Report config** — TypeScript object per report (`shared/configs/`). Defines SQL `tableSchema`, columns, filters, and `privilegeKey`.
2. **Query builder**`ReportQueryBuilder` compiles AG Grid `queryModel` + config into parameterized Drizzle SQL (`db.execute`).
3. **Generic UI**`ReportProvider` loads configs for a `groupName` and renders one tab per report via `ReportTable` (AG Grid Server-Side Row Model).
Persisted engine data:
- `report_bookmarks` — saved filters (`FILTER_TABLE`) and table layouts (`TABLE_CONFIG`)
Report rows are **never** stored; they are queried live from business tables.
## Groups and privilege keys
| Group | `groupName` | Privilege key | Menu path |
| --- | --- | --- | --- |
| Sales reports | `sales_report` | `SALES.REPORT` | `/app/sales/reports/index` |
| Logistics reports | `logistics_report` | `LOGISTICS.REPORT` | `/app/logistics/reports/index` |
## HTTP APIs
| Method | Path | Body / query |
| --- | --- | --- |
| `GET` | `/reports/config` | `groupNames` (array or repeated) |
| `POST` | `/reports/data` | `{ groupName, uniqueName, queryModel }` |
| `POST` | `/reports/meta` | same as data → `{ totalRow, limit, offset }` |
| `GET` | `/report-bookmarks` | list filters (`groupName`, `uniqueName`, `type`, pagination) |
| `POST` | `/report-bookmarks` | create bookmark |
| `PUT` | `/report-bookmarks/applied/:id` | apply |
| `PUT` | `/report-bookmarks/unapplied/:id` | unapply |
| `DELETE` | `/report-bookmarks/:id` | delete |
All endpoints require JWT. Report data/config endpoints use `ReportPrivilegeGuard` (config `privilegeKey` + `view`). Bookmarks are scoped to `createdBy` (current user).
## Adding a report
1. Add a `ReportConfigEntity` file under `shared/configs/`.
2. Register it in `shared/configs/index.ts`.
3. No new controller or React page — the generic UI picks it up when `groupName` matches.
## TrackGo-specific notes
- JSON uses **camelCase** (`groupName`, `queryModel`, `columnConfigs`).
- SQL values are **bound parameters**; only config-authored fragments use `sql.raw()`.
- Cell formatting uses `DateTime`, `Status`, and `Decimal` value objects.
- Excel export is **not** implemented in this phase.
See [report-list.md](./report-list.md) for the seven shipped reports.
+104
View File
@@ -0,0 +1,104 @@
# TrackGo Reports
Reports implemented in the report engine. Columns reflect **available data** only — fields from the legacy PMPS UI without backing tables are omitted.
## Sales reports (`sales_report`)
### Report Sales Order
| Column | Source |
| --- | --- |
| Date | `sales_orders.date` |
| Branch | `branches.name` |
| Division | `divisions.name` |
| No. Sales Order | `sales_orders.code` |
| Customer | `customers.name` |
| Invoice Amount | `SUM(sales_order_products.quantity * price)` |
| Sales Rep. | `employees.name` |
| Last Status Order | `sales_orders.status` |
### Report Request Order
| Column | Source |
| --- | --- |
| Date | `sales_requests.date` |
| Branch | `branches.name` |
| Division | `divisions.name` |
| No. Request Order | `sales_requests.code` |
| Customer | `customers.name` |
| Sales Rep. | `employees.name` |
| Status | `sales_requests.status` |
### Report Invoice
| Column | Source |
| --- | --- |
| Date | `sales_invoices.date` |
| Branch | `branches.name` |
| Division | `divisions.name` |
| Customer | `customers.name` |
| Customer code | `customers.code` |
| Sales Order No. | `sales_invoices.sales_order_code` |
| Invoice No. | `sales_invoices.code` |
| Status | `sales_invoices.status` |
| Sales Rep. | `employees.name` |
| Balance | `sales_invoices.balance` |
### Report Payment
| Column | Source |
| --- | --- |
| Date | `sales_payments.date` |
| Payment No. | `sales_payments.code` |
| Customer code | via `sales_invoices``customers.code` |
| Branch | via invoice → `branches.name` |
| Division | via invoice → `divisions.name` |
| Sales Rep | via invoice → `employees.name` |
| Invoice ID | `sales_invoices.code` |
| Invoice Amount | `sales_invoices.balance` |
| Payment Amount | `sales_payment_invoices.amount` |
| Status | `sales_payments.status` |
### Report Visit Plan
| Column | Source |
| --- | --- |
| Date | `plans.date` (`purpose = sales`) |
| Sales Rep | `employees.name` |
| Branch | start branch name |
| Plan | count of `plan_destinations` |
| Invoice | count of `plan_invoices` |
| Status | `plans.status` |
## Logistics reports (`logistics_report`)
### Report Packing Slip
| Column | Source |
| --- | --- |
| Date | `packing_slips.date` |
| Sales Order No. | `packing_slips.sales_order_number` |
| Packing Slip No. | `packing_slips.code` |
| Customer | `customers.name` |
| Status | `packing_slips.status` |
### Report Delivery Plan
| Column | Source |
| --- | --- |
| Date | `plans.date` (`purpose = logistics`) |
| Sales Rep | `employees.name` (driver) |
| Branch | start branch name |
| Plan | count of `plan_destinations` |
| Packing Slip | count of `plan_packing_slips` |
| Status | `plans.status` |
## Not built (no backing data)
These reports from the legacy PMPS list require visit tracking, permissions, or alerts tables that do not exist in TrackGo:
- Report Performance (sales and logistic)
- Report Sales Permission / Report Logistic Permission
- Report Alert (sales and logistic)
Also omitted as columns everywhere: Visited, Break Time, Driving, Stop Time, Cancel, Alert counts, and live visit actuals.