Spaces:
Sleeping
Sleeping
updated meta apps and made overview return just the data needed
Browse files
docs/agent/frontend/FIELD_AGENT_DASHBOARD_API.md
CHANGED
|
@@ -18,7 +18,10 @@ Enhanced the existing `GET /api/v1/analytics/user/overview` endpoint to support
|
|
| 18 |
|
| 19 |
## Response Structure
|
| 20 |
|
| 21 |
-
### For
|
|
|
|
|
|
|
|
|
|
| 22 |
```json
|
| 23 |
{
|
| 24 |
"user_info": {
|
|
@@ -31,39 +34,91 @@ Enhanced the existing `GET /api/v1/analytics/user/overview` endpoint to support
|
|
| 31 |
"total": 3,
|
| 32 |
"active": 2
|
| 33 |
},
|
| 34 |
-
"
|
| 35 |
-
"
|
| 36 |
-
"
|
| 37 |
-
"in_progress":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
},
|
| 39 |
"notifications": {
|
| 40 |
"unread": 5
|
| 41 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
"generated_at": "2025-11-27T10:30:00Z"
|
| 43 |
}
|
| 44 |
```
|
| 45 |
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
-
|
| 49 |
|
| 50 |
-
#### 1. Field Agent Stats
|
| 51 |
```json
|
| 52 |
-
|
| 53 |
-
"
|
| 54 |
-
"
|
| 55 |
-
"
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
}
|
| 58 |
```
|
| 59 |
|
| 60 |
-
|
| 61 |
-
- `hours_worked_this_week`: Sum of hours from Timesheet table (ISO week: Monday-Sunday)
|
| 62 |
-
- `pending_expenses_amount`: Sum of unapproved TicketExpense records
|
| 63 |
-
- `inventory_on_hand`: Count of active InventoryAssignment (not returned/installed/consumed)
|
| 64 |
-
- `tickets_completed_this_week`: Count of completed TicketAssignment (ISO week)
|
| 65 |
-
|
| 66 |
-
#### 2. Work Queue
|
| 67 |
```json
|
| 68 |
"work_queue": {
|
| 69 |
"pending_assignments": [
|
|
@@ -111,23 +166,23 @@ const data = await response.json();
|
|
| 111 |
|
| 112 |
**Display Summary Cards:**
|
| 113 |
```typescript
|
| 114 |
-
//
|
| 115 |
-
const
|
| 116 |
|
| 117 |
<Card title="Hours This Week">
|
| 118 |
-
{
|
| 119 |
</Card>
|
| 120 |
|
| 121 |
<Card title="Pending Expenses">
|
| 122 |
-
KES {
|
| 123 |
</Card>
|
| 124 |
|
| 125 |
<Card title="Inventory On Hand">
|
| 126 |
-
{
|
| 127 |
</Card>
|
| 128 |
|
| 129 |
<Card title="Completed This Week">
|
| 130 |
-
{
|
| 131 |
</Card>
|
| 132 |
```
|
| 133 |
|
|
|
|
| 18 |
|
| 19 |
## Response Structure
|
| 20 |
|
| 21 |
+
### For Field Agents/Sales Agents (Simplified Response)
|
| 22 |
+
|
| 23 |
+
Field agents receive a **personalized response** showing only THEIR work, not organization-wide stats:
|
| 24 |
+
|
| 25 |
```json
|
| 26 |
{
|
| 27 |
"user_info": {
|
|
|
|
| 34 |
"total": 3,
|
| 35 |
"active": 2
|
| 36 |
},
|
| 37 |
+
"my_tickets": {
|
| 38 |
+
"total_assigned": 15,
|
| 39 |
+
"pending": 4,
|
| 40 |
+
"in_progress": 2,
|
| 41 |
+
"completed_this_week": 9
|
| 42 |
+
},
|
| 43 |
+
"my_expenses": {
|
| 44 |
+
"total_amount": 12500.00,
|
| 45 |
+
"pending_approval": 3,
|
| 46 |
+
"pending_amount": 4500.00
|
| 47 |
+
},
|
| 48 |
+
"my_inventory": {
|
| 49 |
+
"items_on_hand": 8
|
| 50 |
+
},
|
| 51 |
+
"my_time": {
|
| 52 |
+
"hours_worked_this_week": 32.5
|
| 53 |
},
|
| 54 |
"notifications": {
|
| 55 |
"unread": 5
|
| 56 |
},
|
| 57 |
+
"work_queue": {
|
| 58 |
+
"pending_assignments": [...],
|
| 59 |
+
"total_pending": 4,
|
| 60 |
+
"high_priority": 1,
|
| 61 |
+
"due_today": 2
|
| 62 |
+
},
|
| 63 |
"generated_at": "2025-11-27T10:30:00Z"
|
| 64 |
}
|
| 65 |
```
|
| 66 |
|
| 67 |
+
**Key Differences from Manager Response:**
|
| 68 |
+
- ✅ `my_tickets` - Only tickets assigned to ME (not all project tickets)
|
| 69 |
+
- ✅ `my_expenses` - Only MY expenses (not team expenses)
|
| 70 |
+
- ✅ `my_inventory` - Only items I have (not warehouse inventory)
|
| 71 |
+
- ✅ `my_time` - MY hours worked
|
| 72 |
+
- ✅ `work_queue` - MY pending assignments
|
| 73 |
+
- ❌ No `team` stats (don't manage team)
|
| 74 |
+
- ❌ No `sales_orders` (not relevant)
|
| 75 |
+
- ❌ No organization-wide `inventory` value
|
| 76 |
+
|
| 77 |
+
**Metrics Explained:**
|
| 78 |
+
- `my_tickets.total_assigned`: All tickets ever assigned to me
|
| 79 |
+
- `my_tickets.pending`: Tickets with status "assigned" (not started)
|
| 80 |
+
- `my_tickets.in_progress`: Tickets with status "en_route" or "in_progress"
|
| 81 |
+
- `my_tickets.completed_this_week`: Completed this ISO week (Monday-Sunday)
|
| 82 |
+
- `my_expenses.total_amount`: Sum of all MY expenses (approved + pending)
|
| 83 |
+
- `my_expenses.pending_approval`: Count of MY unapproved expenses
|
| 84 |
+
- `my_expenses.pending_amount`: Sum of MY unapproved expenses
|
| 85 |
+
- `my_inventory.items_on_hand`: Equipment I collected but haven't returned/installed
|
| 86 |
+
- `my_time.hours_worked_this_week`: Hours from MY timesheets (ISO week)
|
| 87 |
+
|
| 88 |
+
### For Managers/Admins (Full Response)
|
| 89 |
|
| 90 |
+
Managers receive organization-wide stats:
|
| 91 |
|
|
|
|
| 92 |
```json
|
| 93 |
+
{
|
| 94 |
+
"user_info": {...},
|
| 95 |
+
"projects": {...},
|
| 96 |
+
"team": {
|
| 97 |
+
"total_members": 25
|
| 98 |
+
},
|
| 99 |
+
"tickets": {
|
| 100 |
+
"total": 150,
|
| 101 |
+
"open": 45,
|
| 102 |
+
"in_progress": 30
|
| 103 |
+
},
|
| 104 |
+
"expenses": {
|
| 105 |
+
"total_amount": 250000.00,
|
| 106 |
+
"pending_approval": 15
|
| 107 |
+
},
|
| 108 |
+
"sales_orders": {
|
| 109 |
+
"total": 80,
|
| 110 |
+
"pending": 20
|
| 111 |
+
},
|
| 112 |
+
"inventory": {
|
| 113 |
+
"total_value": 500000.00,
|
| 114 |
+
"active_assignments": 45
|
| 115 |
+
},
|
| 116 |
+
"notifications": {...},
|
| 117 |
+
"generated_at": "..."
|
| 118 |
}
|
| 119 |
```
|
| 120 |
|
| 121 |
+
### Work Queue Structure (Field Agents Only)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
```json
|
| 123 |
"work_queue": {
|
| 124 |
"pending_assignments": [
|
|
|
|
| 166 |
|
| 167 |
**Display Summary Cards:**
|
| 168 |
```typescript
|
| 169 |
+
// Field agent response has simplified structure
|
| 170 |
+
const { my_tickets, my_expenses, my_inventory, my_time } = data;
|
| 171 |
|
| 172 |
<Card title="Hours This Week">
|
| 173 |
+
{my_time.hours_worked_this_week} hrs
|
| 174 |
</Card>
|
| 175 |
|
| 176 |
<Card title="Pending Expenses">
|
| 177 |
+
KES {my_expenses.pending_amount.toLocaleString()}
|
| 178 |
</Card>
|
| 179 |
|
| 180 |
<Card title="Inventory On Hand">
|
| 181 |
+
{my_inventory.items_on_hand} items
|
| 182 |
</Card>
|
| 183 |
|
| 184 |
<Card title="Completed This Week">
|
| 185 |
+
{my_tickets.completed_this_week} tickets
|
| 186 |
</Card>
|
| 187 |
```
|
| 188 |
|
src/app/config/apps.py
CHANGED
|
@@ -464,8 +464,8 @@ META_APPS_BY_ROLE: Dict[str, List[str]] = {
|
|
| 464 |
"project_manager": ["overview", "projects", "users", "finance", "notifications", "settings", "help"],
|
| 465 |
"sales_manager": ["overview", "projects", "users", "finance", "notifications", "settings", "help"],
|
| 466 |
"dispatcher": ["overview", "projects", "users", "finance", "notifications", "settings", "help"],
|
| 467 |
-
"field_agent": ["overview", "
|
| 468 |
-
"sales_agent": ["overview", "
|
| 469 |
|
| 470 |
# Admin roles: No context switching - empty list means show all apps always
|
| 471 |
"platform_admin": [],
|
|
|
|
| 464 |
"project_manager": ["overview", "projects", "users", "finance", "notifications", "settings", "help"],
|
| 465 |
"sales_manager": ["overview", "projects", "users", "finance", "notifications", "settings", "help"],
|
| 466 |
"dispatcher": ["overview", "projects", "users", "finance", "notifications", "settings", "help"],
|
| 467 |
+
"field_agent": ["overview", "notifications", "projects", "payroll"],
|
| 468 |
+
"sales_agent": ["overview", "notifications", "projects", "payroll"],
|
| 469 |
|
| 470 |
# Admin roles: No context switching - empty list means show all apps always
|
| 471 |
"platform_admin": [],
|
src/app/services/dashboard_service.py
CHANGED
|
@@ -757,23 +757,45 @@ class DashboardService:
|
|
| 757 |
ProjectTeam.removed_at.is_(None)
|
| 758 |
).count()
|
| 759 |
|
| 760 |
-
# Ticket stats
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 777 |
|
| 778 |
# Unread notifications
|
| 779 |
unread_notifications = db.query(Notification).filter(
|
|
@@ -782,10 +804,36 @@ class DashboardService:
|
|
| 782 |
Notification.deleted_at.is_(None)
|
| 783 |
).count()
|
| 784 |
|
| 785 |
-
# Expense stats
|
| 786 |
total_expenses = 0.0
|
| 787 |
pending_expenses = 0
|
| 788 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 789 |
expense_sum = db.query(func.sum(TicketExpense.total_cost)).join(
|
| 790 |
Ticket
|
| 791 |
).filter(
|
|
@@ -851,49 +899,86 @@ class DashboardService:
|
|
| 851 |
if current_user.role in [AppRole.FIELD_AGENT.value, AppRole.SALES_AGENT.value]:
|
| 852 |
work_queue = DashboardService._get_work_queue(db, current_user, limit)
|
| 853 |
|
| 854 |
-
response
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
"
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
"
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
"
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
"
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 897 |
|
| 898 |
return response
|
| 899 |
|
|
|
|
| 757 |
ProjectTeam.removed_at.is_(None)
|
| 758 |
).count()
|
| 759 |
|
| 760 |
+
# Ticket stats - different for field agents vs managers
|
| 761 |
+
if current_user.role in [AppRole.FIELD_AGENT.value, AppRole.SALES_AGENT.value]:
|
| 762 |
+
# Field agents: Only their assigned tickets
|
| 763 |
+
from app.models.ticket_assignment import TicketAssignment
|
| 764 |
+
|
| 765 |
+
total_tickets = db.query(TicketAssignment).filter(
|
| 766 |
+
TicketAssignment.user_id == current_user.id,
|
| 767 |
+
TicketAssignment.deleted_at.is_(None)
|
| 768 |
+
).count()
|
| 769 |
+
|
| 770 |
+
open_tickets = db.query(TicketAssignment).filter(
|
| 771 |
+
TicketAssignment.user_id == current_user.id,
|
| 772 |
+
TicketAssignment.status.in_(["assigned"]),
|
| 773 |
+
TicketAssignment.deleted_at.is_(None)
|
| 774 |
+
).count()
|
| 775 |
+
|
| 776 |
+
in_progress_tickets = db.query(TicketAssignment).filter(
|
| 777 |
+
TicketAssignment.user_id == current_user.id,
|
| 778 |
+
TicketAssignment.status.in_(["en_route", "in_progress"]),
|
| 779 |
+
TicketAssignment.deleted_at.is_(None)
|
| 780 |
+
).count()
|
| 781 |
+
else:
|
| 782 |
+
# Managers/admins: All tickets in their projects
|
| 783 |
+
total_tickets = db.query(Ticket).filter(
|
| 784 |
+
Ticket.project_id.in_(project_ids) if project_ids else False,
|
| 785 |
+
Ticket.deleted_at.is_(None)
|
| 786 |
+
).count()
|
| 787 |
+
|
| 788 |
+
open_tickets = db.query(Ticket).filter(
|
| 789 |
+
Ticket.project_id.in_(project_ids) if project_ids else False,
|
| 790 |
+
Ticket.deleted_at.is_(None),
|
| 791 |
+
Ticket.status.in_(["open", "assigned"])
|
| 792 |
+
).count()
|
| 793 |
+
|
| 794 |
+
in_progress_tickets = db.query(Ticket).filter(
|
| 795 |
+
Ticket.project_id.in_(project_ids) if project_ids else False,
|
| 796 |
+
Ticket.deleted_at.is_(None),
|
| 797 |
+
Ticket.status == "in_progress"
|
| 798 |
+
).count()
|
| 799 |
|
| 800 |
# Unread notifications
|
| 801 |
unread_notifications = db.query(Notification).filter(
|
|
|
|
| 804 |
Notification.deleted_at.is_(None)
|
| 805 |
).count()
|
| 806 |
|
| 807 |
+
# Expense stats
|
| 808 |
total_expenses = 0.0
|
| 809 |
pending_expenses = 0
|
| 810 |
+
|
| 811 |
+
if current_user.role in [AppRole.FIELD_AGENT.value, AppRole.SALES_AGENT.value]:
|
| 812 |
+
# Field agents: Only their own expenses
|
| 813 |
+
from app.models.ticket_assignment import TicketAssignment
|
| 814 |
+
|
| 815 |
+
expense_sum = db.query(func.sum(TicketExpense.total_cost)).join(
|
| 816 |
+
Ticket
|
| 817 |
+
).join(
|
| 818 |
+
TicketAssignment, TicketAssignment.ticket_id == Ticket.id
|
| 819 |
+
).filter(
|
| 820 |
+
TicketAssignment.user_id == current_user.id,
|
| 821 |
+
TicketExpense.deleted_at.is_(None)
|
| 822 |
+
).scalar()
|
| 823 |
+
total_expenses = float(expense_sum) if expense_sum else 0.0
|
| 824 |
+
|
| 825 |
+
pending_expenses = db.query(TicketExpense).join(
|
| 826 |
+
Ticket
|
| 827 |
+
).join(
|
| 828 |
+
TicketAssignment, TicketAssignment.ticket_id == Ticket.id
|
| 829 |
+
).filter(
|
| 830 |
+
TicketAssignment.user_id == current_user.id,
|
| 831 |
+
TicketExpense.deleted_at.is_(None),
|
| 832 |
+
TicketExpense.is_approved == False
|
| 833 |
+
).count()
|
| 834 |
+
|
| 835 |
+
elif current_user.role in [AppRole.PROJECT_MANAGER.value, AppRole.CONTRACTOR_ADMIN.value, AppRole.CLIENT_ADMIN.value, AppRole.PLATFORM_ADMIN.value]:
|
| 836 |
+
# Managers/admins: All expenses in their projects
|
| 837 |
expense_sum = db.query(func.sum(TicketExpense.total_cost)).join(
|
| 838 |
Ticket
|
| 839 |
).filter(
|
|
|
|
| 899 |
if current_user.role in [AppRole.FIELD_AGENT.value, AppRole.SALES_AGENT.value]:
|
| 900 |
work_queue = DashboardService._get_work_queue(db, current_user, limit)
|
| 901 |
|
| 902 |
+
# Build response based on role
|
| 903 |
+
if current_user.role in [AppRole.FIELD_AGENT.value, AppRole.SALES_AGENT.value]:
|
| 904 |
+
# Simplified response for field agents - only what they care about
|
| 905 |
+
response = {
|
| 906 |
+
"user_info": {
|
| 907 |
+
"id": str(current_user.id),
|
| 908 |
+
"name": current_user.name,
|
| 909 |
+
"email": current_user.email,
|
| 910 |
+
"role": current_user.role
|
| 911 |
+
},
|
| 912 |
+
"projects": {
|
| 913 |
+
"total": total_projects,
|
| 914 |
+
"active": active_projects
|
| 915 |
+
},
|
| 916 |
+
"my_tickets": {
|
| 917 |
+
"total_assigned": total_tickets,
|
| 918 |
+
"pending": open_tickets,
|
| 919 |
+
"in_progress": in_progress_tickets,
|
| 920 |
+
"completed_this_week": field_agent_stats["tickets_completed_this_week"] if field_agent_stats else 0
|
| 921 |
+
},
|
| 922 |
+
"my_expenses": {
|
| 923 |
+
"total_amount": total_expenses,
|
| 924 |
+
"pending_approval": pending_expenses,
|
| 925 |
+
"pending_amount": field_agent_stats["pending_expenses_amount"] if field_agent_stats else 0.0
|
| 926 |
+
},
|
| 927 |
+
"my_inventory": {
|
| 928 |
+
"items_on_hand": field_agent_stats["inventory_on_hand"] if field_agent_stats else 0
|
| 929 |
+
},
|
| 930 |
+
"my_time": {
|
| 931 |
+
"hours_worked_this_week": field_agent_stats["hours_worked_this_week"] if field_agent_stats else 0.0
|
| 932 |
+
},
|
| 933 |
+
"notifications": {
|
| 934 |
+
"unread": unread_notifications
|
| 935 |
+
},
|
| 936 |
+
"work_queue": work_queue if work_queue else {
|
| 937 |
+
"pending_assignments": [],
|
| 938 |
+
"total_pending": 0,
|
| 939 |
+
"high_priority": 0,
|
| 940 |
+
"due_today": 0
|
| 941 |
+
},
|
| 942 |
+
"generated_at": datetime.utcnow().isoformat() + "Z"
|
| 943 |
+
}
|
| 944 |
+
else:
|
| 945 |
+
# Full response for managers/admins
|
| 946 |
+
response = {
|
| 947 |
+
"user_info": {
|
| 948 |
+
"id": str(current_user.id),
|
| 949 |
+
"name": current_user.name,
|
| 950 |
+
"email": current_user.email,
|
| 951 |
+
"role": current_user.role
|
| 952 |
+
},
|
| 953 |
+
"projects": {
|
| 954 |
+
"total": total_projects,
|
| 955 |
+
"active": active_projects
|
| 956 |
+
},
|
| 957 |
+
"team": {
|
| 958 |
+
"total_members": total_team_members
|
| 959 |
+
},
|
| 960 |
+
"tickets": {
|
| 961 |
+
"total": total_tickets,
|
| 962 |
+
"open": open_tickets,
|
| 963 |
+
"in_progress": in_progress_tickets
|
| 964 |
+
},
|
| 965 |
+
"notifications": {
|
| 966 |
+
"unread": unread_notifications
|
| 967 |
+
},
|
| 968 |
+
"expenses": {
|
| 969 |
+
"total_amount": total_expenses,
|
| 970 |
+
"pending_approval": pending_expenses
|
| 971 |
+
},
|
| 972 |
+
"sales_orders": {
|
| 973 |
+
"total": total_orders,
|
| 974 |
+
"pending": pending_orders
|
| 975 |
+
},
|
| 976 |
+
"inventory": {
|
| 977 |
+
"total_value": total_inventory_value,
|
| 978 |
+
"active_assignments": active_assignments
|
| 979 |
+
},
|
| 980 |
+
"generated_at": datetime.utcnow().isoformat() + "Z"
|
| 981 |
+
}
|
| 982 |
|
| 983 |
return response
|
| 984 |
|