Overview
A "report" in GB5 is a configured MREPORTVIEW row hanging off a menu item. Opening that menu item
loads <gb-reportviewer>, which in turn loads the report's metadata, its default view, and its
filter criteria, then renders whichever view type the admin configured for that view — a plain
grid, a cross-tab pivot, a formatted HTML statement, or a portlet dashboard.
gb-filter alone is embedded well
beyond reports — dashboard widgets, workflow/approval screens, ESS leave/attendance/permission screens all reuse
it. Anything that touches the shared HTTP/cache layer or gbfilter itself has an app-wide blast
radius, not just a report-screen one.Component architecture
Two more components plug in for specific view types: app-gbstandardhtml renders the Html view's
sanitized HTML string, and gb-dynamicwidget (borrowed from the main dashboard feature) renders each
portlet in a Page view.
The 5 view types, at a glance
| Value | Name | What it looks like | Backend behavior |
|---|---|---|---|
| 0 | Grid | Standard sortable/groupable data grid | Flat row list — the default path |
| 1 | Template | Same grid, alternate field template | Flat row list (identical to Grid) |
| 2 | Pivot | Cross-tab with dynamic columns + grand total | Server-side pivot via DataPivotEngine |
| 3 | Html | Full formatted statement/document | Handlebars-rendered HTML string |
| 4 | Page | Portlet dashboard (charts, cards, tables) | Portlet layout + per-portlet data |
Full technical detail on each is in the reference table below and the developer dispatch section.
Applying Filters & Criteria
Every report opens with its filter panel (gb-filter) either visible alongside the grid or reachable via a "Filter" control, depending on the screen. The panel lists the report's configured criteria fields — text, picklist, date, and dependent-picklist fields — plus a date-range control driven by period presets.
Typical flow
- Open a report tab. The filter panel loads with any default criteria already applied.
- Adjust field values, pick a date period, or type a search value.
- Click Apply — the grid reloads with the new criteria.
- Clicking Apply repeatedly is safe — a double-click guard prevents duplicate requests.
Period filters explained
The date-range dropdown offers a fixed set of presets, each resolving to a concrete from/to date under the hood:
| Preset | Resolves to |
|---|---|
| Today (DTD) | Today's date, both ends |
| Current Week (WTD) | Start of this week → today |
| Current Month (MTD) | 1st of this month → today |
| Current Year (YTD) | 1st of this fiscal/calendar year → today |
| Previous Week / Month / Year | The equivalent full prior period |
| Previous Year DTD / WTD / MTD / YTD | Same-position cut-off, one year back |
| All / Others | No date restriction, or a fully custom from/to pair |
Saving & loading a filter configuration
If you find yourself re-entering the same criteria every time, save it as a named Configuration:
- Open the filter panel's Configuration tab.
- Pick an existing saved configuration from the searchable dropdown, or type a new name to create one.
- Click Save — enabled once a name is entered. Re-saving under the same name overwrites it.
- Picking a saved configuration from the dropdown immediately repopulates every filter field from it.
- Delete removes the currently-selected saved configuration.
Switching view types
If a report has more than one configured view (e.g. both a Grid and a Pivot view of the same data), a dropdown in the action bar lets you switch between them. Switching views re-triggers the underlying data load — a Pivot view in particular recomputes its columns every time criteria change, since the set of pivot columns depends on what distinct values are actually in the result.
Exporting a report
The action bar's Settings menu offers PDF, CSV, Excel, and "Grid Export" options, depending on what the report is configured to allow. An admin can restrict which formats appear for a given report (see ReportConfig) and cap how many rows a single export can contain — if your export would exceed that cap, you'll see a dialog telling you it was capped, rather than a silent partial file.
Scheduling a report
Where enabled (see ReportConfig's "Schedulable" setting), the action bar shows a Scheduler option. This opens the Job Worker dialog, pre-filled with the report's current filter criteria, where you choose:
| Field | Options |
|---|---|
| Output format | PDF, CSV, HTML, Jasper |
| Delivery option | Email, WhatsApp, SMS, Notification |
| Priority | High, Low |
| Start / End / Expiry | Date + time pickers — the window the job is allowed to run/remain valid in |
Submitted jobs appear in My Jobs, with live status (Pending → Running → Completed/Failed/Cancelled) updated in real time, plus Cancel, Clone, Delete, and Download-Result actions.
Configuring Report Views
Report views are configured in the Report View Setting admin screen
(gbreportviewsetting). Its wizard's first step is literally labeled "View Type" — a
card grid to pick Grid / Template / Pivot / Html / Page for the view you're creating.
Setting up a Pivot view
Checking "Enable Data Pivot" on a view reveals extra per-field columns in the Properties step:
| Column | Meaning |
|---|---|
| DType (Display Type) | Horizontal / Vertical / Info / Summary — whether this field is a row dimension, column dimension, or measure |
| AType (Aggregation Type) | None, Sum, Distinct Sum, Avg, Distinct Avg, Count, Distinct Count, Max, Min |
| CT (Column Total) | Include this field's column in the grand-total row |
| RT (Row Total) | Include a subtotal per row group |
| PL (Pivot Level) | Numeric nesting level for multi-level row dimensions |
ReportConfig — per-report governance
Beyond the view itself, a separate ReportConfig record governs cross-cutting behavior for a report: which export formats are offered, how many rows an export may contain, how long its metadata stays cached, and whether it can be scheduled. See the full field reference for every setting.
gb-report-config
admin form exists and is fully functional, but has no route or menu entry anywhere in the application —
there is no click-path to it today. A report with no ReportConfig row simply uses safe defaults (all export
formats shown, 10-minute cache, scheduling hidden). Getting a route/menu item added is the prerequisite for
admins to actually use this feature.Page/Portlet dashboards (View=4)
A Page-type view points at an MPAGE record via its PageId. That page's portlet layout
(type, position, size, criteria, drill-down routing) is managed the same way the main dashboard's portlets are —
through MPORTLET/MUSERVSPORTLET/MROLEVSPORTLET configuration. Portlet types
available: Chart, Map, Datatable, Single/Multiple Card, Report, HTML, Markdown, Video, Announcement, Overall DB
Card, Category Card. Portlets can publish/subscribe to each other on the same page (selecting a row in one portlet
filters others) via configured pub/sub channel names.
Managing who can schedule a report
Set IsSchedulable = 1 on a report's ReportConfig to show the Scheduler option to its users. This defaults to 0 (hidden) for any report without a ReportConfig row — deliberately restrictive, since scheduling is a capability that should be turned on per-report, not exposed everywhere by default.
Known gaps for admins
- ReportConfig admin screen has no route/menu entry — needs one added before it's usable.
- The drag-and-drop Pivot Rows/Columns/Values wizard step exists but its trigger button is commented out.
- There is no recurring ("every Monday") scheduling option for reports today — only one-shot job submission. See Scheduler architecture for the reason.
Data Flow & Request Lifecycle
Opening a report (ngOnChanges → defaultreportsettings()) fires four requests
concurrently via forkJoin, only resolving isLoading once all four settle (each wrapped in
its own catchError, so one failing doesn't starve the others):
const rows$ = Rowservice(...).pipe(map(res => applyRowServiceResult(res)), catchError(...));
const criteriaConfig$ = CriteriaConfigservice(...).pipe(...);
const viewFields$ = viewseletionsetting(selectedview); // Grid metadata, or Pivot/Page setup
const reportConfig$ = ReportConfigservice(menuReportId).pipe(...);
forkJoin([rows$, criteriaConfig$, viewFields$, reportConfig$])
.subscribe(() => { this.isLoading = false; this.cdr.detectChanges(); });
View type dispatch — viewseletionsetting()
All view-type branching lives in one method in reportviewer.component.ts. Grid and Pivot branch on
the numeric View value; everything else branches on the SecondTemplateLocation string:
if (selectedview.View == 0) {
viewFields$ = this.executeGridViewSetup(selectedview); // fetches ReportViewFieldsservice
} else if (selectedview.View == 2) {
this.viewtype = 'PIVOT'; // no separate metadata fetch —
this.columnData = []; // columns are self-describing per response
} else if (selectedview.View == 4) {
this.viewtype = 'PAGE';
viewFields$ = this.loadPageView(selectedview); // loads portlets via PortletLoaderService
} else {
// Template / Html / fallback-Grid, dispatched via SecondTemplateLocation
}
Grid's column definitions are fetched once up front (executeGridViewSetup →
ReportViewFieldsservice) because they're fixed, admin-configured fields. Pivot's are not
fetched up front — they're rebuilt from every row-service response, because the pivot column set genuinely changes
with the data (different date ranges → different Jan/Feb/Mar columns).
Response contracts per view type
All four call sites that handle a Rowservice response (defaultreportsettings,
drilldownsettings, ApplyFilter, ReloadoRefresh, paginationfn)
are consolidated into one shared applyRowServiceResult(), which branches on viewtype:
private applyRowServiceResult(res: any, isInfiniteScroll = false): void {
const pivotPayload = this.viewtype === 'PIVOT' ? this.extractPivotPayload(res) : null;
if (pivotPayload) {
this.rowData = [...pivotPayload.rows, { ...pivotPayload.grandTotal, __grandTotal: true }];
this.columnData = this.buildPivotColumnDefs(pivotPayload.rowHeaders, pivotPayload.pivotColumns);
} else {
this.rowData = this.extractReportDetail(res); // flat ReportDetail array (Grid/Template/Html)
}
...
}
Pivot's confirmed backend contract (same endpoint as everything else, POST /cos/Common.svc/Report —
the backend routes internally off ReportViewId):
{
"rowHeaders": ["Department", "Region"],
"pivotColumns": ["Jan_Amount", "Feb_Amount", "Mar_Amount"],
"rows": [{ "Department": "Sales", "Region": "North", "Jan_Amount": 12345.00 }],
"grandTotal": { "__grandTotal": true, "Jan_Amount": 22221.00 }
}
Dynamic pivot column keys follow {colDimensionValue}_{summaryFieldTitle}. Page/Portlet's contract is
a portlet layout array — see PortletLoaderService.loadPortlets() for the exact mapping into
DashboardItems.
ReportConfig wiring
Fetched as the forkJoin's 4th member via ReportConfigservice(menuReportId)
(Analytics.ReportConfig.ByReport, through the shared FormActiondbservice — no
cross-project import needed since that service lives in features/gblayout/). Resolves to
null on 404/error, which every consumer treats as "use defaults", not a failure:
| Field | Consumed by | Default when null |
|---|---|---|
| AllowedExportFormats | gbreportaction's allowedExportFormats getter | Show every export button |
| MaxExportRows | gbslickgrid.manulexportToExcel() | No cap |
| CacheDurationMinutes | ttlMs param on CriteriaConfigservice/ReportViewFieldsservice | 10 minutes |
| IsSchedulable | Scheduler menu item's *ngIf | Hidden |
reportConfig isn't resolved
yet when criteriaConfig$/viewFields$ are constructed — so
CacheDurationMinutes only takes effect from a report's 2nd load onward in the same component
instance (view switches, drilldowns), not the very first cold load.Caching architecture
- Report row data itself is never cached.
reportData()'sdexiebooleanparameter defaults tofalseand no caller anywhere passes ittrue— deliberate, matches the platform-wide rule against caching financial/ledger data. - Metadata is cached (CriteriaConfig picklist, ReportViewFields, ReportConfig itself) via
gbhttppost/gbhttpget'sIsstoreflag, default TTL 600000ms (10 min). - TTL is now per-call configurable —
gbhttpget/gbhttppostboth take an optional trailingttlMsparam (defaults to 600000, so every existing caller is unaffected).DexieService.postgetApiResponse()enforces the same TTL on the read side. - Invalidation uses
gbhttpservice.gbremovedexieurl(routeKey)— called after any admin save that changes cached metadata (e.g.ReportConfig.save()/delete()invalidateAnalytics.ReportConfig.ByReport).
Scheduler architecture — two separate systems
One-shot Job Worker wired to reports
A single run within a start/end window. gbreportaction's Scheduler menu item emits
ScheduleReport, which reportviewer.component.ts handles by delegating to a
@ViewChild(GbFilterComponent) reference's existing openJobSubmission() — reusing
gbfilter's already-correct criteria→job payload logic as-is. Monitored via My Jobs
(live SignalR status).
Recurring TSCHEDULER not connected to reports
Real cron/interval/one-time recurrence — but its schema has no payload/target field at all
(just SchedulerType/CronExpression/IntervalSeconds/ScheduledDate). Attaching a report+criteria to
a recurring schedule needs a backend schema change; out of scope for the frontend alone.
Extending the stack
Adding a new view type
- Add a numeric branch in
viewseletionsetting(), setthis.viewtypeto a new string. - If it needs its own data shape, extend
applyRowServiceResult()/add anextract*Payload()helper — don't bendextractReportDetail(). - Add a template branch keyed on the new
viewtypestring inreportviewer.component.html.
Adding a new export format
Add the format string to AllowedExportFormats's comma-separated convention, add an
*ngIf gate on allowedExportFormats.includes('X') in gbreportaction.component.html,
implement the actual export call.
Adding a new portlet type
Register it in WidgetComponentMap and handle its PortletTypeName in
gbdynamicwidget.component.ts's renderWidget() switch.
Key files reference
| File | Role |
|---|---|
| features/gbreportviewer/reportviewer/reportviewer.component.ts | Shell — owns rowData/columnData/viewtype, view dispatch, forkJoin orchestration |
| features/gbfilter/gbfilter.component.ts | Filter panel — criteria, periods, saved configs, job submission (OnPush) |
| features/gbslickgrid/gbslickgrid.component.ts | Grid/Pivot renderer, export (incl. MaxExportRows cap) |
| features/gblayout/gbreportaction/reportactionscreen/gbreportaction.component.ts | Action bar — view switcher, export menu, scheduler trigger |
| features/gbdashboard/service/portlet-loader.service.ts | Shared portlet-list-to-DashboardItem mapping (gb-dashboard + View=4) |
| projects/analytics/master/reportconfig/reportconfig.component.ts | ReportConfig admin form (currently unrouted) |
| libs/common/src/lib/gbservice/gbhttpservice/gbhttp.service.ts | Shared HTTP layer — dedup, cache, TTL (providedIn:'root' singleton) |
| features/gbdexiedb/gbdexie.db.ts | IndexedDB cache backing store |
Reference
View type table (MREPORTVIEW.REPORTVIEWTYPE)
| Value | Name | Backend | FE dispatch |
|---|---|---|---|
| 0 | Grid | Flat row list | View==0 → executeGridViewSetup() |
| 1 | Template | Flat row list (same as Grid) | SecondTemplateLocation string dispatch |
| 2 | Pivot | DataPivotEngine — {rowHeaders,pivotColumns,rows,grandTotal} | View==2 → viewtype='PIVOT' |
| 3 | Html | Handlebars → full HTML string | SecondTemplateLocation=='Standard Html View' → STDHTMLVIEW |
| 4 | Page | Portlet layout via GetPortletListWithCriteria | View==4 → loadPageView() |
ReportConfig field reference
| Field | Type | Default | Meaning |
|---|---|---|---|
| ReportId | number | — | FK to the report (MenuReportId) |
| WorkspaceId | number | 0 | Analytics workspace grouping |
| DataSourceType | 0-4 | 0 Default | 0 Default · 1 Internal · 2 ETL · 3 Direct DB · 4 REST API |
| PivotEngine | 1-3 | 1 | 1 Native (GB5) · 2 AG Grid Pivot · 3 Server-side |
| ReportCategory | 1-4 | 1 | 1 Operational · 2 Analytical · 3 Executive · 4 Compliance |
| AsyncThresholdRows | number | 1000 | Row count above which the report should run async (not yet wired to FE) |
| MaxExportRows | number | 50000 | Caps manulexportToExcel()'s row count |
| CacheDurationMinutes | number | 5 | Overrides the 10-min default TTL for this report's metadata |
| ResultExpiryHours | number | 24 | How long a scheduled job's result stays downloadable (not yet wired to FE) |
| IsSchedulable | 0/1 | 0 | Gates the Scheduler menu item |
| DefaultPriority | 1-3 | 2 | 1 Low · 2 Normal · 3 High — default Job Worker priority |
| AllowedExportFormats | CSV string | 'CSV,XLSX,PDF' | Gates which export buttons show |
| SortOrder | number | 1 | Display ordering among a workspace's reports |
API route reference (dot-code keys)
| Method | Key | Path |
|---|---|---|
| GET | Framework.ReportMenu.Get | /fws/Menu/ReportMenuDetailsForMenu |
| POST | Framework.Report.GetSelectListReportviewandReportViewFields | /fws/ReportView/GetSelectListReportviewandReportViewFields |
| POST | Framework.CriteriaConfig.Picklist | /fws/CriteriaConfig.svc/SelectList |
| GET | Analytics.ReportConfig.ByReport | /Analytics/ReportConfig/GetReportConfigByReport (via ReportId) |
| POST | (row data) /cos/Common.svc/Report | Backend routes internally off ReportViewId — same URL for Grid/Pivot/Html |
| GET | Framework.PortletListWithCriteria.Get | /fws/Portlet/GetPortletListWithCriteria |
| POST | Framework.UserVsPortlet.Save | /fws/UserVsPortlet/SaveUserVsPortlet |
| DEL | Framework.UserVsPortlet.DeleteByPage | /fws/UserVsPortlet/DeleteUserVsPortletWithUserIdandPageId |
| POST | Framework.SystemJob.Save | /fws/SystemJob/SaveSystemJob |
| GET | Framework.SystemJob.GetList | /fws/SystemJob/GetListSystemJob |
FAQ & known issues
IsSchedulable=0.MaxExportRows cap — you'll see a dialog confirming this rather than a silent truncation.AsyncThresholdRows/
ResultExpiryHours are modeled but not yet consumed anywhere on the frontend.