Skip to content

Indicators Dashboard Documentation

Overview

The Indicators Dashboard provides a comprehensive view of programme performance across your organisation. It displays real-time statistics on compliance and success indicators, session attendance, and element progress for entrepreneurs enrolled in programmes.

This documentation is divided into two sections:

  • General Overview: For administrators, clients, and dashboard users
  • Developer Documentation: For technical staff maintaining or extending the system

General Overview

What is the Indicators Dashboard?

The Indicators Dashboard is a performance monitoring tool that shows how well entrepreneurs and programmes are performing against set indicators. The dashboard presents aggregated statistics and detailed views to help you track progress and identify areas needing attention.

There are two levels of dashboard available:

  1. Portfolio Dashboard: Shows data across all programmes within a portfolio
  2. Cluster (Parent Group) Dashboard: Shows data across all programmes within a specific cluster or parent group

Access & Permissions

To access the dashboards, you need specific permissions:

  • Portfolio Dashboard: Requires "View tenant portfolio dashboard" permission
  • Cluster Dashboard: Requires "View parent group dashboard" permission

Your system administrator can grant these permissions based on your role and responsibilities.

Understanding What You See

Which Programmes Appear

The dashboard only displays programmes that meet all of the following criteria:

  1. The programme has at least one published indicator (compliance or success)
  2. The programme has enrolled entrepreneurs with programme seats
  3. The programme's indicators match your dashboard level and organisational scope
  4. The programme is linked to a tenant in your portfolio or cluster

Which Entrepreneurs Appear

Entrepreneurs appear on the dashboard when:

  1. They have a programme seat
  2. Their programme seat's contract start date has passed (not in the future)
  3. Their programme seat falls within your portfolio or cluster tenants
  4. The programme they're enrolled in has published indicators

Dashboard Level Filtering

The data you see depends on your dashboard level:

Portfolio Level Dashboard:

  • Shows only portfolio-level indicators
  • Data aggregates across all clusters within the portfolio

Cluster Level Dashboard:

  • Shows both cluster-level indicators (assigned to your cluster) AND portfolio-level indicators (assigned to your portfolio)
  • Provides a more granular view of specific cluster performance

Statistics & Calculations

The dashboard displays several key metrics:

Overall Compliance

Shows the percentage of compliance indicators that have been achieved across all entrepreneurs on all programmes as filtered on the dashboard.

Calculation: (Number of achieved compliance tasks / Total compliance tasks that are due) × 100

Only includes tasks where:

  • The due date has passed, OR
  • The task has been completed early

This is to ensure that we are not measuring the performance of indicator tasks that are not yet due.

Overall Success

Shows the percentage of success indicators that have been achieved across all entrepreneurs.

Calculation: (Number of achieved success tasks / Total success tasks that are due) × 100

Uses the same task filtering rules as compliance.

Element Progress

Shows the average progress percentage across all entrepreneurs for completing programme elements.

Calculation: Average of each entrepreneur's latest element progress snapshot

Important: This uses snapshot data that updates daily (see Daily Data Updates below).

Note: If there are multiple cohorts present on the dashboard, the element progress will average across all cohorts and may be less meaningful, since it represents entrepreneurs at different stages of their programmes.

Learning Session Attendance

Shows the average attendance percentage for learning sessions across all entrepreneurs.

Calculation: Average of each entrepreneur's latest learning session attendance snapshot.

Note: This is the attendance to date on each entrepreneur's programme.

Mentor Session Attendance

Shows the average attendance percentage for mentoring sessions across all entrepreneurs.

Calculation: Average of each entrepreneur's latest mentoring session attendance snapshot.

Note: This is the attendance to date on each entrepreneur's programme.

Daily Data Updates

Element progress and session attendance statistics are based on snapshots taken automatically each night:

  • Element Progress Snapshots: Update daily at 02:00 (Africa/Johannesburg timezone)

    • Command: update:element_compliance_reporting
    • Calculates progress for all programme elements
  • Session Attendance Snapshots: Update daily at 02:30 (Africa/Johannesburg timezone)

    • Command: update:session_attendance_snapshots
    • Calculates attendance percentages for learning and mentoring sessions

This means that element progress and attendance statistics reflect data as of the previous night. Any changes made during the current day will appear on the dashboard the following day.

