KPI Matrix How-to Guide Public

02 — KPI Categories & Metrics Definition

02 — KPI Categories & Metrics Definition Document: KPI Categories, Individual Metrics, Scoring Scales, Weights & Aggregation Module: KPI Matrix (Employee Performance Management)…

Guide version: r1 Module version: 1.4.0 Updated: 2026-07-22 Estimated time: 26 min 1 views

Document: KPI Categories, Individual Metrics, Scoring Scales, Weights & Aggregation Module: KPI Matrix (Employee Performance Management) Target System: ERPat ERP (CodeIgniter 3 / PHP 8.2+) Status: Implemented — documentation reconciled to code Last Revised: June 2026 (aligned to modules/KpiMatrix/config/kpi_templates.php + modules/KpiMatrix/libraries/Kpi_calculation_engine.php)


Table of Contents

  1. Overview & KPI Design Philosophy
  2. How to Read a Metric Definition
  3. Scoring Scale Standard
  4. Normalization (As Implemented)
  5. Category 1 — Attendance & Punctuality (ATT)
  6. Category 2 — Task Performance (TSK)
  7. Category 3 — Project Delivery (PRJ)
  8. Category 4 — Support & Service Quality (TKT)
  9. Category 5 — Behavioral Compliance (BHV)
  10. Category 6 — Leave Management (LVE)
  11. Category 7 — Professional Development (PRF)
  12. Category 8 — Time Management (TMS)
  13. Weight Distribution Templates
  14. Composite Score Calculation
  15. Rating Bands & Salary Mapping
  16. Data Sufficiency & Edge Cases
  17. Appendix A — Master Metric Register (29 Metrics)
  18. Appendix B — Best-Practice Enhancements (Future)
  19. Appendix C — Implementation Fidelity Notes

1. Overview & KPI Design Philosophy

The KPI Matrix organizes employee performance into 8 categories containing 29 metrics in total. Every metric is computed automatically from data ERPat already captures, normalized to a common 1.00–5.00 scale, weighted, and rolled up into a single composite score that drives review ratings and salary-progression recommendations.

This document is the single source of truth for metric semantics. The runtime definitions live in modules/KpiMatrix/config/kpi_templates.php (categories, metric metadata, thresholds, templates) and the formulas live in modules/KpiMatrix/libraries/Kpi_calculation_engine.php. When this document and code disagree, code wins and this document must be updated — see Appendix C for the known reconciliation list.

1.1 Design Principles

These principles are derived from current performance-management best practice (balanced scorecard, SMART KPIs, and the leading/lagging-indicator framework) and adapted to data ERPat actually holds:

  1. Data-driven first. 25 of 29 metrics compute with zero manual grading. The remaining 4 are manual/semi-manual because the underlying signal (subjective quality, customer satisfaction, off-system training) does not yet exist as structured data — see Appendix B.
  2. Balanced dimensions. Each category mixes the four performance dimensions so no single behavior dominates:
    • Quantity — how much was produced (task throughput, tickets resolved).
    • Quality — how good the output was (task quality, ticket satisfaction).
    • Efficiency — output relative to time/resource (resolution time, productive-hours ratio).
    • Reliability / Compliance — consistency and rule-adherence (attendance, schedule adherence, discipline).
  3. Leading + lagging balance. Lagging indicators confirm results that already happened (attendance rate, task completion); leading indicators predict future performance (certifications, training, leave/overtime health as burnout signals). Both are tagged per metric so reviewers can act on leading signals before lagging ones deteriorate.
  4. Bi-directional & normalized. Some metrics reward higher values (direction: higher); some penalize them (direction: lower). All are normalized onto the same 1.00–5.00 band so they can be compared and averaged fairly across units.
  5. Configurable, not hard-coded. Category weights, metric weights, and scoring thresholds are stored per template and per template-metric (override_thresholds), so the same engine serves a developer, a call-center agent, and a production worker with different emphases.
  6. Auditable. Every computed score persists its raw_value, the normalized score, and a JSON computation_log (inputs + formula + timestamp) in kpi_metric_scores, so any rating is fully reconstructable.

1.2 Avoiding Common KPI Pitfalls

Pitfall How this design avoids it
Vanity metrics (look good, mean little) Every metric maps to a documented business behavior and a performance dimension; pure-volume metrics (throughput, tickets) are balanced by quality/reliability metrics in the same category.
Gaming a single number Composite is a weighted blend of 8 categories; over-optimizing one (e.g. overtime) is neutralized by bell-curve scoring and counter-metrics (undertime, leave health).
One-size-fits-all Five role-based weight templates; categories can be set to 0% to disable for a role.
Measuring activity, not outcome On-time delivery, SLA-style resolution time, and project completion measure outcomes, not just effort.
Too many metrics Best practice favors 3–8 active drivers per scope. Each category carries 3–5 metrics; templates can disable the long tail to keep an employee's scorecard focused.

2. How to Read a Metric Definition

Each metric below is documented with a consistent header block:

