Analytics Report & Analytics Platform

Analytics & Report Platform

Unified multi-tenant analytics — from live grid reports to ad-hoc AnalysisQuery, multisource KPI feeds, and performance intelligence

4
Report Modes
6
Multisource Connectors
5
KPI Entity Types
3
Dapr KPI Subscribers
18
DataSync Tables

Platform Philosophy

The GB5 Analytics Platform is a unified, multi-tenant, streaming-first reporting infrastructure. It handles everything from simple paginated grids to cross-tabulated PDF exports generated asynchronously, to fully ad-hoc AnalysisQuery execution with live column designers, to ingestion of real-time KPI signals from PERM, OKR, and external data sources via Dapr pub/sub.

No SQL Injection

Zero string concatenation. All queries parameterized Dapper statements. AnalysisQuery uses a safe column-whitelist before any dynamic generation.

Streaming IO

Report data consumed as IAsyncEnumerable — 100k+ row exports never fully loaded into memory. PDF/Excel streamed directly to response.

Configurable Per Report

Async threshold, export formats, datasource preference, pivot mode, timeout — all in MREPORTCONFIG. Zero code changes to tune a report.

Multi-DB Routing

MREPORTDATASOURCERULE routes each report to OLTP, Report DB, or Archive DB based on date-range rules — transparent to the caller.

Architecture Layers