Understanding Data Delays

The dashboard uses caching to ensure fast loading times:

  • Dashboard data is cached for 15-30 minutes depending on the data type
  • If underlying data changes (e.g., a new submission is made, an indicator is updated), it may take up to 30 minutes to reflect on the dashboard
  • Changing filters clears the cache immediately, so filtered views always show current data

If you've made changes and want to see them reflected immediately, try changing a filter. The reason for the caching is because of the vast quantity of data that is being loaded and computed, so caching attempts to reduce the load on the system of calculating the same statistics over and over again.

Common Scenarios Explained

Why am I not seeing a programme?

If a programme doesn't appear on your dashboard, it could be because:

  1. No Published Indicators: The programme has no published compliance or success indicators
  2. No Enrolled Entrepreneurs: The programme has no entrepreneurs with programme seats in your portfolio/cluster
  3. Dashboard Level Mismatch: The programme's indicators don't match your dashboard level
    • Example: You're viewing the Portfolio dashboard, but the programme only has cluster-level indicators
  4. Scope Mismatch: The programme belongs to a different portfolio or cluster than yours

Why is an entrepreneur not showing?

Entrepreneurs might be missing from the dashboard because:

  1. No Programme Seat: The person doesn't have a programme seat for any programme with indicators
  2. Future Contract Start Date: The programme seat's contract start date is in the future
  3. Wrong Portfolio/Cluster: The programme seat is not within your portfolio or cluster scope
  4. Inactive Seat: The programme seat has been marked as inactive

Why are some indicators not visible?

Indicators might not appear because:

  1. Not Published: Only published indicators appear on the dashboard. Unpublished or draft indicators are excluded
  2. Portfolio/Cluster Assignment: The indicator is assigned to a different portfolio or cluster than yours
  3. Dashboard Level Mismatch:
    • You're on the portfolio dashboard but the indicator is assigned at the cluster level
    • The indicator is for a different portfolio than the one you're viewing
  4. No Programme Association: The indicator is not linked to any programmes in your scope

Why do statistics show "N/A" or empty values?

You might see "N/A" or empty statistics when:

  1. No Due Tasks: No indicator tasks are due yet for the entrepreneurs
    • Tasks only count once their due date has passed or they're completed early
  2. No Submissions: No submissions exist for the indicators
  3. No Snapshot Data: No daily snapshot data is available for attendance or element progress
    • This can happen for newly enrolled entrepreneurs before the first nightly update
  4. All Excluded: All relevant entrepreneurs were excluded from the calculation
    • For example, all seats lack valid snapshots for element progress

Understanding "Due in Month X"

When you see an indicator showing "Due in Month X", this means:

  • The indicator is scheduled to become active in a future programme month
  • It will appear in statistics once the entrepreneur's programme reaches that month
  • The month number is calculated from the entrepreneur's contract start date

This is common for success indicators that are only relevant at specific points in the programme (e.g., "Business Plan Completed" might be due in month 6).

Filtering Your View

The dashboard provides several filters to help you focus on specific data:

  • Regions: Filter by delivery location/region
  • Tenants: Filter by specific tenants
  • Programmes: Show data for specific programmes only
  • Cohorts: Focus on entrepreneurs from specific cohort start dates

Tip: Applying filters will clear the cache and show you the most current data for your selection.

Programme Drill-Down View

Clicking on a programme in the main dashboard table takes you to a detailed programme view where you can:

  • Select specific cohorts within the programme
  • View compliance and success indicators for that cohort
  • See individual entrepreneur performance
  • Access entrepreneur-specific indicator dashboards

This detailed view helps you identify which entrepreneurs need additional support and which indicators are performing well.


Developer Documentation

Architecture Overview

The Indicators Dashboard is built using a service-oriented architecture with three main layers:

