👨‍💻 Developer 🛠 Admin 👤 User fix/reportviewer-reliability-hardening

GbReportViewer — the one report screen behind every report in GB5

Every MenuType==2 tab across the whole application — Grid reports, Pivot cross-tabs, HTML statements, and Page/Portlet dashboards — is rendered by the same component tree: gbreportviewer + gbfilter + gbslickgrid + gbreportaction. This guide covers how to use it, how to configure it, and how it's built.

5 view typesGrid · Template · Pivot · Html · Page
1 filter panelgb-filter, reused app-wide beyond reports
1 config surfaceReportConfig — export limits, cache, scheduling gate
2 scheduling pathsone-shot Job Worker · recurring TSCHEDULER (unconnected)

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.

ℹ️
One core, many consumers. 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

Component tree for a report tab
Shell
gb-reportviewer
Owns rowData/columnData/viewtype, orchestrates every load
→
Action bar
gb-reportaction
View switcher, export menu, refresh, scheduler, mail
→
Filter panel
gb-filter
Criteria, periods, saved configs, job submission
→
Grid/Pivot renderer
app-gbslickgrid
SlickGrid wrapper, shared by Grid and Pivot views

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

ValueNameWhat it looks likeBackend behavior
0GridStandard sortable/groupable data gridFlat row list — the default path
1TemplateSame grid, alternate field templateFlat row list (identical to Grid)
2PivotCross-tab with dynamic columns + grand totalServer-side pivot via DataPivotEngine
3HtmlFull formatted statement/documentHandlebars-rendered HTML string
4PagePortlet 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.

👤 For Users

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

  1. Open a report tab. The filter panel loads with any default criteria already applied.
  2. Adjust field values, pick a date period, or type a search value.
  3. Click Apply — the grid reloads with the new criteria.
  4. 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:

PresetResolves 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 / YearThe equivalent full prior period
Previous Year DTD / WTD / MTD / YTDSame-position cut-off, one year back
All / OthersNo 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.
ℹ️
Saved configurations are personal to the report + web service they were created against — they don't carry across unrelated reports.

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:

FieldOptions
Output formatPDF, CSV, HTML, Jasper
Delivery optionEmail, WhatsApp, SMS, Notification
PriorityHigh, Low
Start / End / ExpiryDate + 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.

⚠️
This is a one-shot job, not a recurring schedule. It runs once, in the window you specify. There is no "every Monday at 8am" option today for reports — see Scheduler architecture for why.
🛠 For Admins

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:

ColumnMeaning
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
⚠️
A more visual drag-and-drop Rows / Columns / Values wizard step also exists in the codebase (with the same 9 aggregation types, one dropdown per Values chip) but its entry button is currently commented out in the UI — not reachable today. Use the Properties-step columns above until that's re-enabled.

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.

🚧
Currently unreachable in the running app. The 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.
👨‍💻 For Developers

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:

FieldConsumed byDefault when null
AllowedExportFormatsgbreportaction's allowedExportFormats getterShow every export button
MaxExportRowsgbslickgrid.manulexportToExcel()No cap
CacheDurationMinutesttlMs param on CriteriaConfigservice/ReportViewFieldsservice10 minutes
IsSchedulableScheduler menu item's *ngIfHidden
⚠️
Because all 4 forkJoin members fire concurrently, 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()'s dexieboolean parameter defaults to false and no caller anywhere passes it true — deliberate, matches the platform-wide rule against caching financial/ledger data.
  • Metadata is cached (CriteriaConfig picklist, ReportViewFields, ReportConfig itself) via gbhttppost/gbhttpget's Isstore flag, default TTL 600000ms (10 min).
  • TTL is now per-call configurable — gbhttpget/gbhttppost both take an optional trailing ttlMs param (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() invalidate Analytics.ReportConfig.ByReport).

Scheduler architecture — two separate systems

One-shot Job Worker wired to reports
JobWorkerComponent · SysJobDbService

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
ScheduleManagerComponent · JobEngineDbService

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

  1. Add a numeric branch in viewseletionsetting(), set this.viewtype to a new string.
  2. If it needs its own data shape, extend applyRowServiceResult()/add an extract*Payload() helper — don't bend extractReportDetail().
  3. Add a template branch keyed on the new viewtype string in reportviewer.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