Field Meaning
Key The metric_key used in kpi_templates.php, kpi_template_metrics, and kpi_metric_scores. Stable identifier — never rename without a migration.
Dimension Quantity / Quality / Efficiency / Reliability / Compliance / Development / Wellbeing.
Indicator Lagging (confirms past result) or Leading (predicts future result).
Direction higher (↑ better), lower (↓ better), or bell (a target band is best — encoded via non-monotonic thresholds).
Automation Automated / Semi-automated (auto with manual fallback) / Manual.
Source The model/method or SQL table(s) the engine reads.
Formula The exact raw_value formula the engine computes (mirrors the computation_log.formula string).
Thresholds The five raw-value bands from kpi_templates.php that map to the 1–5 scale.
SMART target A recommended, configurable target expressed in SMART terms for reviewers setting expectations.

3. Scoring Scale Standard

All metrics normalize to a 1.00–5.00 continuous scale, stored as DECIMAL(4,2):

Score Range Rating Band (rating_band) Meaning Color
4.50 – 5.00 outstanding Consistently exceeds expectations #27ae60
3.50 – 4.49 exceeds_expectations Frequently surpasses requirements #2ecc71
2.50 – 3.49 meets_expectations Satisfactorily fulfills the role #f39c12
1.50 – 2.49 needs_improvement Below acceptable standard #e67e22
1.00 – 1.49 unsatisfactory Significantly below minimum #e74c3c

These bands are defined once in $config['kpi_rating_bands'] and consumed by both the engine (_get_rating_band()) and the UI badges. The 1–5 scale (not 0–100) is the canonical internal scale; raw values are only ever shown alongside the normalized score for transparency.


4. Normalization (As Implemented)

The engine method _normalize_score($raw_value, $thresholds, $direction) — which delegates to the pure Modules\KpiMatrix\Services\ScoreNormalizer (library) — converts a raw value to a 1.00–5.00 score using within-band linear interpolation, not a single global linear map. This is the authoritative algorithm:

  1. Direction handling. Bands are matched directly against the authored threshold ranges — they are never relabelled or swapped. For direction = 'lower' metrics, only the within-band interpolation is reversed (step 3): a low raw value earns the high end of its band's score window. (The config authors every band quality-first, so a lower metric's best/lowest raw values already sit in the outstanding band.)

  2. Band lookup. The raw value is matched against the five threshold bands. Each band maps to a score sub-range:

    Band Score sub-range
    outstanding 4.50 – 5.00
    exceeds 3.50 – 4.49
    meets 2.50 – 3.49
    needs_improvement 1.50 – 2.49
    unsatisfactory 1.00 – 1.49
  3. Interpolation within the band. Position inside the band's raw range determines the exact score. For higher metrics position runs from the band's min upward; for lower metrics it is reversed (the only place direction changes the result):

    position = (raw_value − band_min) / (band_max − band_min)          // direction = 'higher'
    position = (band_max − raw_value) / (band_max − band_min)          // direction = 'lower'
    score    = band_base + position × (band_ceiling − band_base)
    score    = CLAMP(score, 1.00, 5.00)
  4. Out-of-range → clamp to global range. Before matching, the raw value is clamped into the authored global [min, max] range (the union of all defined bands). An out-of-range value therefore scores as the extreme band that owns that end of the scale — e.g. overtime_ratio at 200% (above the top authored value) lands in its worst band, not 5.00. There is no "above outstanding.max5.00" shortcut.

  5. Null. A null raw value (insufficient data / division-by-zero) returns null and is excluded from the category aggregate (weight redistributed — see §14).

4.1 Bell-Curve (Target-Band) Metrics

Three metrics are healthiest at a moderate value, not a maximum: overtime_ratio, leave_utilization, and leave_balance. These are encoded with direction: higher but with non-monotonic thresholds so the outstanding band sits in the middle of the value range. Example — overtime_ratio:

Raw OT/regular % Band Score
0 – 4.99% meets 2.50–3.49 (under-utilized)
5 – 15% outstanding 4.50–5.00 (healthy)
15.01 – 25% exceeds 3.50–4.49
25.01 – 35% needs_improvement 1.50–2.49 (overwork)
35.01 – 100% unsatisfactory 1.00–1.49 (burnout risk)

Note: This "fold the bands" technique works with the current matcher but is fragile (the band labels no longer correspond to monotonic quality). A first-class direction: 'target' mode with an explicit ideal range is listed in Appendix B as the recommended hardening.


5. Category 1 — Attendance & Punctuality (ATT)

Code: ATT · Default weight (General Staff): 20% · Dimension focus: Reliability + Wellbeing Sources: attendance, attendance_metrics, Payslips_model, holidays

Attendance is the foundation of reliability. Note that _get_working_days() excludes weekends (Sat/Sun) and rows from the holidays table, so rates are measured against true scheduled working days, not raw calendar days.

5.1 att_rate — Attendance Rate

Dimension / Indicator Reliability · Lagging
Direction / Automation ↑ higher · Automated
Source attendance (distinct approved in-days) ÷ _get_working_days()
Formula (days_present / total_working_days) × 100
Thresholds ≥98 Outstanding · 95–97.99 Exceeds · 90–94.99 Meets · 80–89.99 Needs Imp · <80 Unsatisfactory
SMART target "Maintain ≥95% approved attendance against scheduled working days each review period."

5.2 punctuality_rate — Punctuality Rate