Angular Frontend — Grid components · Export buttons (Excel/PDF/CSV) · My Reports inbox · Pivot UI · AnalysisQuery designer · KPI dashboard tiles
↕ HTTP (FastEndpoints)
Service Layer (AnalyticsSL) — /CommonReport/Report · /Analysis/Execute · /KPI/* · /DataSource/* · /Workspace/* · /PerfKPI/*
↕
Business Logic Layer (AnalyticsBLL) — CommonReportBLL · DataPivotEngine · AnalysisQueryBLL · KPIListBLL · PerfKPIFeedBLL · IQueryOrchestrator · AsyncDecisionEngine · DataSourceRuleEvaluator · PivotExcelExport · PivotPdfExport
↕
Data Access Layer (AnalyticsDAL) — AnalysisDAL (refactored) · KPIListDAL · PerfKPIFeedDAL · CommonReportDAL · SystemJobDAL · DataSourceRuleDAL · WorkspaceDAL
↕ Dapper / IQueryExecutor
SQL Server / PostgreSQL — MREPORT · MREPORTVIEW · MREPORTCONFIG · MANALYSIS · MKPI · TKPILIST · TKPIMANUAL · MROLEKPI · TPERFKPIFEED · TSYSJOB · MREPORTDATASOURCERULE · DataSync tables (18)
AnalysisDAL Refactor (2026-06): AnalysisDAL was refactored from a monolithic class to use the shared IQueryExecutor pattern — consistent connection management, HybridCache support, and CancellationToken propagation throughout. Module DALs that previously called AnalysisDAL directly now call through AnalysisBLL interfaces.

Report Modes

REPORTVIEWTYPEModeData Shape to FrontendExport Support
0Standard (flat)Paginated rows; column headers from MREPORTVIEWFIELDS; user sorts/filters in-browserExcel (flat), PDF, CSV
1AnalysisDynamic SQL result; field list varies per AnalysisQuery; ad-hoc column compositionExcel (flat), CSV
2PivotIsPivotView=true → BE cross-tabulated grid; IsPivotView=false → raw rows for FE drag-drop pivot UIExcel (cross-tab), PDF (pivot-aware table), CSV (flat)
3KPI DashboardKPI card tiles with value + trend + target vs actual; no paginationExcel (KPI summary), PDF

Async Decision Flow

Request arrives
ReportViewId + params
→
AsyncDecisionEngine
row estimate vs threshold
→
Sync path
stream direct to response
— or for large datasets —
Async path
TSYSJOB created
→
QuartzJob runs
generates file, stores to blob
→
My Reports inbox
notification + download link

MultiSource Data Connectors

Analytics connects to multiple data sources beyond the primary OLTP database. Each source is registered in MREPORTDATASOURCE and selected at report-time via MREPORTDATASOURCERULE based on date range, module, and tenant configuration.

Supported Source Types

S
SQL Server (OLTP)
Primary transactional database per tenant. Routes via LoginDTO.DatabaseName. Covers all real-time operational queries.
R
Report DB (Replica)
Read replica or dedicated report database. Used for large exports and analytical queries to avoid OLTP contention. Configured per tenant in MREPORTDATASOURCE.
A
Archive DB
Long-retention cold storage. Selected automatically when query date range extends beyond archive threshold (default: 2 years). MREPORTDATASOURCERULE drives selection.
P
PostgreSQL (Npgsql)
Alternate RDBMS per tenant/module. IQueryExecutor handles both SQL Server and PostgreSQL via LoginDTO.DatabaseType. Same Dapper queries; driver-aware execution.
D
DataSync Mirror
DataSync-replicated tables (see ETL section). Used when cross-tenant or cross-region consolidation is needed. Registered as a separate MREPORTDATASOURCE entry.
X
External API (future)
Extensibility placeholder for REST-based data connectors (ERP integrations, CRM data). IDataSourceConnector interface already defined; no production connectors yet.

DataSource Schema

TablePurposeKey Columns
MREPORTDATASOURCERegistered data source configurations per tenantDataSourceId, Name, SourceType (0=OLTP/1=ReportDB/2=Archive/3=PostgreSQL/4=DataSync), ConnectionStringKey (Vault path), IsDefault, TenantId
MREPORTDATASOURCERULEConditional routing rules — which source to use whenRuleId, DataSourceId, ReportViewId (nullable=applies to all), ModuleCode (nullable), DateRangeDays (if query spans >N days → use this source), Priority, TenantId
Security: ConnectionStringKey stores a HashiCorp Vault path — never the actual connection string. At query time, ServerConfigCache resolves the key to a live connection string via VaultSharp. Strings are cached in-process for 60s with automatic rotation on error.

DataSource Rule Evaluation

// DataSourceRuleEvaluator — selects best datasource for a given report+params
1. Load rules for ReportViewId (or global rules if none specific)
2. Filter: ModuleCode matches or is null
3. Filter: query date range exceeds DateRangeDays threshold (if set)
4. Sort by Priority ASC — first matching rule wins
5. Fallback: MREPORTDATASOURCE WHERE IsDefault=true AND TenantId=@t
6. Resolve connection string from Vault via ServerConfigCache.GetConnectionStringAsync(key)

Workspace Concept

A Workspace is a named, saveable AnalysisQuery configuration — a user's or team's persistent "analytical view" of a dataset. Workspaces capture: the selected DataSource, column configuration, applied filters, sort order, grouping, and optionally a scheduled refresh or export.

Workspace Schema

TableColumnNotes
TANALYSISWORKSPACEWorkspaceId int PK
WorkspaceName nvarchar(200)User-visible name (e.g., "Q2 Sales Trend")
AnalysisId int FK MANALYSISBase analysis definition this workspace is built on
OwnerId intEmployeeId who created/owns this workspace
IsShared bitShared with all users in the tenant if true
ColumnConfigJson nvarchar(max)JSON: selected columns, ordering, visibility, widths
FilterJson nvarchar(max)JSON: applied filter conditions + values
SortJson nvarchar(max)JSON: sort column + direction
TenantId intIndexes: (OwnerId, TenantId), (AnalysisId, TenantId, IsShared)

Personal Workspace

IsShared=false. Visible only to OwnerId. Allows individual analysts to save their "daily views" without polluting shared config. Unlimited personal workspaces per analysis.

Shared Workspace

IsShared=true. Visible to all users in the tenant. Created by ADMIN role. Represents org-wide standard views (e.g., "Finance Month-End Checklist", "HR Bell Curve View").

Workspace vs Report

Reports are static, code-driven views defined in MREPORT. Workspaces are dynamic, user-driven configurations layered on top of AnalysisQuery — no dev work required to create new views.

ETL & DataSync Platform

ETL Pipeline Architecture

GB5's ETL capability is a push-based, configurable pipeline that extracts data from registered sources (GB5 OLTP, external APIs, file uploads), transforms via configurable mapping rules, and loads into target tables. Used for: cross-tenant consolidation, historical migrations, and the DataSync module.

Extract
Source-specific connector reads data in chunks. IAsyncEnumerable<T> — never loads full dataset.
Transform
METLCOLUMNMAP defines column-level transformations. Type coercion, default filling, computed fields, date normalization.
Validate
Row-level validation rules from METLRULE. Failed rows → TETLERRLOG with RowData. Never abort; collect errors.
Load
BulkInsertAsync for batches >100 rows. Upsert via ExecuteAsync for delta loads. Transaction per batch; rollback per batch on error.
Audit
TETLJOBRUN row created per execution. Tracks: TotalRows, LoadedRows, ErrorRows, StartedOn, CompletedOn, Status.

DataSync Module

DataSync lives inside GB5Framework (not a separate microservice) and orchestrates the replication of multi-tenant data across environments. It migrates and replaces the legacy Gb4Import replication service.

DataSync Database Schema (18 tables)

Table GroupTablesPurpose
ConfigurationMSWDATASOURCE, MSWCONNECTION, MSWDATABASEMAPSource/target DB registrations, connection configs, DB-to-DB routing maps
DDL & ProvisioningMSWDDLSCRIPT, MSWDDLSCRIPTDETAIL, MSWPROVISIONPLAN, MSWPROVISIONSTEPSchema version scripts, automated DB provisioning plans with ordered steps
Sync JobsMSWSYNCRULE, MSWSYNCSCHEDULE, TSWSYNCRUN, TSWSYNCRUNDETAILPer-table sync rules (delta/full), scheduling, execution audit with row counts per table
MetadataMSWMETADATASNAPSHOT, MSWMETADATACHANGE, MSWOBJECTMAPSchema snapshots for drift detection; change records; object name mappings for renames
UpgradeMSWUPGRADEPACKAGE, MSWUPGRADESTEP, TSWUPGRADERUNNamed upgrade bundles of DDL scripts; step-by-step execution with rollback support
Error TrackingTSWSYNCERRORPer-row errors during sync with SourceRowKey, ErrorText, RetryCount

GB4 Import Migration

The legacy Gb4Import WCF service replicated product/customer/financial master data across GB4 tenants via a polling-based XML pipeline. DataSync replaces this with a modern push-based approach:

Legacy Gb4ImportGB5 DataSync Equivalent
WCF service polling every 15 minMSWSYNCSCHEDULE (configurable cron, default every 5 min)
XML serialization/deserializationDirect Dapper parameter binding; no intermediate serialization
Hardcoded table listMSWSYNCRULE per table — add/remove without code changes
Full table copy each runDelta sync via MSWSYNCRULE.DeltaColumn (ModifiedOn/RowVersion)
No audit trailTSWSYNCRUN + TSWSYNCRUNDETAIL: every run fully audited
Error → silent log fileTSWSYNCERROR + Dapr event + admin alert
Manual schema migrationMSWDDLSCRIPT + MSWUPGRADEPACKAGE: versioned, automated
DataSync Phase 1-4 code complete and compile-clean as of 2026-06-10. Production cutover pending final UAT sign-off from operations team.

KPI Management Platform

KPI Management provides the framework for defining, categorizing, and tracking Key Performance Indicators across modules. KPIs are either manually entered, auto-computed from TGOAL/TAPPRAISAL data, or ingested from external sources via ETL.

KPI Schema

MKPI — KPI Master

ColumnTypeNotes
KPIId, KPICode, KPINameint / nvarcharUnique Code per tenant
KPICategorysmallint0=Financial, 1=Customer, 2=Process, 3=People, 4=OKR, 5=Custom
MeasureUnitnvarchar(50)%, $, count, days, score, etc.
AggregationTypesmallint0=Sum, 1=Avg, 2=Max, 3=Min, 4=Count, 5=LastValue
Frequencysmallint0=Daily, 1=Weekly, 2=Monthly, 3=Quarterly, 4=Annual
TargetDirectionsmallint0=HigherIsBetter, 1=LowerIsBetter, 2=TargetBand
LowerBand / UpperBanddecimal(18,4) nullableFor TargetDirection=2 (band target)
Statussmallint0=Inactive, 1=Active
TenantIdintIndex: (TenantId, KPICategory, Status). Unique: (KPICode, TenantId)

TKPILIST — KPI List (Computed)

Stores periodically computed KPI values — either from module data (PERM, OKR) via Dapr events, or from SQL queries scheduled through the ETL pipeline.

ColumnTypeNotes
KPIListId, KPIId, Period, PeriodLabelint / date / nvarcharPeriod = start of the frequency window (month start, quarter start, etc.)
ActualValue / TargetValuedecimal(18,4)TargetValue from MKPITARGET or overridden manually
Variancedecimal(18,4)Computed: ActualValue - TargetValue (positive = above target for HigherIsBetter)
VariancePercentdecimal(5,2)Computed: (Variance / TargetValue) × 100
SourceTypesmallint0=ManualEntry, 1=PERMFeed, 2=OKRFeed, 3=ETLImport, 4=SQLComputed
SourceRefIdintAppraisalPlanId / OKRCycleId / ETLJobId based on SourceType
OUID / DeptIdint nullableOrg unit scope; null = company-wide KPI
TenantIdintIndexes: (KPIId, TenantId, Period), (TenantId, Period, KPICategory)

TKPIMANUAL — Manual KPI Entry

Allows HR / Finance / managers to enter KPI values directly for KPIs that cannot be auto-computed from system data (e.g., NPS score, customer satisfaction from survey, market share from external report).

ColumnTypeNotes
KPIManualId, KPIId, Periodint / dateUnique: (KPIId, Period, OUID, TenantId)
EnteredValue / Notesdecimal / nvarchar
EnteredBy / EnteredOn / ApprovedByint / datetimeApproval workflow — ApprovedBy populated on approval
Statussmallint0=Draft, 1=PendingApproval, 2=Approved, 3=Rejected
OUID / TenantIdint

MROLEKPI — Role-Based KPI Assignments

Maps which KPIs are relevant for which roles. Drives role-specific KPI dashboard tiles in ESS and manager views.

ColumnTypeNotes
RoleKPIId, RoleId, KPIIdintRoleId = MJOBROLE. Unique: (RoleId, KPIId, TenantId)
IsDefaultbitShown on dashboard without user customization
DisplayOrdersmallintCard sort order on dashboard
TenantIdintIndex: (RoleId, TenantId)

AnalysisQuery Designer

AnalysisQuery is GB5's ad-hoc query builder — a no-code dynamic SQL composer that allows analysts and power users to define custom analytical queries without writing SQL directly. Queries are defined once in MANALYSIS and executed on-demand.

Analysis Schema

TablePurposeKey Columns
MANALYSISAnalysis definition — the base query templateAnalysisId, Code, Name, BaseTable, JoinsJson (JSON array of JOIN clauses), ConditionsJson (available WHERE params), TenantId
MANALYSISCOLUMNAvailable columns for this analysisAnalysisColumnId, AnalysisId, ColumnAlias (safe whitelist key), ColumnExpression (actual SQL column/expression), DataType, IsFilterable, IsGroupable, DisplayLabel, SortOrder
MANALYSISFILTERSaved filter presets for an analysisFilterId, AnalysisId, FilterName, FilterJson, OwnerId, IsShared, TenantId
TANALYSISWORKSPACEUser-saved workspace configurations (see Workspace section)WorkspaceId, AnalysisId, OwnerId, ColumnConfigJson, FilterJson, SortJson, IsShared

AnalysisQuery Execution Flow

Load MANALYSIS + MANALYSISCOLUMN
whitelist of valid columns
→
Validate request columns
reject any not in whitelist
→
Build SELECT clause
from whitelisted ColumnExpressions only
→
Append JoinsJson + WHERE
parameterized conditions
→
DataSourceRuleEvaluator
selects OLTP / ReportDB
→
Execute via IQueryExecutor
StreamAsync for large results
SQL Injection Prevention in AnalysisQuery: Dynamic column selection is safe because ColumnAlias (from the request) is only used as a dictionary key to look up ColumnExpression from the server-side whitelist. User-supplied aliases are never interpolated into SQL. All WHERE conditions remain parameterized Dapper parameters — no user value ever appears in the SQL string.
// AnalysisQueryBLL.ExecuteAnalysisAsync — simplified safe column composition
var whitelist = await _dal.GetColumnsAsync(analysisId, login, ct);
// Validate: reject any request.Columns not in whitelist
var invalid = req.Columns.Where(c => !whitelist.ContainsKey(c)).ToList();
if (invalid.Any()) throw new ValidationException($"Invalid columns: {string.Join(',', invalid)}");

// Build SELECT using server-side expressions only — never req.Columns directly
var selectClauses = req.Columns.Select(c => $"{whitelist[c].ColumnExpression} AS [{c}]");
var sql = $"SELECT {string.Join(", ", selectClauses)} FROM {analysis.BaseTable} {analysis.JoinsJson}";

// Add WHERE params — all parameterized
if (req.Filters?.Any() == true)
    foreach (var f in req.Filters)
        sql += $" AND {whitelist[f.Column].ColumnExpression} {f.Operator} @{f.Column}";
sql += $" AND TenantId = @TenantId";  // mandatory tenant filter

Performance KPI Feed (PerfKPIFeed)

PerfKPIFeed is the analytics bridge for the Performance Management Platform — it ingests finalized appraisal scores and closed OKR cycles into the KPI analytics framework via Dapr pub/sub events. This keeps performance data current in the KPI dashboard without polling.

TPERFKPIFEED Schema

ColumnTypeNotes
PerfKPIFeedIdint PK
FeedSourcesmallint0=PERMAppraisal, 1=OKRCycle
SourceIdintAppraisalPlanId or OKRCycleId
PerioddatePerformance period end date
OUID / DeptIdint nullableOrg scope; null = company aggregate
MetricKeynvarchar(100)Identifies the specific metric: 'AVG_FINAL_SCORE', 'BELL_CURVE_DIST', 'OKR_COMPLETION_RATE', 'GOAL_ATTRITION_RATE'
MetricValuedecimal(18,4)Numeric value of the metric
MetricDetailJsonnvarchar(max) nullableOptional breakdown JSON: e.g., bell curve grade distribution array
IngestedOndatetimeDapr event processing timestamp
TenantIdintIndexes: (SourceId, FeedSource, TenantId), (TenantId, Period, MetricKey)

Dapr Subscribers

PerformanceKPISubscriber — subscribes to perm.score.finalized

// AnalyticsSL/Subscribers/PerformanceKPISubscriber.cs
[ApiController][Route("analytics/subscribe")]
public class PerformanceKPISubscriber : ControllerBase
{
    [HttpPost("perm-score-finalized")]
    [Topic("pubsub", "perm.score.finalized")]
    public async Task<IActionResult> OnPermScoreFinalized(
        [FromBody] PermScoreFinalizedEvent evt, CancellationToken ct)
    {
        // evt: { AppraisalPlanId, PeriodEnd, EmployeeCount, Scores, GradeDistribution, TenantId }
        await _perfKpiBll.IngestPermScoresAsync(evt, ct);
        return Ok();
    }

    [HttpPost("okr-cycle-closed")]
    [Topic("pubsub", "okr.cycle.closed")]
    public async Task<IActionResult> OnOkrCycleClosed(
        [FromBody] OkrCycleClosedEvent evt, CancellationToken ct)
    {
        // evt: { OKRCycleId, PeriodEnd, TotalGoals, CompletedGoals, AverageProgress, TenantId }
        await _perfKpiBll.IngestOkrCycleAsync(evt, ct);
        return Ok();
    }
}

PerfKPIFeedBLL — metric computation on ingest

// IngestPermScoresAsync — writes TPERFKPIFEED rows per metric per OUID
public async Task IngestPermScoresAsync(PermScoreFinalizedEvent evt, CancellationToken ct)
{
    // Aggregate: company-level + per-OU breakdown
    var metrics = new List<PerfKPIFeedDTO>();

    // AVG_FINAL_SCORE — average FinalPercentage across all employees in plan
    metrics.Add(new() { FeedSource=0, SourceId=evt.AppraisalPlanId, Period=evt.PeriodEnd,
        MetricKey="AVG_FINAL_SCORE", MetricValue=evt.Scores.Average(s => s.FinalPercentage) });

    // BELL_CURVE_DIST — store grade distribution as JSON detail
    metrics.Add(new() { FeedSource=0, SourceId=evt.AppraisalPlanId, Period=evt.PeriodEnd,
        MetricKey="BELL_CURVE_DIST",
        MetricValue=evt.GradeDistribution.Count,  // count of distinct grades
        MetricDetailJson=JsonSerializer.Serialize(evt.GradeDistribution) });

    // Per-OU: avg score per org unit
    foreach (var ou in evt.Scores.GroupBy(s => s.OUID))
        metrics.Add(new() { FeedSource=0, SourceId=evt.AppraisalPlanId, Period=evt.PeriodEnd,
            OUID=ou.Key, MetricKey="AVG_FINAL_SCORE", MetricValue=ou.Average(s => s.FinalPercentage) });

    await _dal.BulkInsertPerfKPIFeedAsync(metrics, login, ct);

    // Propagate to TKPILIST for KPI ID mapped to AVG_FINAL_SCORE
    await _kpiListBll.UpdateFromPerfFeedAsync(metrics, ct);
}

Dapr Topic Subscriptions (3 total)

TopicSource ModuleAnalytics ActionTPERFKPIFEED MetricKeys
perm.score.finalizedPERMIngest appraisal scores per org unitAVG_FINAL_SCORE, BELL_CURVE_DIST, HIGH_PERFORMER_PCT, PIP_TRIGGER_COUNT
perm.appraisal.finalizedPERMUpdate KPI period trackingAPPRAISAL_COMPLETION_RATE (finalized vs total in plan)
okr.cycle.closedOKRIngest OKR cycle summaryOKR_COMPLETION_RATE, GOAL_ATTRITION_RATE, AVG_FINAL_PROGRESS, AT_RISK_GOAL_PCT

API Endpoints Reference

Core Report Platform

MethodRouteCacheNotes
GET/CommonReport/ReportNOT_REQUIREDMain report endpoint; streams via IAsyncEnumerable
GET/CommonReport/GetMyReportsUSER_LEVEL 2 minAsync job inbox for completed reports
GET/CommonReport/GetPivotConfigCLIENT_LEVEL 10 minPivot column + row definitions from MREPORTVIEWFIELDS
POST/CommonReport/ExecuteAsyncJobnullQueues TSYSJOB for large async report generation
GET/CommonReport/DownloadReport/{jobId}NOT_REQUIREDStreams completed async report file (blob → response)

DataSource Management

MethodRouteCache
GET/Analytics/DataSource/GetDataSourceListCLIENT_LEVEL 15 min
POST/Analytics/DataSource/SaveDataSourcenull
GET/Analytics/DataSource/GetDataSourceRuleList {ReportViewId?}CLIENT_LEVEL 10 min
POST/Analytics/DataSource/SaveDataSourceRulenull
DELETE/Analytics/DataSource/DeleteDataSourceRule {RuleId}null

Workspace

MethodRouteCache
GET/Analytics/Workspace/GetWorkspaceList {AnalysisId}USER_LEVEL 5 min
GET/Analytics/Workspace/GetWorkspace {WorkspaceId}USER_LEVEL 5 min
POST/Analytics/Workspace/SaveWorkspacenull
DELETE/Analytics/Workspace/DeleteWorkspace {WorkspaceId}null

AnalysisQuery

MethodRouteCache
GET/Analysis/GetAnalysisListCLIENT_LEVEL 10 min
GET/Analysis/GetAnalysisColumns {AnalysisId}CLIENT_LEVEL 15 min — column whitelist
POST/Analysis/ExecuteNOT_REQUIRED — always live
POST/Analysis/ExportAnalysisnull — triggers async export if large
POST/Analysis/SaveAnalysisnull (admin only)
POST/Analysis/SaveAnalysisColumnnull (admin only — extends whitelist)

KPI Management

MethodRouteCache
GET/Analytics/KPI/GetKPIList {Category?, Status?}CLIENT_LEVEL 10 min
GET/Analytics/KPI/GetKPI {KPIId}CLIENT_LEVEL 10 min
POST/Analytics/KPI/SaveKPInull
DELETE/Analytics/KPI/DeleteKPI {KPIId}null
GET/Analytics/KPIList/GetKPIValues {KPIId, PeriodFrom, PeriodTo}NOT_REQUIRED
POST/Analytics/KPIList/SaveManualKPIEntrynull
POST/Analytics/KPIList/ApproveManualKPIEntry {KPIManualId}null
GET/Analytics/RoleKPI/GetRoleKPIList {RoleId}ROLE_LEVEL 15 min
POST/Analytics/RoleKPI/SaveRoleKPInull
DELETE/Analytics/RoleKPI/DeleteRoleKPI {RoleKPIId}null
GET/Analytics/KPI/GetKPIDashboard {RoleId?, OUID?, Period}NOT_REQUIRED — live KPI values

Performance KPI Feed

MethodRouteCache
GET/Analytics/PerfKPI/GetPerfKPIFeed {FeedSource, SourceId}CLIENT_LEVEL 5 min
GET/Analytics/PerfKPI/GetPerfKPITrend {MetricKey, TenantId, PeriodFrom, PeriodTo}NOT_REQUIRED — trend data
GET/Analytics/PerfKPI/GetOUPerformanceSummary {OUID, Period}CLIENT_LEVEL 5 min

Sample Scenarios

Scenario A — Standard Report with DataSource Routing

A Finance user runs "Inventory Valuation Report" for the past 3 years. MREPORTDATASOURCERULE has a rule: date range > 730 days → use Archive DB.
GET /CommonReport/Report?ReportViewId=501&DateFrom=2023-01-01&DateTo=2025-12-31&Format=Excel

// AsyncDecisionEngine checks row estimate: ~250,000 rows > threshold (100k)
// → Queues TSYSJOB (JobId=8821)
// Response: { "IsAsync": true, "JobId": 8821, "Message": "Report is being generated..." }

// User checks My Reports after 2 min:
GET /CommonReport/GetMyReports
// Response includes: { JobId: 8821, Status: 2 (Completed), FileUrl: "/report/8821" }

// DataSource resolution that happened internally:
// 1. Load MREPORTDATASOURCERULE for ReportViewId=501
// 2. Query date range = 1095 days > rule.DateRangeDays = 730
// 3. Selected DataSourceId = 3 (Archive DB)
// 4. Resolved connection string from Vault path "gb5/finance/archive-db"

Scenario B — AnalysisQuery Designer (Sales Dashboard)

// Step 1: Load available columns for Sales Analysis (AnalysisId: 12)
GET /Analysis/GetAnalysisColumns?AnalysisId=12
// Returns whitelist: OrderDate, CustomerName, ProductCode, Revenue, Qty, Region, SalesRep

// Step 2: Execute ad-hoc query — user selected columns + filter
POST /Analysis/Execute
{
  "AnalysisId": 12,
  "Columns": ["OrderDate", "Region", "Revenue", "Qty"],
  "Filters": [
    { "Column": "Region", "Operator": "=", "Value": "North" },
    { "Column": "OrderDate", "Operator": ">=", "Value": "2026-01-01" }
  ],
  "GroupBy": ["Region"],
  "AggregateColumns": { "Revenue": "SUM", "Qty": "SUM" },
  "SortColumn": "Revenue", "SortDirection": "DESC"
}
// Backend builds: SELECT Region, SUM([o].[Revenue]) AS Revenue, SUM([o].[Qty]) AS Qty
//   FROM [TORDER] o ... WHERE Region = @Region AND OrderDate >= @OrderDate AND TenantId = @TenantId
// Column expressions come from whitelist — user-supplied column names used only as keys

// Step 3: Save as workspace for reuse
POST /Analytics/Workspace/SaveWorkspace
{ "WorkspaceName": "North Region Q1 2026", "AnalysisId": 12, "IsShared": false,
  "ColumnConfigJson": "[{\"col\":\"Region\",...}, ...]",
  "FilterJson": "[{\"Column\":\"Region\",\"Value\":\"North\"}]" }

Scenario C — KPI Dashboard Setup for HR Role

KPICategoryUnitAggTargetDirectionFeedSource
Avg Appraisal ScorePeopleScore (0-100)AvgHigherIsBetterPERM Dapr event
OKR Completion RateOKR%AvgHigherIsBetterOKR Dapr event
Bell Curve (A-Grade %)People%LastValueTargetBand (10-20%)PERM Dapr event
PIP Trigger RatePeople%AvgLowerIsBetterPERM Dapr event
Employee NPSPeopleScore (-100 to 100)LastValueHigherIsBetterManual entry
Avg Appraisal Score
72.4
↑ 3.2 vs prev cycle
Target: 70.0
OKR Completion Rate
68%
↓ 4% vs prev cycle
Target: 75%
A-Grade Distribution
14.2%
✓ Within band 10-20%
Bell curve target band
PIP Trigger Rate
5.1%
↓ 1.3% improvement
Target: < 8%

Scenario D — PerfKPIFeed: Dapr Event → TPERFKPIFEED → KPI Dashboard

FinalizeAppraisal (PERM)
AppraisalPlanId: 301
→
Dapr: perm.score.finalized
Payload: EmployeeCount=120, Scores[], GradeDist[]
→
PerformanceKPISubscriber
AnalyticsSL receives event
→
PerfKPIFeedBLL.IngestPermScoresAsync
Computes 4 MetricKeys
→
TPERFKPIFEED + TKPILIST updated
KPI dashboard auto-refreshes
-- TPERFKPIFEED rows inserted for AppraisalPlanId=301, PeriodEnd=2026-03-31:
| FeedSource | SourceId | MetricKey           | MetricValue | MetricDetailJson        |
|------------|----------|---------------------|-------------|-------------------------|
| 0 (PERM)   | 301      | AVG_FINAL_SCORE     | 72.4        | NULL                    |
| 0 (PERM)   | 301      | BELL_CURVE_DIST     | 5           | [{"grade":"A","pct":14.2},{"grade":"B","pct":38.1},...]  |
| 0 (PERM)   | 301      | HIGH_PERFORMER_PCT  | 14.2        | NULL                    |
| 0 (PERM)   | 301      | PIP_TRIGGER_COUNT   | 6           | NULL                    |

-- TKPILIST updated for KPIId mapped to AVG_FINAL_SCORE:
| KPIId | Period     | ActualValue | TargetValue | Variance | SourceType | SourceRefId |
|-------|------------|-------------|-------------|----------|------------|-------------|
| 801   | 2026-03-31 | 72.4        | 70.0        | +2.4     | 1 (PERM)   | 301         |

Scenario E — ETL DataSync: GB4 Master Data Replication

// MSWSYNCRULE for MPRODUCT (delta sync — only rows modified in last N hours):
{
  "SyncRuleCode": "MPRODUCT_DELTA",
  "SourceDatabaseId": 1,       // GB4 OLTP SQL Server
  "TargetDatabaseId": 5,       // GB5 DataSync mirror
  "TargetTable": "MPRODUCT",
  "SyncMode": 1,               // Delta (0=Full)
  "DeltaColumn": "MODIFIEDON", // Incremental watermark column
  "BatchSize": 500,
  "Schedule": "*/15 * * * *",  // Every 15 min
  "IsActive": true
}

