Skip to main content

Report Program

Report Programs in Protrak are C# classes that implement the IReportProgramAsync interface. They are used to generate custom reports by querying, aggregating, and formatting data from Protrak instances according to business requirements. These programs are invoked by the reporting engine to provide data for dashboards, widgets, and downloadable reports.

Coding guidelines

  • A Report Program must implement the IReportProgramAsync interface:

    public interface IReportProgramAsync
    {
    Task<ReportConfig> GetReportConfigAsync();
    Task<ReportData> GetReportDataAsync(ReportQuery reportQuery);
    }

    NOTE: Implement IReportProgramAsync with the RunAsync method to enable asynchronous, scalable trigger logic. Most internal service methods are now async, and their synchronous versions are deprecated and will be removed. Use RunAsync to ensure compatibility with these changes.

  • The main responsibilities of a Report Program are:

    • Define the report structure (columns, grouping, etc.) in GetReportConfigAsync().
    • Fetch and process data in GetReportDatAsync().
  • Use the provided InstanceService to query Protrak instances based on filters and attributes.

  • Always apply necessary attribute filters to avoid returning irrelevant or incomplete data.

  • Use DataTables to structure the report data, ensuring column names and types match the report configuration.

  • For cumulative or grouped reports, implement logic to aggregate data as required by the business scenario.

  • Avoid hardcoding attribute names and column headers; use constants or readonly fields for maintainability.

  • Handle nulls and missing data gracefully to prevent runtime exceptions.

  • If drill-down or detailed views are needed, provide additional methods to fetch and format this data.

Typical use cases

  • Generating year-wise, department-wise, or status-wise summary reports for business dashboards.
  • Producing cumulative or trend reports (e.g., cumulative disclosures submitted per year).
  • Providing drill-down data for interactive report widgets.
  • Custom aggregations or calculations that cannot be achieved using standard reporting configuration.
  • Exporting data for compliance, audit, or management review.

Anti-patterns

  • Do not use Report Programs for data mutation or side effects (e.g., updating instances, sending emails).
  • Avoid duplicating logic that can be handled by standard report configuration or simple SQL queries.
  • Do not fetch all data and filter in memory; always use attribute filters and queries to minimize data volume.
  • Avoid complex business logic unrelated to reporting; keep the focus on data retrieval and formatting.
  • Do not assume all attributes will be present or valid; always check for nulls and handle missing data.

Sample Code

1. Cumulative Year-wise Report

public class YearlyUserSignUpSummaryReport : IReportProgram
{
public IInstanceService InstanceService { get; set; }

private readonly string ATTRIBUTE_SIGNUP_DATE = "SignUpDate";
private readonly string COLUMN_YEAR = "Year";
private readonly string COLUMN_TOTAL_SIGNUPS = "Total Signups (Cumulative)";
private readonly string COLUMN_GROUP = "Group";
private readonly string COLUMN_USER_NAME = "UserName";
private readonly string COLUMN_USER_ID = "UserId";
private readonly string COLUMN_SIGNUP_COUNT = "SignUpCount";

public ReportConfig GetReportConfig()
{
return new ReportConfig()
{
Group = new ReportGroup()
{
Attribute = new LayoutField() { AttributeName = COLUMN_YEAR },
DisplayName = COLUMN_YEAR,
},
Columns = (new List<ReportColumn>()
{
new ReportColumn() {
DisplayName = "User Name",
FirstAttribute = new LayoutField() { AttributeName = COLUMN_USER_NAME},
ShowInDrillDown=true,
},
new ReportColumn() {
DisplayName = COLUMN_TOTAL_SIGNUPS,
FirstAttribute = new LayoutField() { AttributeName = COLUMN_SIGNUP_COUNT},
AggregateType = AggregateType.Sum
}
}).ToArray()
};
}

public ReportData GetReportData(ReportQuery reportQuery)
{
DataTable dt = GetDrillDownReportTable();
AddAttributeFilters(reportQuery);
GetDrillDownData(ref dt, reportQuery);
return new ReportData() { Data = dt };
}

// ... Helper methods for filtering, aggregation, and DataTable construction ...
}