Dimension / Indicator Reliability · Lagging
Direction / Automation ↑ higher · Automated
Source attendanceattendance_metrics — days with late = 0 ÷ total attendance days
Formula (on_time_days / total_attendance_days) × 100
Thresholds ≥98 · 95–97.99 · 90–94.99 · 80–89.99 · <80
SMART target "≥95% of attended days with zero recorded tardiness."

5.3 absence_rate — Absence Rate

Dimension / Indicator Reliability · Lagging
Direction / Automation ↓ lower · Automated
Source Payslips_model::get_yearly_absent_total() (hours ÷ 8 = days) ÷ working days
Formula (absent_days / total_working_days) × 100
Thresholds (lower better) 0–1 Outstanding · 1.01–2.5 Exceeds · 2.51–5 Meets · 5.01–8 Needs Imp · >8 Unsatisfactory
SMART target "Keep unpaid absence below 1% of scheduled days per period."

5.4 overtime_ratio — Overtime Utilization (bell-curve)

Dimension / Indicator Wellbeing · Leading
Direction / Automation target-band (encoded higher) · Automated
Source attendance_metricsSUM(overtime) / SUM(regular_hours)
Formula (overtime_hours / regular_hours) × 100
Healthy band 5–15% scores highest; <5% under-utilized; >25% burnout risk (see §4.1)
SMART target "Sustain overtime within 5–15% of regular hours; investigate sustained >25%."

5.5 undertime_rate — Undertime Rate

Dimension / Indicator Reliability · Lagging
Direction / Automation ↓ lower · Automated
Source attendance_metricsSUM(undertime) / SUM(regular_hours)
Formula (undertime_hours / regular_hours) × 100
Thresholds (lower better) 0–0.5 · 0.51–1.5 · 1.51–3 · 3.01–5 · >5
SMART target "Undertime under 0.5% of regular hours per period."

6. Category 2 — Task Performance (TSK)

Code: TSK · Default weight (General Staff): 20% · Dimension focus: Quantity + Quality + Reliability Sources: tasks, activity_logs, kpi_manual_entries

Task assignment uses FIND_IN_SET(user_id, tasks.assigned_to) (comma-separated assignees) and status_id = 3 for completed. Balances volume (task_throughput) against quality (task_quality) and reliability (task_ontime).

6.1 task_completion — Task Completion Rate

Dimension / Indicator Quantity / Outcome · Lagging
Direction / Automation ↑ higher · Automated (Tasks_model::get_task_statistics)
Formula (completed_tasks / total_assigned_tasks) × 100 (tasks created in period)
Thresholds ≥95 · 85–94.99 · 70–84.99 · 50–69.99 · <50
SMART target "Complete ≥85% of tasks assigned during the period."

6.2 task_ontime — On-Time Task Delivery

Dimension / Indicator Reliability · Lagging
Direction / Automation ↑ higher · Automated
Source/Formula Intended: completed tasks delivered by deadline ÷ total completed → × 100
Thresholds ≥90 · 80–89.99 · 65–79.99 · 50–64.99 · <50
SMART target "Deliver ≥90% of completed tasks on or before deadline."

⚠️ Returns null (data limitation, C-10): tasks records no completion timestamp, so on-time delivery cannot be computed. Excluded from the composite until a completed_at exists. See Appendix C.

6.3 task_quality — Task Quality Score (semi-automated)

Dimension / Indicator Quality · Lagging
Direction / Automation ↑ higher · Semi-automated (rework detection, manual fallback)
Source tasksactivity_logs (action = 'reopened'); falls back to kpi_manual_entries when the employee has zero completed tasks in the period
Formula (tasks_without_rework / total_completed) × 100
Thresholds ≥95 · 85–94.99 · 70–84.99 · 50–69.99 · <50
SMART target "≤5% of completed tasks reopened/reworked."

6.4 task_throughput — Task Throughput

Dimension / Indicator Quantity · Lagging
Direction / Automation ↑ higher · Automated
Formula completed_tasks / months_in_range (monthly run-rate)
Thresholds (count/mo) ≥20 · 15–19.99 · 10–14.99 · 5–9.99 · <5
SMART target Role-calibrated — "Sustain ≥15 completed tasks/month" (adjust per role; raw count is volume-sensitive, so set thresholds per template).

7. Category 3 — Project Delivery (PRJ)

Code: PRJ · Default weight (General Staff): 10% · Dimension focus: Outcome + Reliability Sources: projects, project_members, tasks

Measures contribution to team delivery, distinct from individual tasks. Project membership resolves via project_members.user_id.

7.1 proj_completion — Project Completion Rate

Dimension / Indicator Outcome · Lagging
Direction / Automation ↑ higher · Automated (Projects_model::get_details by team_member_id)
Formula (Σ completed_points / Σ total_points) × 100 across the member's projects
Thresholds ≥95 · 85–94.99 · 70–84.99 · 50–69.99 · <50
SMART target "Member's projects average ≥85% point completion."

7.2 proj_ontime — Project On-Time Delivery