// On schedule fire — QuartzJob:
// 1. Read last successful sync watermark from TSWSYNCRUN
// 2. Extract: SELECT * FROM MPRODUCT WHERE MODIFIEDON > @lastWatermark
// 3. IAsyncEnumerable — 500 rows per batch
// 4. Transform: apply MSWOBJECTMAP column renames (GB4 COLNAME → GB5 ColumnName)
// 5. Load: BulkInsertAsync on new rows; ExecuteAsync UPDATE for existing
// 6. TSWSYNCRUN row: { TotalRows: 42, LoadedRows: 42, ErrorRows: 0, Status: 2 }

Scenario F — Pivot Report: Appraisal Bell Curve by Grade × Department

GET /CommonReport/Report?ReportViewId=721&AppraisalPlanId=301&IsPivotView=true&GroupBy=Grade&CrossBy=Department

// BE pivot engine returns:
// Columns: Grade | IT | Finance | HR | Sales | Total
// Row A    | 3   | 2  | 1  | 4   | 10
// Row B    | 12  | 8  | 5  | 13  | 38
// Row C    | 6   | 4  | 3  | 5   | 18
// Row D    | 1   | 1  | 0  | 2   | 4
// Total    | 22  | 15 | 9  | 24  | 70

// Same endpoint with Format=Excel → ClosedXML cross-tab xlsx with conditional formatting
// Same endpoint with Format=PDF → PuppeteerSharp rendered table with page header/footer