2. Pivot Table-style Report (e.g., Department vs. Year Count)

public class DepartmentYearlyUserSignUpPivotReport : IReportProgram
{
public IInstanceService InstanceService { get; set; }
private readonly string ATTRIBUTE_DEPARTMENT = "Department";
private readonly string ATTRIBUTE_SIGNUP_DATE = "SignUpDate";
public ReportConfig GetReportConfig()
{
return new ReportConfig
{
Group = new ReportGroup { Attribute = new LayoutField { AttributeName = ATTRIBUTE_DEPARTMENT }, DisplayName = "Department" },
Columns = new[]
{
new ReportColumn { DisplayName = "Year", FirstAttribute = new LayoutField { AttributeName = "Year" } },
new ReportColumn { DisplayName = "Sign Up Count", FirstAttribute = new LayoutField { AttributeName = "Count" }, AggregateType = AggregateType.Sum }
}
};
}
public ReportData GetReportData(ReportQuery reportQuery)
{
var dt = new DataTable();
dt.Columns.Add("Department", typeof(string));
dt.Columns.Add("Year", typeof(int));
dt.Columns.Add("Count", typeof(int));
// Query and group by department and year
var instanceQuery = new InstanceQuery
{
InstanceTypeName = reportQuery.InstanceTypeName,
AttributeFilterExpressions = reportQuery.AttributeFilterExpressions,
StateFilter = reportQuery.StateFilter,
Take = int.MaxValue,
Attributes = new[] { ATTRIBUTE_DEPARTMENT, ATTRIBUTE_SIGNUP_DATE }
};
var pagedInstances = InstanceService.GetInstances(instanceQuery);
var grouped = pagedInstances.Items
.Where(i => i.Attributes.Any(a => a.Name == ATTRIBUTE_DEPARTMENT) && i.Attributes.Any(a => a.Name == ATTRIBUTE_SIGNUP_DATE && a.DateValue != null))
.GroupBy(i => new {
Department = i.Attributes.First(a => a.Name == ATTRIBUTE_DEPARTMENT).Value,
Year = i.Attributes.First(a => a.Name == ATTRIBUTE_SIGNUP_DATE).DateValue.Value.Year
})
.Select(g => new { g.Key.Department, g.Key.Year, Count = g.Count() });
foreach (var row in grouped)
{
dt.Rows.Add(row.Department, row.Year, row.Count);
}
return new ReportData { Data = dt };
}
}

3. State-wise Task Delay and Performance Report

This pattern is useful for workflow or process tracking, where you want to analyze how long tasks spend in each state, compare actual durations to expected durations, and identify bottlenecks.