Dimension / Indicator Reliability · Lagging
Direction / Automation ↑ higher · Automated
Formula Intended: completed projects delivered by deadline ÷ total completed → × 100
Thresholds ≥90 · 80–89.99 · 65–79.99 · 50–64.99 · <50
SMART target "≥90% of completed projects delivered by deadline."

⚠️ Returns null (data limitation, C-10): projects records no completion-date column, so on-time project delivery cannot be computed. Excluded from the composite. See Appendix C.

7.3 proj_contribution — Project Points Contribution

Dimension / Indicator Quantity · Lagging
Direction / Automation ↑ higher · Automated (tasksproject_members)
Formula (user_completed_task_points / total_project_task_points) × 100
Thresholds ≥30 · 20–29.99 · 10–19.99 · 5–9.99 · <5
SMART target "Contribute ≥20% of completed task-points on shared projects."

8. Category 4 — Support & Service Quality (TKT)

Code: TKT · Default weight (General Staff): 10% · Dimension focus: Efficiency + Quality Sources: tickets, ticket_comments, kpi_manual_entries

Tickets assigned via tickets.assigned_to = user_id; resolved = status = 'closed'.

8.1 ticket_resolved — Ticket Resolution Rate

Dimension / Indicator Efficiency / Outcome · Lagging
Direction / Automation ↑ higher · Automated
Formula (resolved_tickets / tickets_in_queue) × 100, where in queue = assigned to the user, created on/before period end, and not closed before the period started
Thresholds ≥95 · 85–94.99 · 70–84.99 · 50–69.99 · <50
SMART target "Resolve ≥85% of the tickets in your queue during the period."

Resolved (C-1, Jun 2026): the engine previously emitted a raw count scored against % thresholds; it now computes a true resolution rate. See Appendix C.

8.2 ticket_response — Average Response Time

Dimension / Indicator Efficiency · Leading
Direction / Automation ↓ lower · Automated
Source/Formula First comment by the assignee per ticket → AVG(first_response − created_at) in hours
Thresholds (hours, lower better) 0–1 · 1.01–4 · 4.01–8 · 8.01–24 · >24
SMART target "First response within 1 hour on average."

8.3 ticket_resolution — Average Resolution Time

Dimension / Indicator Efficiency · Lagging
Direction / Automation ↓ lower · Automated
Formula AVG(closed_date − created_at) in hours, closed tickets
Thresholds (hours, lower better) 0–4 · 4.01–12 · 12.01–24 · 24.01–48 · >48
SMART target "Average resolution under 4 hours."

8.4 ticket_satisfaction — Ticket Satisfaction (manual)

Dimension / Indicator Quality / Outcome · Lagging
Direction / Automation ↑ higher · Manual (kpi_manual_entries)
Formula AVG(manual satisfaction entries), 1–5 scale
SMART target "Maintain ≥4.5/5 average requester satisfaction." Becomes automatable once a ticket-feedback signal exists (Appendix B).

9. Category 5 — Behavioral Compliance (BHV)

Code: BHV · Default weight (General Staff): 10% · Dimension focus: Compliance Sources: discipline_entries, leave_applications

This is an inverse category — fewer infractions = higher score. Discipline entries map to a user via FIND_IN_SET(user_id, discipline_entries.employee_ids).

9.1 discipline_score — Disciplinary Score

Dimension / Indicator Compliance · Lagging
Direction / Automation ↑ higher (already a 1–5 score) · Automated
Formula MAX(1.00, 5.00 − Σ severity_weight) where verbal=0.25, written=0.50, suspension=1.00, termination=2.50
Thresholds 4.5–5 · 3.5–4.49 · 2.5–3.49 · 1.5–2.49 · 1–1.49
SMART target "Zero formal disciplinary actions per period (score 5.00)."

9.2 leave_compliance — Leave Compliance

Dimension / Indicator Compliance · Lagging
Direction / Automation ↑ higher · Automated
Formula (approved_leaves / total_leave_applications) × 100 (defaults to 100 when no applications)
Thresholds ≥98 · 90–97.99 · 80–89.99 · 60–79.99 · <60
SMART target "≥98% of leave filed through proper approval channels."

9.3 policy_adherence — Policy Adherence Score (derived)

Dimension / Indicator Compliance · Lagging
Direction / Automation ↑ higher · Automated (derived)
Formula (discipline_score × 0.6) + (leave_compliance_score × 0.4) — both on the 1–5 scale
SMART target "Composite conduct score ≥4.5."

Note: policy_adherence is a composite of two other BHV metrics. If all three are enabled in a template, discipline is effectively double-counted; consider disabling policy_adherence where discipline_score + leave_compliance already carry the category (see C-4).


10. Category 6 — Leave Management (LVE)

Code: LVE · Default weight (General Staff): 5% · Dimension focus: Wellbeing + Reliability Sources: leave_credits, leave_applications

Rewards healthy leave behavior — taking enough rest without exhausting entitlements or filing emergencies. Two of three metrics are bell-curve.

10.1 leave_utilization — Leave Credit Utilization (bell-curve)

Dimension / Indicator Wellbeing · Leading
Direction / Automation target-band (encoded higher) · Automated
Formula (approved_used_days / total_credits) × 100 for the year
Healthy band 40–70% scores highest; <20% presenteeism risk; >95% entitlement-exhaustion risk
SMART target "Use 40–70% of annual leave credits."