FileRole
features/gbreportviewer/reportviewer/reportviewer.component.tsShell — owns rowData/columnData/viewtype, view dispatch, forkJoin orchestration
features/gbfilter/gbfilter.component.tsFilter panel — criteria, periods, saved configs, job submission (OnPush)
features/gbslickgrid/gbslickgrid.component.tsGrid/Pivot renderer, export (incl. MaxExportRows cap)
features/gblayout/gbreportaction/reportactionscreen/gbreportaction.component.tsAction bar — view switcher, export menu, scheduler trigger
features/gbdashboard/service/portlet-loader.service.tsShared portlet-list-to-DashboardItem mapping (gb-dashboard + View=4)
projects/analytics/master/reportconfig/reportconfig.component.tsReportConfig admin form (currently unrouted)
libs/common/src/lib/gbservice/gbhttpservice/gbhttp.service.tsShared HTTP layer — dedup, cache, TTL (providedIn:'root' singleton)
features/gbdexiedb/gbdexie.db.tsIndexedDB cache backing store

Reference

View type table (MREPORTVIEW.REPORTVIEWTYPE)

ValueNameBackendFE dispatch
0GridFlat row listView==0 → executeGridViewSetup()
1TemplateFlat row list (same as Grid)SecondTemplateLocation string dispatch
2PivotDataPivotEngine — {rowHeaders,pivotColumns,rows,grandTotal}View==2 → viewtype='PIVOT'
3HtmlHandlebars → full HTML stringSecondTemplateLocation=='Standard Html View' → STDHTMLVIEW
4PagePortlet layout via GetPortletListWithCriteriaView==4 → loadPageView()

ReportConfig field reference

FieldTypeDefaultMeaning
ReportIdnumber—FK to the report (MenuReportId)
WorkspaceIdnumber0Analytics workspace grouping
DataSourceType0-40 Default0 Default · 1 Internal · 2 ETL · 3 Direct DB · 4 REST API
PivotEngine1-311 Native (GB5) · 2 AG Grid Pivot · 3 Server-side
ReportCategory1-411 Operational · 2 Analytical · 3 Executive · 4 Compliance
AsyncThresholdRowsnumber1000Row count above which the report should run async (not yet wired to FE)
MaxExportRowsnumber50000Caps manulexportToExcel()'s row count
CacheDurationMinutesnumber5Overrides the 10-min default TTL for this report's metadata
ResultExpiryHoursnumber24How long a scheduled job's result stays downloadable (not yet wired to FE)
IsSchedulable0/10Gates the Scheduler menu item
DefaultPriority1-321 Low · 2 Normal · 3 High — default Job Worker priority
AllowedExportFormatsCSV string'CSV,XLSX,PDF'Gates which export buttons show
SortOrdernumber1Display ordering among a workspace's reports

API route reference (dot-code keys)

MethodKeyPath
GETFramework.ReportMenu.Get/fws/Menu/ReportMenuDetailsForMenu
POSTFramework.Report.GetSelectListReportviewandReportViewFields/fws/ReportView/GetSelectListReportviewandReportViewFields
POSTFramework.CriteriaConfig.Picklist/fws/CriteriaConfig.svc/SelectList
GETAnalytics.ReportConfig.ByReport/Analytics/ReportConfig/GetReportConfigByReport (via ReportId)
POST(row data) /cos/Common.svc/ReportBackend routes internally off ReportViewId — same URL for Grid/Pivot/Html
GETFramework.PortletListWithCriteria.Get/fws/Portlet/GetPortletListWithCriteria
POSTFramework.UserVsPortlet.Save/fws/UserVsPortlet/SaveUserVsPortlet
DELFramework.UserVsPortlet.DeleteByPage/fws/UserVsPortlet/DeleteUserVsPortletWithUserIdandPageId
POSTFramework.SystemJob.Save/fws/SystemJob/SaveSystemJob
GETFramework.SystemJob.GetList/fws/SystemJob/GetListSystemJob

FAQ & known issues

❓
Why doesn't my report show a Scheduler option? Either it has no ReportConfig row (defaults to hidden), or its ReportConfig has IsSchedulable=0.
❓
Why did my export get cut short? The report's ReportConfig has a MaxExportRows cap — you'll see a dialog confirming this rather than a silent truncation.
❓
Can I schedule a report to run every week? Not yet — only one-shot job submission exists for reports today. See Scheduler architecture.
🚧
Open items: ReportConfig admin screen needs a route/menu entry; the drag-and-drop Pivot wizard step needs its trigger re-enabled; AsyncThresholdRows/ ResultExpiryHours are modeled but not yet consumed anywhere on the frontend.