Core Components

  1. Dashboard Pages (Filament Pages)

    • BaseIndicatorsDashboard: Shared dashboard functionality and widget configuration
    • ClusterIndicatorsDashboard: Cluster (parent group) level dashboard
    • PortfolioIndicatorsDashboard: Portfolio level dashboard
    • IndicatorsDashboardProgrammeView: Detailed programme view with cohort filtering
  2. Service Layer

    • DashboardContextService: Manages data scoping, filtering, and aggregation for the main dashboard
    • DashboardCalculatorService: Performs all statistical calculations
    • ProgrammeStatsService: Handles programme-specific and cohort-specific statistics
  3. Widgets (Filament Widgets)

    • GlobalOverallComplianceWidget: Overall compliance percentage
    • GlobalOverallSuccessWidget: Overall success percentage
    • GlobalElementProgressWidget: Average element progress
    • GlobalLearningSessionWidget: Learning session attendance
    • GlobalMentorSessionWidget: Mentor session attendance
    • GlobalProgrammesTableWidget: Programme listing with statistics

Data Flow Diagrams

The dashboard system consists of several interconnected flows. The following diagrams illustrate each component of the system in detail.

Main Dashboard Flow

This diagram shows the complete lifecycle of a dashboard request, from initial user access through permission checks, context initialization, data scoping, statistical calculations, and final widget display. This represents the primary dashboard page that users see when accessing the Indicators Dashboard.

The high-level flow:

  1. User accesses dashboard → Permission check
  2. Dashboard level determined from session (voy_dashboard)
  3. Filters retrieved from session
  4. DashboardContextService initializes with scoping and caching
  5. Seat query built with eager loading
  6. Statistics calculated via DashboardCalculatorService
  7. Results displayed in widgets
  8. User can click a programme to drill down

Programme Detail View Flow

When users click on a specific programme from the main dashboard, they are taken to a detailed view where they can filter by cohort, view programme-specific indicators, and see individual entrepreneur performance. This diagram illustrates the data flow for this drill-down functionality.

Key features:

  1. Programme-specific scoping with ProgrammeStatsService
  2. Cohort filtering with reactive updates
  3. Dynamic indicator processing (averaging vs achievement types)
  4. Individual entrepreneur status calculations
  5. Navigation to entrepreneur-specific dashboards

Scheduled Data Jobs

The dashboard relies on nightly scheduled jobs to generate snapshot data for element progress and session attendance. This diagram shows how these jobs run and populate the snapshot tables that feed into dashboard calculations.

Important notes:

  1. Element progress snapshots update at 02:00 AM
  2. Session attendance snapshots update at 02:30 AM
  3. Both jobs run in Africa/Johannesburg timezone
  4. Snapshot data represents the previous day's calculations
  5. Dashboard queries these snapshots for performance

Cache Strategy

To ensure fast loading times, the dashboard implements a comprehensive caching strategy. This diagram illustrates how cache keys are generated, when cache is invalidated, and how the system balances performance with data freshness.

Cache characteristics:

  1. Multi-level caching with different TTLs
  2. Automatic invalidation on filter changes
  3. Event-driven invalidation on data updates
  4. Unique cache keys per user/tenant/filter combination
  5. Tagged cache for bulk invalidation

Data Scoping Logic

Seat Filtering

All dashboard data starts with OrganisationProgrammeSeat models that are filtered by:

Portfolio Level (DashboardContextService::applyDashboardLevelScope):

$query->tenantsOfCurrentTenantPortfolio(restrict_landlord: true);

Cluster Level:

$query->tenantsOfCurrentTenantCluster(useFilterValue: false, restrict_landlord: true);

Programme Indicator Filtering

Only programmes with published indicators are included (DashboardContextService::applyProgrammeIndicatorsScope):

$query->where(function ($q) {
    $q->whereHas('programme.publishedIndicatorSuccesses')
      ->orWhereHas('programme.publishedIndicatorCompliances');
});

Eager Loading Strategy

To prevent N+1 queries, the context service eagerly loads all required relationships:

->with([
    'programme:id,title,period',
    'tenant:id,name,tenant_cluster_id',
    'organisation:id,session_delivery_location_id',
    'organisation.sessionDeliveryLocation:id,name',
    'user:id,name,email',
    'indicatorTasks' => function ($query) { /* ... */ },
    'indicatorTasks.indicatable',
    'sessionAttendanceSnapshots:id,type,attendance_percentage,...',
])

Reference: DashboardContextService::initialize() (lines 108-157)

Task Relevance Rules

Not all indicator tasks are included in dashboard calculations. Tasks must meet specific criteria:

Due Status Filter

Only tasks that are:

  • Already due: due_date <= now(), OR
  • Completed early: status === COMPLETED

This is implemented in DashboardCalculatorService::filterTasksByDueStatus():

return $tasks->filter(function ($task) {
    return ($task->due_date && $task->due_date <= now())
        || $task->status === IndicatorTaskStatusEnum::COMPLETED;
});

Level-Based Filtering

Tasks are filtered based on dashboard level and indicator assignment:

Portfolio Level:

  • Only portfolio-level indicators
  • Must match current portfolio ID

Cluster Level:

  • Cluster-level indicators matching current cluster ID, OR
  • Portfolio-level indicators matching current portfolio ID

Reference: DashboardCalculatorService::filterTasksByLevel() (lines 214-233)

Latest Task Per Indicator

When multiple tasks exist for the same indicator (e.g., monthly submissions), only the latest is considered:

return $tasks
    ->sortByDesc('due_date')
    ->groupBy(function ($task) {
        return $task->indicatable_type.'|'.$task->indicatable_id;
    })
    ->map(function ($group) {
        return $group->first();
    })
    ->values();

Reference: DashboardCalculatorService::getLatestTaskPerIndicator() (lines 315-326)

Calculation Methods

All calculations are performed by DashboardCalculatorService:

Element Progress

public function calculateElementProgressValue(Collection $seats): ?float
  • Gets latest snapshot for each seat via IndicatorElementProgressService
  • Averages only seats with valid snapshots (null snapshots are excluded)
  • Returns null if no valid snapshots exist

Reference: Lines 31-50

Session Attendance

public function calculateAttendanceValue(Collection $seats, SessionCategoryType $type): ?float
  • Filters sessionAttendanceSnapshots by type (LEARNING or MENTORING)
  • Gets latest snapshot for each seat (sorted by contract_start_date)
  • Averages only seats with non-null attendance percentages
  • Returns null if no valid snapshots exist

Reference: Lines 62-94

Compliance Percentage

public function calculateOverallCompliancePercentage(Collection $seats, string $dashboardLevel): ?float
  • For each seat, gets relevant compliance tasks
  • Counts total tasks and achieved tasks (is_achieved === true)
  • Returns (achievedTasks / totalTasks) × 100
  • Returns null if no tasks found

Reference: Lines 106-122

Success Percentage

public function calculateOverallSuccessPercentage(Collection $seats, string $dashboardLevel): ?float
  • Identical logic to compliance percentage, but for success indicators
  • Returns null if no tasks found

Reference: Lines 134-150

Per-Seat Status

public function getStatusForSeat(OrganisationProgrammeSeat $seat, Collection $indicators, Collection $dueMonths, int $currentMonth): array
  • Checks each indicator to see if it's due (based on first due month)
  • Gets latest submission task for due indicators
  • Returns ['achieved' => count, 'total' => count]

Reference: Lines 369-396

Caching Strategy

The dashboard implements multi-level caching for performance:

Cache Tags

All cache entries use the tag: indicators_dashboard

This allows bulk invalidation:

Cache::tags(['indicators_dashboard'])->flush();

Cache Keys

Cache keys are constructed to be unique per context:

private function cacheKey(string $suffix): string
{
    $userId = $this->user?->id ?? 0;
    $tenantId = app()->bound('currentTenant') ? app('currentTenant')->id : 0;
    $filtersHash = md5(json_encode($this->normalizeFilters($this->filters)));

    return "indicators:dash:{$userId}:{$tenantId}:{$this->dashboardLevel}:{$filtersHash}:{$suffix}";
}

Reference: DashboardContextService::cacheKey() (lines 98-105)

Cache TTL (Time To Live)

Different data types have different cache durations:

  • Scoped seats: 30 minutes
  • Programme/tenant/cohort options: 30 minutes
  • Dashboard statistics (compliance, success, attendance): 15 minutes
  • Element progress: 5 minutes (more frequently changing)
  • Programme due months: 30 minutes

Reference: Various Cache::remember() calls throughout DashboardContextService

Cache Invalidation