10.2 unplanned_leave — Unplanned Leave Rate

Dimension / Indicator Reliability · Leading
Direction / Automation ↓ lower · Automated
Formula Approved leaves filed on/after their own start date ÷ total approved → × 100
Thresholds (lower better) 0–5 · 5.01–15 · 15.01–25 · 25.01–40 · >40
SMART target "Keep same-day/emergency leave under 5% of all leave."

10.3 leave_balance — Leave Balance Health (bell-curve)

Dimension / Indicator Wellbeing · Leading
Direction / Automation target-band (encoded higher) · Automated
Formula (remaining_credits / total_credits) × 100 at period end
Healthy band 30–60% remaining scores highest; very high = hoarding, very low = depleted
SMART target "End the year with 30–60% of credits intact."

11. Category 7 — Professional Development (PRF)

Code: PRF · Default weight (General Staff): 10% · Dimension focus: Development (all leading indicators) Sources: Certifications_model, kpi_manual_entries

Development metrics are leading indicators — investment now predicts capability later.

11.1 cert_count — Active Certifications

Dimension / Indicator Development · Leading
Direction / Automation ↑ higher · Automated (Certifications_model::get_total_certifications_of_user)
Formula COUNT(active, non-expired certifications)
Thresholds (count) ≥5 · 3–4.99 · 2–2.99 · 1–1.99 · 0
SMART target "Hold ≥3 role-relevant active certifications."

11.2 skill_growth — Skill Coverage Ratio (manual)

Dimension / Indicator Development · Leading
Direction / Automation ↑ higher · Manual (config + engine now agree)
Formula AVG(manual coverage entries); intended automated form: (matched_skills / role_required_skills) × 100
Thresholds ≥90 · 75–89.99 · 60–74.99 · 40–59.99 · <40
SMART target "Cover ≥75% of the role's required skill set."

Resolved (C-2, Jun 2026): config previously declared this automated while the engine used manual entry; config is now automated=false to match. Full automation (role→skillset coverage) is tracked as B-4.

11.3 training_hours — Training Hours (manual)

Dimension / Indicator Development · Leading
Direction / Automation ↑ higher · Manual
Formula AVG(manual training-hour entries)
Thresholds (hours) ≥40 · 25–39.99 · 15–24.99 · 5–14.99 · <5
SMART target "Log ≥25 training hours/year." Automatable via an LMS or training-tagged timesheets (Appendix B).

11.4 training_completion — Training Completion Rate (manual)

Dimension / Indicator Development · Lagging
Direction / Automation ↑ higher · Manual
Formula AVG(manual completion entries)
Thresholds ≥95 · 80–94.99 · 60–79.99 · 40–59.99 · <40
SMART target "Complete ≥95% of assigned training."

12. Category 8 — Time Management (TMS)

Code: TMS · Default weight (General Staff): 15% · Dimension focus: Efficiency + Reliability Sources: Timesheets_model, project_time, attendance, attendance_metrics

12.1 timesheet_util — Timesheet Compliance Rate

Dimension / Indicator Reliability · Leading
Direction / Automation ↑ higher · Automated (Timesheets_model::get_summary_details)
Formula (logged_hours / expected_hours) × 100, where expected_hours = working_days × 8
Thresholds ≥95 · 85–94.99 · 70–84.99 · 50–69.99 · <50
SMART target "Log ≥95% of expected working hours to timesheets."

12.2 timesheet_accuracy — Productive Hours Ratio

Dimension / Indicator Efficiency · Lagging
Direction / Automation ↑ higher · Automated
Source/Formula Timesheet hours (project_time, duration derived from start/end + manual hours) vs approved attendance hours → (1 − ABS(ts_hours − att_hours) / att_hours) × 100, floored at 0
Thresholds ≥90 · 80–89.99 · 60–79.99 · 40–59.99 · <40
SMART target "Logged productive hours within 10% of clocked hours."

12.3 schedule_adherence — Schedule Adherence

Dimension / Indicator Reliability · Lagging
Direction / Automation ↑ higher · Automated
Formula Days with late = 0 AND undertime = 0 ÷ total attendance days → × 100
Thresholds ≥95 · 85–94.99 · 70–84.99 · 50–69.99 · <50
SMART target "≥95% of days fully within schedule (no late, no undertime)."

13. Weight Distribution Templates

Five role-based templates ship as defaults in $config['kpi_template_weights']. Each distributes the 8 category weights to total 100%. Within a category, metric weights (kpi_template_metrics.weight) determine the blend.

Category General Staff Technical / Developer Customer-Facing Management Operations
ATT — Attendance & Punctuality 20 10 25 10 25
TSK — Task Performance 20 30 10 15 25
PRJ — Project Delivery 10 15 5 20 5
TKT — Support & Service Quality 10 5 30 10 5
BHV — Behavioral Compliance 10 5 10 10 15
LVE — Leave Management 5 5 5 5 5
PRF — Professional Development 10 15 5 15 5
TMS — Time Management 15 15 10 15 15
Total 100 100 100 100 100