Caching Strategy

DataLevelTTLInvalidation
MREPORT / MREPORTVIEW metadataCLIENT_LEVEL15 minSaveReport (admin)
MREPORTVIEWFIELDS (column config)CLIENT_LEVEL15 minSaveReportViewField
MREPORTDATASOURCERULECLIENT_LEVEL10 minSave, Delete
MANALYSIS + MANALYSISCOLUMNCLIENT_LEVEL15 minSaveAnalysis, SaveAnalysisColumn
TANALYSISWORKSPACE list (user)USER_LEVEL5 minSave, Delete
Report execution resultsNOT_REQUIRED—Always live — report data changes constantly
MKPI master listCLIENT_LEVEL10 minSaveKPI, DeleteKPI
MROLEKPI per roleROLE_LEVEL15 minSaveRoleKPI, DeleteRoleKPI
TKPILIST valuesNOT_REQUIRED—Live — updated by Dapr events
TPERFKPIFEED feed entriesCLIENT_LEVEL5 minOn Dapr subscriber ingest
DataSync MSWSYNCRULE configsCLIENT_LEVEL10 minSave, Delete
ServerConfigCache (connection strings)In-process Singleton60sOn VaultSharp read error (auto-rotate)

Security & Multi-Tenancy

Multi-Tenancy Rules