public class TaskStateDelayPerformanceReport : IReportProgram
{
public IInstanceService InstanceService { get; set; }
private readonly string ATTRIBUTE_TASK_ID = "TaskId";
private readonly string COLUMN_STATE = "State";
private readonly string COLUMN_EXPECTED_DAYS = "Expected Days";
private readonly string COLUMN_AVG_DELAY = "Average Delay (Days)";
private readonly string COLUMN_TASK_COUNT = "Task Count";

// Example: expected durations for each state
private static readonly Dictionary<string, TimeSpan> StateExpectedDurations = new Dictionary<string, TimeSpan> {
{ "To Do", TimeSpan.FromDays(2) },
{ "In Progress", TimeSpan.FromDays(5) },
{ "Review", TimeSpan.FromDays(3) },
{ "Done", TimeSpan.FromDays(0) }
};

public ReportConfig GetReportConfig()
{
return new ReportConfig
{
Group = new ReportGroup { Attribute = new LayoutField { AttributeName = COLUMN_STATE }, DisplayName = "State" },
Columns = new[]
{
new ReportColumn { DisplayName = COLUMN_EXPECTED_DAYS, FirstAttribute = new LayoutField { AttributeName = COLUMN_EXPECTED_DAYS }, AggregateType = AggregateType.Average },
new ReportColumn { DisplayName = COLUMN_AVG_DELAY, FirstAttribute = new LayoutField { AttributeName = COLUMN_AVG_DELAY }, AggregateType = AggregateType.Average },
new ReportColumn { DisplayName = COLUMN_TASK_COUNT, AggregateType = AggregateType.Count }
}
};
}
public ReportData GetReportData(ReportQuery reportQuery)
{
var dt = new DataTable();
dt.Columns.Add(COLUMN_STATE, typeof(string));
dt.Columns.Add(COLUMN_EXPECTED_DAYS, typeof(double));
dt.Columns.Add(COLUMN_AVG_DELAY, typeof(double));
dt.Columns.Add(COLUMN_TASK_COUNT, typeof(int));
// Query tasks and their activities (state transitions)
var instanceQuery = new InstanceQuery
{
InstanceTypeName = reportQuery.InstanceTypeName,
AttributeFilterExpressions = reportQuery.AttributeFilterExpressions,
StateFilter = reportQuery.StateFilter,
ActivityQuery = new ActivityQuery { Type = ActivityType.ActionPromoted, Take = int.MaxValue },
Take = int.MaxValue,
Attributes = new[] { ATTRIBUTE_TASK_ID }
};
var instances = InstanceService.GetInstances(instanceQuery);
var allActivities = instances.Items.SelectMany(i => i.Activities.Select(a => new {
TaskId = i.Id,
State = !string.IsNullOrWhiteSpace(a.ToState.DisplayName) ? a.ToState.DisplayName : a.ToState.Name,
Duration = (a.EndTime - a.StartTime).TotalDays
}));
var grouped = allActivities.GroupBy(a => a.State);
foreach (var group in grouped)
{
var expected = StateExpectedDurations.ContainsKey(group.Key) ? StateExpectedDurations[group.Key].TotalDays : 0;
var avgDelay = group.Average(a => Math.Max(0, a.Duration - expected));
dt.Rows.Add(group.Key, expected, Math.Round(avgDelay, 2), group.Count());
}
return new ReportData { Data = dt };
}
}

This pattern is ideal for process improvement dashboards, SLA monitoring, or identifying workflow bottlenecks by comparing actual vs. expected durations for each state.

4. Generic Feedback Report Using Query Builder

This example demonstrates a generic feedback report using the Query Builder service. It shows how to fetch related feedback data for entities (such as employees, products, or services) and flatten it for reporting.

using Prorigo.Protrak.API.Contracts;
using Prorigo.Protrak.API.Contracts.Enum;
using Prorigo.Protrak.API.Contracts.Filters;
using Prorigo.Protrak.API.Contracts.Layout;
using Prorigo.Protrak.API.Services;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;

