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:
- Portfolio Dashboard: Shows data across all programmes within a portfolio
- 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:
- The programme has at least one published indicator (compliance or success)
- The programme has enrolled entrepreneurs with programme seats
- The programme's indicators match your dashboard level and organisational scope
- The programme is linked to a tenant in your portfolio or cluster
Which Entrepreneurs Appear
Entrepreneurs appear on the dashboard when:
- They have a programme seat
- Their programme seat's contract start date has passed (not in the future)
- Their programme seat falls within your portfolio or cluster tenants
- 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
- Command:
-
Session Attendance Snapshots: Update daily at 02:30 (Africa/Johannesburg timezone)
- Command:
update:session_attendance_snapshots - Calculates attendance percentages for learning and mentoring sessions
- Command:
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:
- No Published Indicators: The programme has no published compliance or success indicators
- No Enrolled Entrepreneurs: The programme has no entrepreneurs with programme seats in your portfolio/cluster
- 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
- 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:
- No Programme Seat: The person doesn't have a programme seat for any programme with indicators
- Future Contract Start Date: The programme seat's contract start date is in the future
- Wrong Portfolio/Cluster: The programme seat is not within your portfolio or cluster scope
- Inactive Seat: The programme seat has been marked as inactive
Why are some indicators not visible?
Indicators might not appear because:
- Not Published: Only published indicators appear on the dashboard. Unpublished or draft indicators are excluded
- Portfolio/Cluster Assignment: The indicator is assigned to a different portfolio or cluster than yours
- 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
- 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:
- 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
- No Submissions: No submissions exist for the indicators
- 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
- 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
-
Dashboard Pages (Filament Pages)
BaseIndicatorsDashboard: Shared dashboard functionality and widget configurationClusterIndicatorsDashboard: Cluster (parent group) level dashboardPortfolioIndicatorsDashboard: Portfolio level dashboardIndicatorsDashboardProgrammeView: Detailed programme view with cohort filtering
-
Service Layer
DashboardContextService: Manages data scoping, filtering, and aggregation for the main dashboardDashboardCalculatorService: Performs all statistical calculationsProgrammeStatsService: Handles programme-specific and cohort-specific statistics
-
Widgets (Filament Widgets)
GlobalOverallComplianceWidget: Overall compliance percentageGlobalOverallSuccessWidget: Overall success percentageGlobalElementProgressWidget: Average element progressGlobalLearningSessionWidget: Learning session attendanceGlobalMentorSessionWidget: Mentor session attendanceGlobalProgrammesTableWidget: 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:
- User accesses dashboard → Permission check
- Dashboard level determined from session (
voy_dashboard) - Filters retrieved from session
DashboardContextServiceinitializes with scoping and caching- Seat query built with eager loading
- Statistics calculated via
DashboardCalculatorService - Results displayed in widgets
- 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:
- Programme-specific scoping with
ProgrammeStatsService - Cohort filtering with reactive updates
- Dynamic indicator processing (averaging vs achievement types)
- Individual entrepreneur status calculations
- 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:
- Element progress snapshots update at 02:00 AM
- Session attendance snapshots update at 02:30 AM
- Both jobs run in Africa/Johannesburg timezone
- Snapshot data represents the previous day's calculations
- 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:
- Multi-level caching with different TTLs
- Automatic invalidation on filter changes
- Event-driven invalidation on data updates
- Unique cache keys per user/tenant/filter combination
- 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
sessionAttendanceSnapshotsby 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:
-
Filters Change: Automatic via
updatedFilters()methodpublic function updatedFilters(): void { DashboardContextService::clearCache(); // ... } -
Manual Clear: Via static method
DashboardContextService::clearCache(); ProgrammeStatsService::clearCache(); -
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_reportingtable - Implementation:
app/Console/Commands/UpdateElementComplianceReporting.php
What it does:
- Iterates through all users with programme seats
- Calculates progress for each catalogue element
- Stores snapshot of completion percentage
- 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_snapshotstable - Implementation:
app/Console/Commands/UpdateSessionAttendanceSnapshots.php
What it does:
- Iterates through all users with programme seats
- Calculates attendance percentage for LEARNING and MENTORING sessions separately
- Stores attendance stats (attended, missed, not marked, total)
- 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:
- Clear cache between tests:
Cache::tags(['indicators_dashboard'])->flush() - Use different cache keys (different users/tenants)
- 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 widgetsapp/Filament/Admin/Pages/ClusterIndicatorsDashboard.php- Cluster level implementationapp/Filament/Admin/Pages/PortfolioIndicatorsDashboard.php- Portfolio level implementationapp/Filament/Admin/Pages/IndicatorsDashboardProgrammeView.php- Programme drill-down view
Services
app/Services/Indicators/AdminDashboard/DashboardContextService.php- Data scoping and aggregationapp/Services/Indicators/AdminDashboard/DashboardCalculatorService.php- Statistical calculationsapp/Services/Indicators/AdminDashboard/ProgrammeStatsService.php- Programme-specific stats
Scheduled Commands
app/Console/Commands/UpdateElementComplianceReporting.php- Element progress snapshotsapp/Console/Commands/UpdateSessionAttendanceSnapshots.php- Attendance snapshotsapp/Console/Kernel.php- Schedule configuration (lines 53-54)
Models
app/Models/OrganisationProgrammeSeat.php- Core seat modelapp/Models/SessionAttendanceSnapshot.php- Attendance snapshot storageapp/Models/IndicatorTask.php- Task management
Extending the Dashboard
Adding New Statistics
- Add calculation method to
DashboardCalculatorService - Add public getter to
DashboardContextService - Create new widget extending
BaseIndicatorsDashboard\BaseWidget - Add widget to
BaseIndicatorsDashboard::getHeaderWidgets()orgetWidgets() - Update cache keys if needed
Adding New Filters
- Add filter to
BaseIndicatorsDashboard::filtersForm() - Update
DashboardContextService::applyFilters()to handle new filter - Ensure filter is included in cache key normalization
- Update documentation
Modifying Scoping Logic
When changing how data is scoped:
- Update relevant method in
DashboardContextService - Clear all cache after deployment:
DashboardContextService::clearCache() - Run full test suite to ensure no regressions
- Update this documentation
Performance Optimization
If dashboard becomes slow:
- Check query count (should be ~5-10 queries total due to eager loading)
- Verify cache is working (check
Cache::tags(['indicators_dashboard'])) - Consider increasing cache TTL for stable data
- Review eager loading strategy - add more relationships if N+1 detected
- Consider database indexing on frequently filtered columns