DB-Level Isolation

LoginDTO.DatabaseName routes to tenant database. Analytics never has a WHERE TenantId filter on the same database — the database itself is the tenant boundary.

AnalysisQuery Scope

AnalysisQuery appends AND TenantId = @TenantId even within a single-tenant DB where AnalysisId is shared. Belt-and-suspenders approach for shared analytics frameworks.

Vault Connection Strings

Archive DB and Report DB connection strings stored in HashiCorp Vault. Connection string key stored in MREPORTDATASOURCE. Never in appsettings.json — even in dev.

Column Whitelist

AnalysisQuery never interpolates user input into SQL. User supplies column aliases only; server maps them to MANALYSISCOLUMN.ColumnExpression. Whitelist enforced server-side.

Role-Based Analytics Access

Endpoint GroupRequired RoleNotes
Standard reportsAny authenticated userMREPORTVIEW.RoleAccess filter applies
AnalysisQuery executeANALYST or ADMINMANALYSIS.RoleAccess validates at execution
SaveAnalysis / SaveAnalysisColumnADMIN onlyColumn whitelist modification — admin-gated
SaveDataSource / RulesSYSADMINVault keys exposed — strictest gate
KPI master CRUDHR_ADMIN or FINANCE_ADMINCategory-based access — HR for People KPIs, Finance for Financial KPIs
KPI manual entry approvalHR_MANAGER or FINANCE_MANAGERSegregated from entry (maker-checker)
GetKPIDashboardAny + role filterMROLEKPI filters dashboard tiles by role; no cross-role leak
PerfKPI feed endpointsHR_ADMIN / HR_MANAGERAggregate performance data — HR access only

Sensitive Data Rules