Template Rules

  1. All 8 categories carry a weight; 0 disables a category for that role.
  2. Category weights must sum to exactly 100% (validated on save).
  3. Within a category, metric weights are blended via a weighted average over non-null metrics (see §14) — they need not pre-sum to 100; the engine normalizes by the sum of present weights.
  4. Disabled or null-valued metrics redistribute their weight to the remaining metrics in the category automatically.
  5. Thresholds may be overridden per template-metric via override_thresholds (JSON) — used to role-calibrate volume metrics like task_throughput.

14. Composite Score Calculation

The engine (compute_composite()) computes a two-level weighted average, skipping null metrics at both levels.

14.1 Category Score

category_score = Σ(metric_score × metric_weight) / Σ(metric_weight)   ── over non-null metrics only

If every metric in a category is null, the category is omitted entirely and its weight is redistributed.

14.2 Composite Score

composite_score = Σ(category_score × category_weight) / Σ(category_weight)   ── over present categories
rating_band     = band(composite_score)

Worked example (General Staff):

category_scores:  ATT=4.08(20)  TSK=3.75(20)  PRJ=3.50(10)  TKT=4.20(10)
                  BHV=5.00(10)  LVE=3.60(5)   PRF=3.00(10)  TMS=3.80(15)
composite = (4.08·20 + 3.75·20 + 3.50·10 + 4.20·10 + 5.00·10 + 3.60·5 + 3.00·10 + 3.80·15) / 100
          = 388.6 / 100 = 3.89  →  exceeds_expectations

14.3 Persistence

compute_composite() returns composite_score, rating_band, category_scores (JSON), metrics_computed, metrics_total, and data_sufficiency_pct. These are saved to kpi_composite_results and snapshotted into kpi_score_history for trend analysis.


15. Rating Bands & Salary Mapping

The composite rating maps to a salary-step recommendation via $config['kpi_default_step_mapping']. Recommendations are advisory and require approval — see 04_SALARY_INTEGRATION.md.

Rating Step change Flags Consecutive rule
outstanding +2 merit_bonus_eligible 3× → grade_promotion_eligible
exceeds_expectations +1 3× → extra_step
meets_expectations 0
needs_improvement 0 pip_triggered 2× → salary_freeze
unsatisfactory 0 mandatory_pip 2× → demotion_review

Step changes are capped by the kpi_salary_max_step_increase setting (default 2). Consecutive-rating detection is provided by Kpi_score_history_model::get_consecutive_rating().


16. Data Sufficiency & Edge Cases

16.1 Data Sufficiency

_check_data_sufficiency() computes coverage = (metrics_total − metrics_null) / metrics_total × 100. Coverage ≥ 60% marks the result data_sufficient = true; below that, the composite is still produced but flagged as low-confidence (surface this in the UI and exclude from rankings).

16.2 Division by Zero / Insufficient Data

Each calculator returns raw_value = null when its denominator is zero (no scheduled days, no tasks, no tickets, no credits). Null metrics are excluded from aggregation and weight is redistributed — they never score 1.00 by default.

16.3 Probationary / New Employees

Employees with < 60% data coverage (typically < ~2 months tenure in the period) should be flagged "Insufficient Data" in the UI and excluded from forced ranking. First-cycle reviews may apply relaxed thresholds via override_thresholds.

16.4 Extended Leave

An employee on leave for the majority of the period will legitimately show null/low operational metrics. Flag "Extended Leave," keep the available metrics for partial assessment, and allow a reviewer override note on the assignment (reviewer_notes, final_override_score).

16.5 Inverse & Bell Metrics

  • Inverse (lower): thresholds inverted at normalization (§4).
  • Bell (overtime_ratio, leave_utilization, leave_balance): healthiest in the middle band (§4.1).

Appendix A — Master Metric Register (29 Metrics)

# Cat metric_key Label Dim Ind Dir Auto Engine method
1 ATT att_rate Attendance Rate Reliability Lag _calc_attendance_rate
2 ATT punctuality_rate Punctuality Rate Reliability Lag _calc_punctuality_rate
3 ATT absence_rate Absence Rate Reliability Lag _calc_absence_rate
4 ATT overtime_ratio Overtime Utilization Wellbeing Lead bell _calc_overtime_ratio
5 ATT undertime_rate Undertime Rate Reliability Lag _calc_undertime_rate
6 TSK task_completion Task Completion Rate Quantity Lag _calc_task_completion_rate
7 TSK task_ontime On-Time Task Delivery Reliability Lag _calc_task_ontime_rate
8 TSK task_quality Task Quality Score Quality Lag _calc_task_quality_score
9 TSK task_throughput Task Throughput Quantity Lag _calc_task_throughput
10 PRJ proj_completion Project Completion Rate Outcome Lag _calc_project_completion
11 PRJ proj_ontime Project On-Time Delivery Reliability Lag _calc_project_ontime_rate
12 PRJ proj_contribution Project Points Contribution Quantity Lag _calc_project_contribution
13 TKT ticket_resolved Ticket Resolution Rate Efficiency Lag _calc_tickets_resolved
14 TKT ticket_response Avg Response Time Efficiency Lead _calc_ticket_response_time
15 TKT ticket_resolution Avg Resolution Time Efficiency Lag _calc_ticket_resolution_time
16 TKT ticket_satisfaction Ticket Satisfaction Quality Lag _calc_ticket_satisfaction
17 BHV discipline_score Disciplinary Score Compliance Lag _calc_discipline_score
18 BHV leave_compliance Leave Compliance Compliance Lag _calc_leave_compliance
19 BHV policy_adherence Policy Adherence Score Compliance Lag _calc_policy_adherence
20 LVE leave_utilization Leave Credit Utilization Wellbeing Lead bell _calc_leave_utilization
21 LVE unplanned_leave Unplanned Leave Rate Reliability Lead _calc_unplanned_leave
22 LVE leave_balance Leave Balance Health Wellbeing Lead bell _calc_leave_balance
23 PRF cert_count Active Certifications Development Lead _calc_certification_count
24 PRF skill_growth Skill Coverage Ratio Development Lead _calc_skill_growth (see C-2)
25 PRF training_hours Training Hours Development Lead _calc_training_hours
26 PRF training_completion Training Completion Rate Development Lag _calc_training_completion
27 TMS timesheet_util Timesheet Compliance Rate Reliability Lead _calc_timesheet_utilization
28 TMS timesheet_accuracy Productive Hours Ratio Efficiency Lag _calc_timesheet_accuracy
29 TMS schedule_adherence Schedule Adherence Reliability Lag _calc_schedule_adherence