public class GenericFeedbackReportProgram : IReportProgram
{
public IQueryBuilderService QueryBuilderService { get; set; }

private readonly string GROUP_ID = "EntityId";
private readonly string ENTITY_NAME = "EntityName";
private readonly string FEEDBACK_TYPE = "Feedback";
private readonly string FEEDBACK_PARAMETER = "FeedbackParameter";
private readonly string FEEDBACK_COMMENT = "FeedbackComment";
private readonly string RELATION_TO_FEEDBACK = "EntityToFeedback";
private readonly string TYPE_ENTITY = "Entity";
private readonly string TYPE_FEEDBACK = "Feedback";

public ReportConfig GetReportConfig()
{
return new ReportConfig
{
Group = new ReportGroup { Attribute = new LayoutField { AttributeName = GROUP_ID }, DisplayName = "Entity ID", ShowInDrillDown = true },
RenderAs = PivotReportDisplayType.FlatTabular,
Columns = new[]
{
new ReportColumn { DisplayName = ENTITY_NAME, FirstAttribute = new LayoutField { AttributeName = ENTITY_NAME, AttributeType = AttributeType.Text } },
new ReportColumn { DisplayName = FEEDBACK_PARAMETER, FirstAttribute = new LayoutField { AttributeName = FEEDBACK_PARAMETER, AttributeType = AttributeType.Picklist } },
new ReportColumn { DisplayName = FEEDBACK_COMMENT, FirstAttribute = new LayoutField { AttributeName = FEEDBACK_COMMENT, AttributeType = AttributeType.Text } }
}
};
}

public ReportData GetReportData(ReportQuery reportQuery)
{
var dt = CreateReportTableStructure();
PopulateReportRows(reportQuery, dt);
return new ReportData { Data = dt };
}

private DataTable CreateReportTableStructure()
{
var dt = new DataTable();
dt.Columns.Add(GROUP_ID, typeof(string));
dt.Columns.Add(ENTITY_NAME, typeof(string));
dt.Columns.Add(FEEDBACK_PARAMETER, typeof(string));
dt.Columns.Add(FEEDBACK_COMMENT, typeof(string));
return dt;
}

private void PopulateReportRows(ReportQuery reportQuery, DataTable dt)
{
var instances = GetAllEntitiesWithFeedback(reportQuery).GetAwaiter().GetResult();
if (instances?.Any() != true) return;
foreach (var instance in instances)
{
var entityId = instance.Id.ToString();
var entityName = GetAttribute(instance, ENTITY_NAME)?.TextValue ?? "";
if (!instance.RelatedItems.TryGetValue(FEEDBACK_TYPE, out var feedback) || feedback?.Items == null) continue;
foreach (var rel in feedback.Items)
{
var param = GetAttribute(rel, FEEDBACK_PARAMETER)?.ArrayValue?.FirstOrDefault() ?? "";
var comment = GetAttribute(rel, FEEDBACK_COMMENT)?.TextValue ?? "";
var row = dt.NewRow();
row[GROUP_ID] = entityId;
row[ENTITY_NAME] = entityName;
row[FEEDBACK_PARAMETER] = param;
row[FEEDBACK_COMMENT] = comment;
dt.Rows.Add(row);
}
}
}

private async Task<List<Instance>> GetAllEntitiesWithFeedback(ReportQuery reportQuery)
{
var instanceQueryDefinition = new InstanceQueryDefinition
{
InstanceQuery = new InstanceQuery
{
InstanceTypeName = TYPE_ENTITY,
Attributes = new[] { ENTITY_NAME },
Skip = 0,
Take = 500
},
RelatedInstanceQueryDefinitions = new[]
{
new RelatedInstanceQueryDefinition
{
RelatedInstanceQuery = new RelatedInstanceQuery
{
RelationFilters = new[]
{
new RelationFilter
{
RelationTypeName = RELATION_TO_FEEDBACK,
RelationDirection = RelationDirection.To,
TypeName = TYPE_FEEDBACK
}
},
Attributes = new[] { FEEDBACK_PARAMETER, FEEDBACK_COMMENT },
Skip = 0,
Take = 1000
},
RelatedInstanceQueryDefinitions = Array.Empty<RelatedInstanceQueryDefinition>()
}
}
};
var result = await QueryBuilderService.GetQueryDefinitionResultAsync(instanceQueryDefinition);
return result?.Items?.ToList() ?? new List<Instance>();
}

private static Attribute GetAttribute(Instance instance, string attributeName)
{
return instance.Attributes?.FirstOrDefault(attr => attr.Name == attributeName);
}
private static Attribute GetAttribute(RelatedInstance instance, string attributeName)
{
return instance.Attributes?.FirstOrDefault(attr => attr.Name == attributeName);
}
}

This pattern is useful for any scenario where you need to join a main entity with related feedback or comments, such as product reviews, service feedback, or survey responses.

Note: The Run method (from IReportProgram) should only be used if the trigger logic is fully synchronous, does not involve any service/API calls, and consists only lightweight in-memory operations.

Summary:
Report Programs are the recommended way to implement custom reporting logic in Protrak. They provide flexibility for complex data aggregation and formatting, while ensuring maintainability and performance through proper use of filters and service APIs. Always follow the coding guidelines and avoid anti-patterns to ensure robust and efficient report implementations.