dashboard bug fixed
This commit is contained in:
parent
9281e3deb3
commit
54462b1658
653
DASHBOARD_FORMULAS.md
Normal file
653
DASHBOARD_FORMULAS.md
Normal file
@ -0,0 +1,653 @@
|
|||||||
|
# Dashboard Formulas & Calculations Documentation
|
||||||
|
|
||||||
|
This document provides a comprehensive breakdown of all formulas and calculations used in the Dashboard for both **Admin/Management** users and **Regular Users**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 **1. REQUEST VOLUME STATISTICS**
|
||||||
|
|
||||||
|
### **Admin/Management View**
|
||||||
|
**Scope:** All requests across the organization
|
||||||
|
|
||||||
|
**Formulas:**
|
||||||
|
```
|
||||||
|
Total Requests = COUNT(*)
|
||||||
|
WHERE submission_date BETWEEN :start AND :end
|
||||||
|
AND is_draft = false
|
||||||
|
AND is_deleted = false
|
||||||
|
AND submission_date IS NOT NULL
|
||||||
|
|
||||||
|
Approved Requests = COUNT(*)
|
||||||
|
WHERE status = 'APPROVED'
|
||||||
|
AND submission_date BETWEEN :start AND :end
|
||||||
|
AND is_draft = false
|
||||||
|
|
||||||
|
Rejected Requests = COUNT(*)
|
||||||
|
WHERE status = 'REJECTED'
|
||||||
|
AND submission_date BETWEEN :start AND :end
|
||||||
|
AND is_draft = false
|
||||||
|
|
||||||
|
Pending Requests = COUNT(*)
|
||||||
|
WHERE status IN ('PENDING', 'IN_PROGRESS')
|
||||||
|
AND is_draft = false
|
||||||
|
(Note: Includes ALL pending requests regardless of creation date)
|
||||||
|
|
||||||
|
Draft Requests = COUNT(*)
|
||||||
|
WHERE is_draft = true
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Regular User View**
|
||||||
|
**Scope:** Only requests initiated by the user
|
||||||
|
|
||||||
|
**Formulas:**
|
||||||
|
```
|
||||||
|
Total Requests = COUNT(*)
|
||||||
|
WHERE submission_date BETWEEN :start AND :end
|
||||||
|
AND is_draft = false
|
||||||
|
AND initiator_id = :userId
|
||||||
|
|
||||||
|
Approved Requests = COUNT(*)
|
||||||
|
WHERE status = 'APPROVED'
|
||||||
|
AND submission_date BETWEEN :start AND :end
|
||||||
|
AND initiator_id = :userId
|
||||||
|
AND is_draft = false
|
||||||
|
|
||||||
|
Rejected Requests = COUNT(*)
|
||||||
|
WHERE status = 'REJECTED'
|
||||||
|
AND submission_date BETWEEN :start AND :end
|
||||||
|
AND initiator_id = :userId
|
||||||
|
AND is_draft = false
|
||||||
|
|
||||||
|
Pending Requests = COUNT(*)
|
||||||
|
WHERE status IN ('PENDING', 'IN_PROGRESS')
|
||||||
|
AND initiator_id = :userId
|
||||||
|
AND is_draft = false
|
||||||
|
|
||||||
|
Draft Requests = COUNT(*)
|
||||||
|
WHERE is_draft = true
|
||||||
|
AND initiator_id = :userId
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⏱️ **2. TAT EFFICIENCY & SLA COMPLIANCE**
|
||||||
|
|
||||||
|
### **Admin/Management View**
|
||||||
|
**Scope:** All completed requests in date range
|
||||||
|
|
||||||
|
**Formulas:**
|
||||||
|
```
|
||||||
|
Completed Requests = COUNT(*)
|
||||||
|
WHERE status IN ('APPROVED', 'REJECTED')
|
||||||
|
AND is_draft = false
|
||||||
|
AND submission_date IS NOT NULL
|
||||||
|
AND (
|
||||||
|
(closure_date IS NOT NULL AND closure_date BETWEEN :start AND :end)
|
||||||
|
OR (closure_date IS NULL AND updated_at BETWEEN :start AND :end)
|
||||||
|
)
|
||||||
|
|
||||||
|
Breached Requests = COUNT(DISTINCT request_id)
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1 FROM tat_alerts ta
|
||||||
|
WHERE ta.request_id = wf.request_id
|
||||||
|
AND ta.is_breached = true
|
||||||
|
)
|
||||||
|
|
||||||
|
Compliant Requests = Total Completed - Breached Requests
|
||||||
|
|
||||||
|
SLA Compliance % = ROUND((Compliant Requests / Total Completed) × 100, 0)
|
||||||
|
If Total Completed = 0, then Compliance = 0%
|
||||||
|
|
||||||
|
Average Cycle Time (Hours) = ROUND(SUM(cycle_times) / COUNT(cycle_times), 1)
|
||||||
|
Where cycle_time = calculateElapsedWorkingHours(submission_date, completion_date, priority)
|
||||||
|
(Respects working hours, weekends, holidays based on priority)
|
||||||
|
|
||||||
|
Average Cycle Time (Days) = ROUND(Average Cycle Time (Hours) / 8, 1)
|
||||||
|
(Assumes 8 working hours per day)
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Regular User View**
|
||||||
|
**Scope:** Only completed requests initiated by the user
|
||||||
|
|
||||||
|
**Formulas:**
|
||||||
|
```
|
||||||
|
Completed Requests = COUNT(*)
|
||||||
|
WHERE status IN ('APPROVED', 'REJECTED')
|
||||||
|
AND is_draft = false
|
||||||
|
AND submission_date IS NOT NULL
|
||||||
|
AND initiator_id = :userId
|
||||||
|
AND (
|
||||||
|
(closure_date IS NOT NULL AND closure_date BETWEEN :start AND :end)
|
||||||
|
OR (closure_date IS NULL AND updated_at BETWEEN :start AND :end)
|
||||||
|
)
|
||||||
|
|
||||||
|
Breached Requests = COUNT(DISTINCT request_id)
|
||||||
|
WHERE initiator_id = :userId
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM tat_alerts ta
|
||||||
|
WHERE ta.request_id = wf.request_id
|
||||||
|
AND ta.is_breached = true
|
||||||
|
)
|
||||||
|
|
||||||
|
Compliant Requests = Total Completed - Breached Requests
|
||||||
|
|
||||||
|
SLA Compliance % = ROUND((Compliant Requests / Total Completed) × 100, 0)
|
||||||
|
|
||||||
|
Average Cycle Time (Hours) = Same calculation as Admin
|
||||||
|
Average Cycle Time (Days) = Same calculation as Admin
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Breaches are tracked at **approver/level level** but counted at **request level** for SLA compliance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👤 **3. APPROVER LOAD STATISTICS**
|
||||||
|
|
||||||
|
### **Admin/Management View**
|
||||||
|
**Not Applicable** - This metric is user-specific
|
||||||
|
|
||||||
|
### **Regular User View**
|
||||||
|
**Scope:** User's approval workload
|
||||||
|
|
||||||
|
**Formulas:**
|
||||||
|
```
|
||||||
|
Pending Actions = COUNT(DISTINCT level_id)
|
||||||
|
WHERE approver_id = :userId
|
||||||
|
AND status = 'IN_PROGRESS'
|
||||||
|
AND request.status IN ('PENDING', 'IN_PROGRESS')
|
||||||
|
AND is_draft = false
|
||||||
|
AND level_number = request.current_level
|
||||||
|
(Only counts requests at user's current active level)
|
||||||
|
|
||||||
|
Completed Today = COUNT(*)
|
||||||
|
WHERE approver_id = :userId
|
||||||
|
AND status IN ('APPROVED', 'REJECTED')
|
||||||
|
AND action_date BETWEEN :start AND :end
|
||||||
|
(Date range start/end for "today")
|
||||||
|
|
||||||
|
Completed This Week = COUNT(*)
|
||||||
|
WHERE approver_id = :userId
|
||||||
|
AND status IN ('APPROVED', 'REJECTED')
|
||||||
|
AND action_date >= start_of_week
|
||||||
|
AND action_date BETWEEN :start AND :end
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 **4. ENGAGEMENT STATISTICS**
|
||||||
|
|
||||||
|
### **Admin/Management View**
|
||||||
|
**Scope:** All work notes and documents across organization
|
||||||
|
|
||||||
|
**Formulas:**
|
||||||
|
```
|
||||||
|
Work Notes Added = COUNT(*)
|
||||||
|
FROM work_notes wn
|
||||||
|
WHERE wn.created_at BETWEEN :start AND :end
|
||||||
|
|
||||||
|
Attachments Uploaded = COUNT(*)
|
||||||
|
FROM documents d
|
||||||
|
WHERE d.uploaded_at BETWEEN :start AND :end
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Regular User View**
|
||||||
|
**Scope:** Only from requests initiated by the user
|
||||||
|
|
||||||
|
**Formulas:**
|
||||||
|
```
|
||||||
|
Work Notes Added = COUNT(*)
|
||||||
|
FROM work_notes wn
|
||||||
|
WHERE wn.created_at BETWEEN :start AND :end
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM workflow_requests wf
|
||||||
|
WHERE wf.request_id = wn.request_id
|
||||||
|
AND wf.initiator_id = :userId
|
||||||
|
AND wf.is_draft = false
|
||||||
|
)
|
||||||
|
|
||||||
|
Attachments Uploaded = COUNT(*)
|
||||||
|
FROM documents d
|
||||||
|
WHERE d.uploaded_at BETWEEN :start AND :end
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM workflow_requests wf
|
||||||
|
WHERE wf.request_id = d.request_id
|
||||||
|
AND wf.initiator_id = :userId
|
||||||
|
AND wf.is_draft = false
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤖 **5. AI INSIGHTS**
|
||||||
|
|
||||||
|
### **Admin/Management View**
|
||||||
|
**Scope:** All approved requests with conclusion remarks
|
||||||
|
|
||||||
|
**Formulas:**
|
||||||
|
```
|
||||||
|
Total with Conclusion = COUNT(*)
|
||||||
|
WHERE status = 'APPROVED'
|
||||||
|
AND submission_date BETWEEN :start AND :end
|
||||||
|
AND conclusion_remark IS NOT NULL
|
||||||
|
AND is_draft = false
|
||||||
|
|
||||||
|
AI Generated Count = COUNT(*)
|
||||||
|
WHERE status = 'APPROVED'
|
||||||
|
AND submission_date BETWEEN :start AND :end
|
||||||
|
AND ai_generated_conclusion IS NOT NULL
|
||||||
|
AND ai_generated_conclusion != ''
|
||||||
|
AND is_draft = false
|
||||||
|
|
||||||
|
Manual Count = COUNT(*)
|
||||||
|
WHERE status = 'APPROVED'
|
||||||
|
AND submission_date BETWEEN :start AND :end
|
||||||
|
AND (ai_generated_conclusion IS NULL OR ai_generated_conclusion = '')
|
||||||
|
AND is_draft = false
|
||||||
|
|
||||||
|
AI Adoption % = ROUND((AI Generated Count / Total with Conclusion) × 100, 0)
|
||||||
|
|
||||||
|
Average Remark Length = ROUND(AVG(LENGTH(conclusion_remark)), 0)
|
||||||
|
WHERE status = 'APPROVED'
|
||||||
|
AND conclusion_remark IS NOT NULL
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Regular User View**
|
||||||
|
**Scope:** Only approved requests initiated by the user
|
||||||
|
|
||||||
|
**Formulas:**
|
||||||
|
```
|
||||||
|
Total with Conclusion = COUNT(*)
|
||||||
|
WHERE status = 'APPROVED'
|
||||||
|
AND submission_date BETWEEN :start AND :end
|
||||||
|
AND conclusion_remark IS NOT NULL
|
||||||
|
AND initiator_id = :userId
|
||||||
|
AND is_draft = false
|
||||||
|
|
||||||
|
AI Generated Count = COUNT(*)
|
||||||
|
WHERE status = 'APPROVED'
|
||||||
|
AND submission_date BETWEEN :start AND :end
|
||||||
|
AND ai_generated_conclusion IS NOT NULL
|
||||||
|
AND ai_generated_conclusion != ''
|
||||||
|
AND initiator_id = :userId
|
||||||
|
AND is_draft = false
|
||||||
|
|
||||||
|
Manual Count = COUNT(*)
|
||||||
|
WHERE status = 'APPROVED'
|
||||||
|
AND submission_date BETWEEN :start AND :end
|
||||||
|
AND (ai_generated_conclusion IS NULL OR ai_generated_conclusion = '')
|
||||||
|
AND initiator_id = :userId
|
||||||
|
AND is_draft = false
|
||||||
|
|
||||||
|
AI Adoption % = ROUND((AI Generated Count / Total with Conclusion) × 100, 0)
|
||||||
|
|
||||||
|
Average Remark Length = Same calculation as Admin (filtered by initiator_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 **6. SUCCESS RATE (Regular Users Only)**
|
||||||
|
|
||||||
|
**Formula:**
|
||||||
|
```
|
||||||
|
Success Rate % = ROUND((Approved Requests / Total Requests) × 100, 0)
|
||||||
|
If Total Requests = 0, then Success Rate = 0%
|
||||||
|
|
||||||
|
Where:
|
||||||
|
Approved Requests = From Request Volume Statistics
|
||||||
|
Total Requests = From Request Volume Statistics
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏢 **7. DEPARTMENT STATISTICS (Admin Only)**
|
||||||
|
|
||||||
|
**Scope:** All requests grouped by initiator's department
|
||||||
|
|
||||||
|
**Formulas:**
|
||||||
|
```
|
||||||
|
Department Stats = GROUP BY initiator.department
|
||||||
|
|
||||||
|
Total Requests per Dept = COUNT(*)
|
||||||
|
WHERE submission_date BETWEEN :start AND :end
|
||||||
|
AND is_draft = false
|
||||||
|
GROUP BY initiator.department
|
||||||
|
|
||||||
|
Approved per Dept = COUNT(*)
|
||||||
|
WHERE status = 'APPROVED'
|
||||||
|
AND submission_date BETWEEN :start AND :end
|
||||||
|
AND is_draft = false
|
||||||
|
GROUP BY initiator.department
|
||||||
|
|
||||||
|
Rejected per Dept = COUNT(*)
|
||||||
|
WHERE status = 'REJECTED'
|
||||||
|
AND submission_date BETWEEN :start AND :end
|
||||||
|
AND is_draft = false
|
||||||
|
GROUP BY initiator.department
|
||||||
|
|
||||||
|
In Progress per Dept = COUNT(*)
|
||||||
|
WHERE status IN ('PENDING', 'IN_PROGRESS')
|
||||||
|
AND submission_date BETWEEN :start AND :end
|
||||||
|
AND is_draft = false
|
||||||
|
GROUP BY initiator.department
|
||||||
|
|
||||||
|
Approval Rate per Dept = ROUND((Approved / Total Requests) × 100, 0)
|
||||||
|
If Total Requests = 0, then Approval Rate = 0%
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Limited to top 10 departments by total requests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 **8. PRIORITY DISTRIBUTION (Admin Only)**
|
||||||
|
|
||||||
|
**Scope:** All requests grouped by priority (EXPRESS vs STANDARD)
|
||||||
|
|
||||||
|
**Formulas:**
|
||||||
|
```
|
||||||
|
Total Count per Priority = COUNT(*)
|
||||||
|
WHERE submission_date BETWEEN :start AND :end
|
||||||
|
AND is_draft = false
|
||||||
|
GROUP BY priority
|
||||||
|
|
||||||
|
Approved Count per Priority = COUNT(*)
|
||||||
|
WHERE status = 'APPROVED'
|
||||||
|
AND submission_date BETWEEN :start AND :end
|
||||||
|
AND is_draft = false
|
||||||
|
GROUP BY priority
|
||||||
|
|
||||||
|
Breached Count per Priority = COUNT(DISTINCT request_id)
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1 FROM tat_alerts ta
|
||||||
|
WHERE ta.request_id = wf.request_id
|
||||||
|
AND ta.is_breached = true
|
||||||
|
)
|
||||||
|
GROUP BY priority
|
||||||
|
|
||||||
|
Average Cycle Time per Priority = ROUND(SUM(cycle_times) / COUNT(cycle_times), 1)
|
||||||
|
Where cycle_time = calculateElapsedWorkingHours(submission_date, completion_date, priority)
|
||||||
|
Only for COMPLETED requests (status IN ('APPROVED', 'REJECTED'))
|
||||||
|
GROUP BY priority
|
||||||
|
|
||||||
|
Compliance Rate per Priority = ROUND(((Total Count - Breached Count) / Total Count) × 100, 0)
|
||||||
|
If Total Count = 0, then Compliance Rate = 0%
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👥 **9. APPROVER PERFORMANCE (Admin Only)**
|
||||||
|
|
||||||
|
**Scope:** All approvers who completed approvals in date range
|
||||||
|
|
||||||
|
**Formulas:**
|
||||||
|
```
|
||||||
|
Total Approved per Approver = COUNT(DISTINCT level_id)
|
||||||
|
WHERE approver_id = :approverId
|
||||||
|
AND action_date BETWEEN :start AND :end
|
||||||
|
AND status IN ('APPROVED', 'REJECTED')
|
||||||
|
AND action_date IS NOT NULL
|
||||||
|
AND level_start_time IS NOT NULL
|
||||||
|
AND tat_hours > 0
|
||||||
|
AND elapsed_hours > 0
|
||||||
|
|
||||||
|
Within TAT Count = COUNT(DISTINCT level_id)
|
||||||
|
WHERE approver_id = :approverId
|
||||||
|
AND action_date BETWEEN :start AND :end
|
||||||
|
AND status IN ('APPROVED', 'REJECTED')
|
||||||
|
AND elapsed_hours IS NOT NULL
|
||||||
|
AND elapsed_hours > 0
|
||||||
|
AND (
|
||||||
|
elapsed_hours < tat_hours
|
||||||
|
OR (elapsed_hours <= tat_hours AND (tat_breached IS NULL OR tat_breached = false))
|
||||||
|
OR (tat_breached IS NOT NULL AND tat_breached = false)
|
||||||
|
)
|
||||||
|
|
||||||
|
Breached Count = COUNT(DISTINCT level_id)
|
||||||
|
WHERE approver_id = :approverId
|
||||||
|
AND action_date BETWEEN :start AND :end
|
||||||
|
AND status IN ('APPROVED', 'REJECTED')
|
||||||
|
AND elapsed_hours IS NOT NULL
|
||||||
|
AND elapsed_hours > 0
|
||||||
|
AND (
|
||||||
|
elapsed_hours > tat_hours
|
||||||
|
OR (tat_breached IS NOT NULL AND tat_breached = true)
|
||||||
|
)
|
||||||
|
|
||||||
|
TAT Compliance % = ROUND((Within TAT Count / Total Approved) × 100, 0)
|
||||||
|
If Total Approved = 0, then TAT Compliance = 0%
|
||||||
|
|
||||||
|
Average Response Hours = ROUND(AVG(elapsed_hours), 1)
|
||||||
|
WHERE approver_id = :approverId
|
||||||
|
AND action_date BETWEEN :start AND :end
|
||||||
|
AND status IN ('APPROVED', 'REJECTED')
|
||||||
|
AND elapsed_hours IS NOT NULL
|
||||||
|
AND elapsed_hours > 0
|
||||||
|
|
||||||
|
Pending Count = COUNT(DISTINCT level_id)
|
||||||
|
WHERE approver_id = :approverId
|
||||||
|
AND status IN ('PENDING', 'IN_PROGRESS')
|
||||||
|
AND request.status IN ('PENDING', 'IN_PROGRESS')
|
||||||
|
AND is_draft = false
|
||||||
|
AND level_number = request.current_level
|
||||||
|
(Only current active level for each request)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sorting Order:**
|
||||||
|
1. TAT Compliance % (DESC - highest first)
|
||||||
|
2. Average Response Hours (ASC - fastest first)
|
||||||
|
3. Total Approved (DESC - most approvals first)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤖 **10. AI REMARK UTILIZATION (Admin Only)**
|
||||||
|
|
||||||
|
**Scope:** All conclusion remarks generated in date range
|
||||||
|
|
||||||
|
**Formulas:**
|
||||||
|
```
|
||||||
|
Total Usage = COUNT(*)
|
||||||
|
FROM conclusion_remarks cr
|
||||||
|
WHERE cr.generated_at BETWEEN :start AND :end
|
||||||
|
|
||||||
|
Total Edits = COUNT(*)
|
||||||
|
FROM conclusion_remarks cr
|
||||||
|
WHERE cr.generated_at BETWEEN :start AND :end
|
||||||
|
AND cr.is_edited = true
|
||||||
|
|
||||||
|
Edit Rate % = ROUND((Total Edits / Total Usage) × 100, 0)
|
||||||
|
If Total Usage = 0, then Edit Rate = 0%
|
||||||
|
|
||||||
|
Monthly Trends (Last 7 Months):
|
||||||
|
AI Usage = COUNT(*)
|
||||||
|
WHERE generated_at >= NOW() - INTERVAL '7 months'
|
||||||
|
GROUP BY DATE_TRUNC('month', generated_at)
|
||||||
|
|
||||||
|
Manual Edits = COUNT(*)
|
||||||
|
WHERE generated_at >= NOW() - INTERVAL '7 months'
|
||||||
|
AND is_edited = true
|
||||||
|
GROUP BY DATE_TRUNC('month', generated_at)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 **11. DATE RANGE CALCULATIONS**
|
||||||
|
|
||||||
|
**Date Range Parsing:**
|
||||||
|
```
|
||||||
|
Today:
|
||||||
|
start = start_of_day(today)
|
||||||
|
end = end_of_day(today)
|
||||||
|
|
||||||
|
This Week:
|
||||||
|
start = start_of_week(today)
|
||||||
|
end = end_of_week(today)
|
||||||
|
|
||||||
|
This Month:
|
||||||
|
start = start_of_month(today)
|
||||||
|
end = end_of_month(today)
|
||||||
|
|
||||||
|
This Quarter:
|
||||||
|
start = start_of_quarter(today)
|
||||||
|
end = end_of_quarter(today)
|
||||||
|
|
||||||
|
This Year:
|
||||||
|
start = start_of_year(today)
|
||||||
|
end = end_of_year(today)
|
||||||
|
|
||||||
|
Custom Range:
|
||||||
|
start = start_of_day(custom_start_date)
|
||||||
|
end = end_of_day(custom_end_date)
|
||||||
|
(Capped at current date if future date provided)
|
||||||
|
|
||||||
|
Default (if not specified):
|
||||||
|
start = 30 days ago (start of day)
|
||||||
|
end = today (end of day)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 **12. CRITICAL ALERTS**
|
||||||
|
|
||||||
|
### **Admin/Management View**
|
||||||
|
**Scope:** All requests with critical TAT status
|
||||||
|
|
||||||
|
**Formulas:**
|
||||||
|
```
|
||||||
|
Critical Requests = Requests where:
|
||||||
|
- TAT percentage used >= 80% (approaching deadline)
|
||||||
|
- OR TAT percentage used >= 100% (breached)
|
||||||
|
- OR has breach alerts (is_breached = true in tat_alerts)
|
||||||
|
|
||||||
|
Breached Count = COUNT(*)
|
||||||
|
WHERE breachCount > 0
|
||||||
|
(From critical requests)
|
||||||
|
|
||||||
|
Warning Count = COUNT(*)
|
||||||
|
WHERE breachCount = 0
|
||||||
|
AND TAT percentage >= 80%
|
||||||
|
(From critical requests)
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Regular User View**
|
||||||
|
**Scope:** Only requests initiated by the user
|
||||||
|
|
||||||
|
**Formulas:**
|
||||||
|
```
|
||||||
|
Critical Requests = Same logic as Admin, filtered by initiator_id = :userId
|
||||||
|
|
||||||
|
Breached Count = Same calculation as Admin (filtered by user)
|
||||||
|
Warning Count = Same calculation as Admin (filtered by user)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 **13. UPCOMING DEADLINES**
|
||||||
|
|
||||||
|
**Scope:** Requests with active levels approaching TAT deadline
|
||||||
|
|
||||||
|
**Formulas:**
|
||||||
|
```
|
||||||
|
Upcoming Deadlines = Requests where:
|
||||||
|
- Current level is active (status IN ('PENDING', 'IN_PROGRESS'))
|
||||||
|
- remainingHours > 0 (not yet breached)
|
||||||
|
- tatPercentageUsed < 100 (not yet breached)
|
||||||
|
|
||||||
|
TAT Percentage Used = ROUND((elapsedHours / tatHours) × 100, 0)
|
||||||
|
Where elapsedHours = calculateElapsedWorkingHours(level_start_time, current_time, priority)
|
||||||
|
|
||||||
|
Remaining Hours = MAX(0, tatHours - elapsedHours)
|
||||||
|
|
||||||
|
Elapsed Hours = calculateElapsedWorkingHours(level_start_time, current_time, priority)
|
||||||
|
(Respects working hours, weekends, holidays based on priority)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Only shows requests that are NOT yet breached (remainingHours > 0 and tatPercentage < 100).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 **14. RECENT ACTIVITY**
|
||||||
|
|
||||||
|
### **Admin/Management View**
|
||||||
|
**Scope:** All workflow activities across organization
|
||||||
|
|
||||||
|
**Formulas:**
|
||||||
|
```
|
||||||
|
Recent Activity = All activities from workflow_requests
|
||||||
|
ORDER BY activity.created_at DESC
|
||||||
|
(No user filter)
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Regular User View**
|
||||||
|
**Scope:** Activities from user's requests or where user is a participant
|
||||||
|
|
||||||
|
**Formulas:**
|
||||||
|
```
|
||||||
|
Recent Activity = Activities where:
|
||||||
|
- request.initiator_id = :userId
|
||||||
|
- OR user is a participant in the request
|
||||||
|
ORDER BY activity.created_at DESC
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 **KEY DIFFERENCES: ADMIN vs REGULAR USER**
|
||||||
|
|
||||||
|
| Metric | Admin/Management | Regular User |
|
||||||
|
|--------|------------------|--------------|
|
||||||
|
| **Request Volume** | All organization requests | Only user-initiated requests |
|
||||||
|
| **TAT Efficiency** | All completed requests | Only user-initiated completed requests |
|
||||||
|
| **Approver Load** | N/A | User's own approval workload |
|
||||||
|
| **Engagement** | All work notes/documents | Only from user's requests |
|
||||||
|
| **AI Insights** | All approved requests | Only user's approved requests |
|
||||||
|
| **Department Stats** | ✅ Available | ❌ Not available |
|
||||||
|
| **Priority Distribution** | ✅ Available | ❌ Not available |
|
||||||
|
| **Approver Performance** | ✅ Available | ❌ Not available |
|
||||||
|
| **AI Remark Utilization** | ✅ Available | ❌ Not available |
|
||||||
|
| **Success Rate** | ❌ Not shown | ✅ Available |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 **IMPORTANT NOTES**
|
||||||
|
|
||||||
|
1. **Date Filtering:**
|
||||||
|
- Most metrics use `submission_date` (when request was submitted), not `created_at`
|
||||||
|
- Completed requests use `closure_date` or `updated_at` for completion date
|
||||||
|
- Pending requests are counted regardless of creation date
|
||||||
|
|
||||||
|
2. **Working Hours Calculation:**
|
||||||
|
- Cycle time uses `calculateElapsedWorkingHours()` which respects:
|
||||||
|
- Working hours (9 AM - 6 PM)
|
||||||
|
- Weekends (for STANDARD priority)
|
||||||
|
- Holidays (configured in system)
|
||||||
|
- Priority type (EXPRESS vs STANDARD)
|
||||||
|
|
||||||
|
3. **Breach Tracking:**
|
||||||
|
- Breaches are tracked at **approver/level level** (each level has its own TAT)
|
||||||
|
- But SLA compliance counts at **request level** (if any level breaches, entire request is non-compliant)
|
||||||
|
|
||||||
|
4. **Rounding:**
|
||||||
|
- Percentages: Rounded to nearest integer (0 decimal places)
|
||||||
|
- Hours: Rounded to 1 decimal place
|
||||||
|
- Days: Rounded to 1 decimal place
|
||||||
|
|
||||||
|
5. **Null Handling:**
|
||||||
|
- All COUNT operations handle NULL values
|
||||||
|
- Division operations use NULLIF to prevent division by zero
|
||||||
|
- Default values are 0 if no data exists
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 **DATA REFRESH**
|
||||||
|
|
||||||
|
All calculations are performed in real-time when:
|
||||||
|
- Dashboard is loaded
|
||||||
|
- Date range filter is changed
|
||||||
|
- Refresh button is clicked
|
||||||
|
- Custom date range is applied
|
||||||
|
|
||||||
|
Data is fetched in parallel for optimal performance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated:** Based on codebase as of current date
|
||||||
|
**Version:** 1.0
|
||||||
|
|
||||||
@ -36,6 +36,20 @@ interface ApprovalStepCardProps {
|
|||||||
testId?: string;
|
testId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to format working hours as days (8 hours = 1 working day)
|
||||||
|
const formatWorkingHours = (hours: number): string => {
|
||||||
|
const WORKING_HOURS_PER_DAY = 8;
|
||||||
|
if (hours < WORKING_HOURS_PER_DAY) {
|
||||||
|
return `${hours.toFixed(1)}h`;
|
||||||
|
}
|
||||||
|
const days = Math.floor(hours / WORKING_HOURS_PER_DAY);
|
||||||
|
const remainingHours = hours % WORKING_HOURS_PER_DAY;
|
||||||
|
if (remainingHours > 0) {
|
||||||
|
return `${days}d ${remainingHours.toFixed(1)}h`;
|
||||||
|
}
|
||||||
|
return `${days}d`;
|
||||||
|
};
|
||||||
|
|
||||||
const getStepIcon = (status: string, isSkipped?: boolean) => {
|
const getStepIcon = (status: string, isSkipped?: boolean) => {
|
||||||
if (isSkipped) return <AlertCircle className="w-4 h-4 sm:w-5 sm:h-5 text-orange-600" />;
|
if (isSkipped) return <AlertCircle className="w-4 h-4 sm:w-5 sm:h-5 text-orange-600" />;
|
||||||
|
|
||||||
@ -143,7 +157,7 @@ export function ApprovalStepCard({
|
|||||||
)}
|
)}
|
||||||
{isCompleted && actualHours && (
|
{isCompleted && actualHours && (
|
||||||
<Badge className="bg-green-600 text-white text-xs" data-testid={`${testId}-completion-time`}>
|
<Badge className="bg-green-600 text-white text-xs" data-testid={`${testId}-completion-time`}>
|
||||||
{actualHours.toFixed(1)} hours
|
{formatWorkingHours(actualHours)}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -167,7 +181,7 @@ export function ApprovalStepCard({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between text-xs">
|
<div className="flex items-center justify-between text-xs">
|
||||||
<span className="text-gray-600">Completed in:</span>
|
<span className="text-gray-600">Completed in:</span>
|
||||||
<span className="font-medium text-gray-900">{actualHours.toFixed(1)} hours</span>
|
<span className="font-medium text-gray-900">{formatWorkingHours(actualHours)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Progress Bar for Completed - Shows actual time used vs TAT allocated */}
|
{/* Progress Bar for Completed - Shows actual time used vs TAT allocated */}
|
||||||
|
|||||||
@ -335,8 +335,10 @@ export function useRequestDetails(
|
|||||||
elapsedHours: Number(a.elapsedHours || 0),
|
elapsedHours: Number(a.elapsedHours || 0),
|
||||||
remainingHours: Number(a.remainingHours || 0),
|
remainingHours: Number(a.remainingHours || 0),
|
||||||
tatPercentageUsed: Number(a.tatPercentageUsed || 0),
|
tatPercentageUsed: Number(a.tatPercentageUsed || 0),
|
||||||
actualHours: a.levelEndTime && a.levelStartTime
|
// Use backend-calculated elapsedHours (working hours) for completed approvals
|
||||||
? Math.max(0, (new Date(a.levelEndTime).getTime() - new Date(a.levelStartTime).getTime()) / (1000 * 60 * 60))
|
// Backend already calculates this correctly using calculateElapsedWorkingHours
|
||||||
|
actualHours: a.elapsedHours !== undefined && a.elapsedHours !== null
|
||||||
|
? Number(a.elapsedHours)
|
||||||
: undefined,
|
: undefined,
|
||||||
comment: a.comments || undefined,
|
comment: a.comments || undefined,
|
||||||
timestamp: a.actionDate || undefined,
|
timestamp: a.actionDate || undefined,
|
||||||
|
|||||||
@ -56,6 +56,7 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
|
|||||||
const [approverPerformance, setApproverPerformance] = useState<ApproverPerformance[]>([]);
|
const [approverPerformance, setApproverPerformance] = useState<ApproverPerformance[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
const [exportingDeptStats, setExportingDeptStats] = useState(false);
|
||||||
|
|
||||||
// Filter states
|
// Filter states
|
||||||
const [dateRange, setDateRange] = useState<DateRange>('month');
|
const [dateRange, setDateRange] = useState<DateRange>('month');
|
||||||
@ -291,6 +292,48 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Export Department Stats to CSV
|
||||||
|
const exportDepartmentStatsToCSV = async () => {
|
||||||
|
try {
|
||||||
|
setExportingDeptStats(true);
|
||||||
|
|
||||||
|
// Get all department stats (not just the displayed ones)
|
||||||
|
const allDeptStats = await dashboardService.getDepartmentStats(dateRange, customStartDate, customEndDate);
|
||||||
|
|
||||||
|
const csvRows = [
|
||||||
|
['Department', 'Total Requests', 'Approved', 'Rejected', 'In Progress', 'Approval Rate (%)'].join(',')
|
||||||
|
];
|
||||||
|
|
||||||
|
allDeptStats.forEach((dept: any) => {
|
||||||
|
const row = [
|
||||||
|
`"${(dept.department || 'Unknown').replace(/"/g, '""')}"`,
|
||||||
|
dept.totalRequests || 0,
|
||||||
|
dept.approved || 0,
|
||||||
|
dept.rejected || 0,
|
||||||
|
dept.inProgress || 0,
|
||||||
|
dept.approvalRate || 0
|
||||||
|
];
|
||||||
|
csvRows.push(row.join(','));
|
||||||
|
});
|
||||||
|
|
||||||
|
const csvContent = csvRows.join('\n');
|
||||||
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
link.setAttribute('href', url);
|
||||||
|
link.setAttribute('download', `department-workflow-summary-${new Date().toISOString().split('T')[0]}.csv`);
|
||||||
|
link.style.visibility = 'hidden';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to export department stats:', error);
|
||||||
|
alert('Failed to export department statistics. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setExportingDeptStats(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Helper function for activity pagination page numbers
|
// Helper function for activity pagination page numbers
|
||||||
const getActivityPageNumbers = () => {
|
const getActivityPageNumbers = () => {
|
||||||
const pages = [];
|
const pages = [];
|
||||||
@ -540,13 +583,6 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
|
|||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Export Button */}
|
|
||||||
{isAdmin && (
|
|
||||||
<Button variant="outline" size="sm" className="gap-2">
|
|
||||||
<Download className="w-4 h-4" />
|
|
||||||
Export
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -1022,9 +1058,17 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" className="gap-2 self-start sm:self-auto">
|
<Button
|
||||||
<Download className="w-3 h-3 sm:w-4 sm:h-4" />
|
variant="outline"
|
||||||
<span className="text-xs sm:text-sm">Export</span>
|
size="sm"
|
||||||
|
className="gap-2 self-start sm:self-auto"
|
||||||
|
onClick={exportDepartmentStatsToCSV}
|
||||||
|
disabled={exportingDeptStats}
|
||||||
|
>
|
||||||
|
<Download className={`w-3 h-3 sm:w-4 sm:h-4 ${exportingDeptStats ? 'animate-pulse' : ''}`} />
|
||||||
|
<span className="text-xs sm:text-sm">
|
||||||
|
{exportingDeptStats ? 'Exporting...' : 'Export'}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@ -1333,7 +1377,7 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Calendar className="h-4 w-4 sm:h-5 sm:w-5 text-orange-600" />
|
<CalendarIcon className="h-4 w-4 sm:h-5 sm:w-5 text-orange-600" />
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-base sm:text-lg">Upcoming Deadlines</CardTitle>
|
<CardTitle className="text-base sm:text-lg">Upcoming Deadlines</CardTitle>
|
||||||
<CardDescription className="text-xs sm:text-sm">
|
<CardDescription className="text-xs sm:text-sm">
|
||||||
|
|||||||
@ -98,12 +98,14 @@ export function DetailedReports({ onBack }: DetailedReportsProps) {
|
|||||||
return diffDays;
|
return diffDays;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to format TAT hours
|
// Helper function to format TAT hours (working hours to working days)
|
||||||
|
// Backend returns working hours, so we divide by 8 (working hours per day) not 24
|
||||||
const formatTAT = (hours: number | null | undefined): string => {
|
const formatTAT = (hours: number | null | undefined): string => {
|
||||||
if (!hours && hours !== 0) return 'N/A';
|
if (!hours && hours !== 0) return 'N/A';
|
||||||
if (hours < 24) return `${hours.toFixed(1)}h`;
|
const WORKING_HOURS_PER_DAY = 8;
|
||||||
const days = Math.floor(hours / 24);
|
if (hours < WORKING_HOURS_PER_DAY) return `${hours.toFixed(1)}h`;
|
||||||
const remainingHours = hours % 24;
|
const days = Math.floor(hours / WORKING_HOURS_PER_DAY);
|
||||||
|
const remainingHours = hours % WORKING_HOURS_PER_DAY;
|
||||||
return remainingHours > 0 ? `${days}d ${remainingHours.toFixed(1)}h` : `${days}d`;
|
return remainingHours > 0 ? `${days}d ${remainingHours.toFixed(1)}h` : `${days}d`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user