Legend: ✅ Automated · ◐ Semi-automated (auto + manual fallback) · ✋ Manual entry · Lag = lagging indicator · Lead = leading indicator. Coverage: 24 fully automated · 1 semi-automated (task_quality) · 4 manual (ticket_satisfaction, skill_growth, training_hours, training_completion) = 29.


Appendix B — Best-Practice Enhancements (Future)

These are recommendations, not implemented behavior. They raise the matrix toward best-in-class without changing the current scoring contract. Each can be added as a new metric or a new engine mode behind config.

# Enhancement Best-practice rationale Effort
B-1 First-class direction: 'target' mode with an explicit ideal range {ideal_min, ideal_max} and symmetric falloff, replacing the fragile folded-band trick for overtime_ratio, leave_utilization, leave_balance. Bell-curve metrics should be expressed as a target, not mislabeled bands; removes misleading band labels. M
B-2 Goal/objective achievement metric (MBO-style): % of period objectives met, sourced from a lightweight goals table or kpi_manual_entries. Outcome alignment is the single highest-leverage KPI per current HR research; pairs lagging results with the employee's own targets. M–L
B-3 Automate ticket_satisfaction from a post-resolution rating signal (CSAT) once captured on tickets. Converts the only customer-quality metric from manual to objective. M
B-4 Automate skill_growth as role-required-skill coverage via Skillsets_workforce_model (close C-2). Removes a manual step and a config/engine contradiction. S–M
B-5 Automate training_hours from training-tagged timesheets or an LMS module. Eliminates manual development data entry. M
B-6 Peer / 180°–360° input as an optional manual category for management templates. Best practice blends quantitative + qualitative, multi-rater perspectives. L
B-7 Period-scoped manual entries: have _get_manual_entry_score() filter by the cycle's date range / cycle_id rather than averaging all-time entries (see C-3). Prevents stale prior-period scores leaking into a new review. S
B-8 eNPS / engagement pulse as an organization-level leading indicator on dashboards (not per-employee scoring). Engagement is a proven leading indicator of retention and performance. M

Appendix C — Implementation Fidelity Notes

Reconciliation items where code and config currently disagree, or where the implementation is incomplete. These are the authoritative to-do list for the next implementation pass; do not "fix" the docs to hide them.