Cache is cleared when:

  1. Filters Change: Automatic via updatedFilters() method

    public function updatedFilters(): void
    {
        DashboardContextService::clearCache();
        // ...
    }
    
  2. Manual Clear: Via static method

    DashboardContextService::clearCache();
    ProgrammeStatsService::clearCache();
    
  3. Indicator Submission: When new submissions are created (via event listener)

Cache Key Normalization

Filters are normalized to prevent duplicate cache entries:

private function normalizeFilters(array $filters): array
{
    return array_filter($filters, fn ($value) => !empty($value));
}

This removes empty values to ensure consistent cache keys regardless of empty filter states.

Edge Cases & Gotchas

1. Null Snapshots in Averages

Issue: Seats without snapshots could skew averages

Solution: Seats with null snapshots are excluded from average calculations (not counted as 0%)

if ($snapshot->attendance_percentage === null) {
    continue; // Skip, don't count as 0
}

Impact: Averages represent only entrepreneurs with data, not all enrolled entrepreneurs

2. Future Due Dates

Issue: Including future tasks would show inaccurate achievement rates

Solution: Tasks are only included if due_date <= now() OR status === COMPLETED

Impact: Future scheduled indicators don't affect current statistics

3. Multiple Tasks Per Indicator

Issue: Monthly indicators create multiple tasks per entrepreneur

Solution: Only the latest task (by due_date) is used for calculations

Impact: Historical performance isn't double-counted

4. Dashboard Level Persistence

Issue: Dashboard level must persist across page loads

Solution: Stored in session with key voy_dashboard

session(['voy_dashboard' => 'cluster']);
// or
session(['voy_dashboard' => 'portfolio']);

Impact: Each dashboard mount sets the session value; navigation determines which dashboard displays

5. Filter Session Storage

Issue: Filters must persist across refreshes but be dashboard-specific

Solution: Stored in session using hashed class name as key

$livewire = md5(PortfolioIndicatorsDashboard::class);
session()->get("{$livewire}_filters", []);

Impact: Portfolio and cluster dashboards maintain separate filter states

6. Indicator Level Mismatches

Issue: Users might not see expected indicators due to level/assignment mismatch

Solution: Strict filtering by level and portfolio/cluster ID

  • Portfolio dashboard: Only shows portfolio-level indicators for current portfolio
  • Cluster dashboard: Shows cluster-level for current cluster + portfolio-level for current portfolio

Impact: Indicators must be correctly assigned to be visible

7. Published vs Unpublished Indicators

Issue: Draft indicators shouldn't appear on dashboard

Solution: Only published indicators are included in programme filtering

->whereHas('programme.publishedIndicatorSuccesses')
->orWhereHas('programme.publishedIndicatorCompliances')

Impact: Unpublishing an indicator removes it from dashboard immediately (after cache expires)

8. Cache Key Filter Sensitivity

Issue: Filter order could create duplicate cache entries

Solution: Filters are normalized before hashing

$filtersHash = md5(json_encode($this->normalizeFilters($this->filters)));

Impact: ['programmes' => [1,2]] and ['programmes' => [2,1]] produce the same cache key

Scheduled Jobs

The dashboard relies on two nightly scheduled jobs for snapshot data:

Element Compliance Reporting

  • Command: update:element_compliance_reporting
  • Schedule: Daily at 02:00 (Africa/Johannesburg timezone)
  • Function: Calculates element progress for all programme seats
  • Database: Truncates and rebuilds element_compliance_reporting table
  • Implementation: app/Console/Commands/UpdateElementComplianceReporting.php

What it does:

  1. Iterates through all users with programme seats
  2. Calculates progress for each catalogue element
  3. Stores snapshot of completion percentage
  4. Used by IndicatorElementProgressService::getLatestSnapshotProgress()

Session Attendance Snapshots

  • Command: update:session_attendance_snapshots
  • Schedule: Daily at 02:30 (Africa/Johannesburg timezone)
  • Function: Calculates session attendance for learning and mentoring sessions
  • Database: Truncates and rebuilds session_attendance_snapshots table
  • Implementation: app/Console/Commands/UpdateSessionAttendanceSnapshots.php

What it does:

  1. Iterates through all users with programme seats
  2. Calculates attendance percentage for LEARNING and MENTORING sessions separately
  3. Stores attendance stats (attended, missed, not marked, total)
  4. Creates one snapshot per seat per session type

Kernel Configuration (app/Console/Kernel.php):

$schedule->command('update:element_compliance_reporting')->daily()->at('02:00');
$schedule->command('update:session_attendance_snapshots')->daily()->at('02:30');

Reference: Lines 53-54

Testing Considerations

Session-Based Context

Dashboard level is session-based, so tests must set the session:

session(['voy_dashboard' => 'portfolio']);
// or
session(['voy_dashboard' => 'cluster']);

Filter Session Keys

Filters are stored using hashed class names:

$livewire = md5(PortfolioIndicatorsDashboard::class);
session(["{$livewire}_filters" => ['programmes' => [1, 2]]]);

Mocking Snapshots

For element progress and attendance tests, create snapshots:

SessionAttendanceSnapshot::factory()->create([
    'type' => SessionCategoryType::LEARNING,
    'organisation_programme_seat_id' => $seat->id,
    'attendance_percentage' => 85.5,
]);

Cache Considerations

Tests should either:

  1. Clear cache between tests: Cache::tags(['indicators_dashboard'])->flush()
  2. Use different cache keys (different users/tenants)
  3. Mock the cache layer

Required Relationships

Dashboard queries expect specific relationships to exist:

  • Programme must have published indicators
  • Seats must have valid contract_start_date
  • Indicators must have valid level and portfolio/cluster assignments
  • Tasks must have valid due_date or completed status

Database Factories

Use the existing factories with proper states:

OrganisationProgrammeSeat::factory()
    ->for($programme)
    ->for($user)
    ->create([
        'contract_start_date' => now()->subMonths(3),
    ]);

IndicatorCompliance::factory()
    ->published()
    ->portfolioLevel()
    ->create();

Key Files Reference

Dashboard Pages

  • app/Filament/Admin/Pages/BaseIndicatorsDashboard.php - Base dashboard with filters and widgets
  • app/Filament/Admin/Pages/ClusterIndicatorsDashboard.php - Cluster level implementation
  • app/Filament/Admin/Pages/PortfolioIndicatorsDashboard.php - Portfolio level implementation
  • app/Filament/Admin/Pages/IndicatorsDashboardProgrammeView.php - Programme drill-down view

Services

  • app/Services/Indicators/AdminDashboard/DashboardContextService.php - Data scoping and aggregation
  • app/Services/Indicators/AdminDashboard/DashboardCalculatorService.php - Statistical calculations
  • app/Services/Indicators/AdminDashboard/ProgrammeStatsService.php - Programme-specific stats

Scheduled Commands

  • app/Console/Commands/UpdateElementComplianceReporting.php - Element progress snapshots
  • app/Console/Commands/UpdateSessionAttendanceSnapshots.php - Attendance snapshots
  • app/Console/Kernel.php - Schedule configuration (lines 53-54)

Models

  • app/Models/OrganisationProgrammeSeat.php - Core seat model
  • app/Models/SessionAttendanceSnapshot.php - Attendance snapshot storage
  • app/Models/IndicatorTask.php - Task management

Extending the Dashboard

Adding New Statistics

  1. Add calculation method to DashboardCalculatorService
  2. Add public getter to DashboardContextService
  3. Create new widget extending BaseIndicatorsDashboard\BaseWidget
  4. Add widget to BaseIndicatorsDashboard::getHeaderWidgets() or getWidgets()
  5. Update cache keys if needed

Adding New Filters

  1. Add filter to BaseIndicatorsDashboard::filtersForm()
  2. Update DashboardContextService::applyFilters() to handle new filter
  3. Ensure filter is included in cache key normalization
  4. Update documentation

Modifying Scoping Logic

When changing how data is scoped:

  1. Update relevant method in DashboardContextService
  2. Clear all cache after deployment: DashboardContextService::clearCache()
  3. Run full test suite to ensure no regressions
  4. Update this documentation

Performance Optimization

If dashboard becomes slow:

  1. Check query count (should be ~5-10 queries total due to eager loading)
  2. Verify cache is working (check Cache::tags(['indicators_dashboard']))
  3. Consider increasing cache TTL for stable data
  4. Review eager loading strategy - add more relationships if N+1 detected
  5. Consider database indexing on frequently filtered columns