ID Item State Resolution
C-1 ticket_resolved count vs rate Resolved (Jun 2026) Engine now computes (resolved / tickets_in_queue) × 100 — a true rate matching the % thresholds.
C-2 skill_growth config/engine mismatch Resolved (Jun 2026) Config flipped to automated=false to match the manual engine behavior. Full automation tracked as B-4.
C-3 Manual-entry scope Resolved (Jun 2026) _get_manual_entry_score() now scopes to the assignment under evaluation (_current_assignment_id), so only the current cycle's entries count. On-demand path still falls back to employee-wide.
C-4 policy_adherence double-counts Resolved as documented (Jun 2026) Confirmed intentional roll-up of discipline_score (0.6) + leave_compliance (0.4). Guidance added at the config source and in §9.3: a template should use either policy_adherence or the discipline_score + leave_compliance pair at full weight — not all three. No engine change (silently altering default scoring would be worse).
C-5 Salary recommendation stub Resolved (Jun 2026) _generate_salary_recommendation() now reads get_current_salary(), prices the recommended step via Salary_grades_model::compute_salary_for_step(), clamps to the grade range, and emits at_grade_ceiling / manual_review_no_grade flags. Approval→salary_history write remains per 04_SALARY_INTEGRATION.md.
C-6 timesheet_accuracy broken query Resolved (Jun 2026) The query referenced a non-existent table (project_time_logs) and a non-existent column (duration), so the metric always errored to null. Now reads project_time and derives duration as TIMESTAMPDIFF(SECOND, start_time, end_time) + ROUND(hours×60)×60, matching Timesheets_model.
C-7 training_hours registry model Resolved (Jun 2026) Removed the unused Timesheets_model from the registry entry (the calculator only reads manual entries). Timesheet/LMS sourcing remains as B-5.
C-8 Metric count Resolved (Jun 2026) Corrected to 29 across docs, the engine docblock, and the config comment.
C-9 Engine SQL referenced columns that don't exist in the real DB Resolved (Jun 2026) A live-schema audit (dash_ahm) found the engine was written against the documented/assumed schema, not the real legacy tables. ~half the metrics queried non-existent columns → threw → silently null. All corrected and validated against the live DB. See the column map below.
C-10 No task/project completion timestamp exists Handled (Jun 2026) tasks and projects have no completed_at/completion_date (and task status changes aren't in activity_logs). task_ontime and proj_ontime cannot be computed; they now return null (excluded from composite) with a documented reason instead of throwing. See Data Limitations below.

C-9 detail — column corrections (verified against dash_ahm)

Metric(s) Was (nonexistent) Now (real column)
overtime_ratio, timesheet_accuracy attendance_metrics.regular_hours, .overtime worked, and reg_ot+rest_ot+special_ot+legal_ot
undertime_rate, schedule_adherence attendance_metrics.regular_hours, .undertime worked, under
att_rate, absence_rate, timesheet_util (via _get_working_days) holidays.start_date holidays.date_from
ticket_resolved, ticket_resolution tickets.closed_date tickets.closed_at
leave_compliance, unplanned_leave leave_applications.date_created leave_applications.created_at
task_completion tasks.date_created tasks.created_date
task_throughput, task_quality, proj_contribution tasks.updated_at (no such column) tasks.created_date (period anchor; see Data Limitations)
discipline_score, policy_adherence discipline_entries.employee_ids, .date_created, .severity (all fictional) user (CSV), date_occurred; severity-weighting replaced with incident-count scoring (MAX(1, 5 − count))
leave_utilization, leave_balance leave_credits.total_credits, .year (no such columns) credit/debit ledger (action/counts): granted=SUM(debit), used=SUM(credit)
proj_completion Projects_model::get_details(['team_member_id']) (ignored → org-wide) ['user_id'] (per-employee membership)

Data Limitations (source data gaps)

These are not bugs — the operational tables simply don't capture the needed signal yet:

  1. No completion timestamp on tasks/projects. task_ontime and proj_ontime return null until a completed_at (tasks) / completion date (projects) is recorded. Recommended: add a completed_at column set when status_id→3 / statuscompleted, or log a completed action to activity_logs. This would also let task_throughput/task_quality/proj_contribution anchor on completion date instead of created_date.
  2. task_quality rework detection. It looks for activity_logs rows with log_for='task', action='reopened', but task events aren't currently written to activity_logs, so rework is always 0 (quality ≈ 100% for anyone with completed tasks). Treat as a manual-leaning metric until task status changes are logged.
  3. discipline_score has no severity input. Scored purely by incident count (each formal action drops one band). If severity is later added to discipline_entries, restore severity-weighting.

Security hardening (Jun 2026)

A controller/model security audit found and fixed:

ID Severity Issue Fix
S-1 Critical assignment_detail_modal() had no ownership/manage check — any base-access user could read any colleague's full KPI scores (IDOR), and the unbound id was SQL-injectable Added authorization (manager or owner only); id is now int-cast in the model
S-2 High Every KPI model get_details() interpolated id filters unescaped (SQLi) All id-type filters (id, assignment_id, employee_id, cycle_id, template_id, reviewer_id, department_id) cast to (int) at extraction across all 9 models
S-3 Critical Kpi_employee_assignments_model department filter referenced a nonexistent team_members table/team_id FK Rewritten to the project convention: EXISTS (… team dept WHERE dept.id = ? AND FIND_IN_SET(employee_id, dept.members))
S-4 Medium overview_data() exposed a company-wide leaderboard (names + scores) to base-access users; the overview tab was shown to everyone Endpoint gated with kpi_matrix_manage; overview tab + its AJAX gated to managers in the view; base users land on My KPIs

Deployment note (D-1) — stale KPI tables in dev DB

The current migration (20260604185300_create_kpi_matrix_tables.php) is the schema source of truth, but each _create_* method short-circuits if the table already exists (SHOW TABLES LIKE … return;). In dash_ahm, only 6 of 9 KPI tables exist and they are from an older schema (e.g. kpi_employee_assignments.user_id/assignment_status, kpi_metric_scores.metric_code, kpi_composite_results.rating_label/data_completeness_pct) — incompatible with the current models/engine (which use employee_id/status, metric_key, rating_band/data_sufficiency_pct). The module cannot run against dash_ahm until the KPI tables are reconciled. Options: (a) drop the 6 stale kpi_* tables and re-run the migration (recreates all 9 with the correct schema; safe only if there is no real KPI data — destructive, requires explicit sign-off), or (b) write an ALTER migration to rename/add the drifted columns. The 3 missing tables (kpi_manual_entries, kpi_score_history, kpi_salary_recommendations) would be created by simply re-running the migration.

Was this guide helpful?

Report a content problem