kamau1 commited on
Commit
133af2e
·
1 Parent(s): ac99973

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 All Users (Existing)
 
 
 
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
- "tickets": {
35
- "total": 45,
36
- "open": 12,
37
- "in_progress": 8
 
 
 
 
 
 
 
 
 
 
 
 
38
  },
39
  "notifications": {
40
  "unread": 5
41
  },
 
 
 
 
 
 
42
  "generated_at": "2025-11-27T10:30:00Z"
43
  }
44
  ```
45
 
46
- ### For Field Agents/Sales Agents (NEW)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
- **Additional Fields:**
49
 
50
- #### 1. Field Agent Stats
51
  ```json
52
- "field_agent_stats": {
53
- "hours_worked_this_week": 32.5,
54
- "pending_expenses_amount": 4500.00,
55
- "inventory_on_hand": 8,
56
- "tickets_completed_this_week": 15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  }
58
  ```
59
 
60
- **Metrics Explained:**
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
- // Use field_agent_stats for summary cards
115
- const stats = data.field_agent_stats;
116
 
117
  <Card title="Hours This Week">
118
- {stats.hours_worked_this_week} hrs
119
  </Card>
120
 
121
  <Card title="Pending Expenses">
122
- KES {stats.pending_expenses_amount.toLocaleString()}
123
  </Card>
124
 
125
  <Card title="Inventory On Hand">
126
- {stats.inventory_on_hand} items
127
  </Card>
128
 
129
  <Card title="Completed This Week">
130
- {stats.tickets_completed_this_week} tickets
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", "settings", "notifications", "help"],
468
- "sales_agent": ["overview", "settings", "notifications", "help"],
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 across all projects
761
- total_tickets = db.query(Ticket).filter(
762
- Ticket.project_id.in_(project_ids) if project_ids else False,
763
- Ticket.deleted_at.is_(None)
764
- ).count()
765
-
766
- open_tickets = db.query(Ticket).filter(
767
- Ticket.project_id.in_(project_ids) if project_ids else False,
768
- Ticket.deleted_at.is_(None),
769
- Ticket.status.in_(["open", "assigned"])
770
- ).count()
771
-
772
- in_progress_tickets = db.query(Ticket).filter(
773
- Ticket.project_id.in_(project_ids) if project_ids else False,
774
- Ticket.deleted_at.is_(None),
775
- Ticket.status == "in_progress"
776
- ).count()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 (for managers/admins)
786
  total_expenses = 0.0
787
  pending_expenses = 0
788
- if current_user.role in [AppRole.PROJECT_MANAGER.value, AppRole.CONTRACTOR_ADMIN.value, AppRole.CLIENT_ADMIN.value, AppRole.PLATFORM_ADMIN.value]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- "user_info": {
856
- "id": str(current_user.id),
857
- "name": current_user.name,
858
- "email": current_user.email,
859
- "role": current_user.role
860
- },
861
- "projects": {
862
- "total": total_projects,
863
- "active": active_projects
864
- },
865
- "team": {
866
- "total_members": total_team_members
867
- },
868
- "tickets": {
869
- "total": total_tickets,
870
- "open": open_tickets,
871
- "in_progress": in_progress_tickets
872
- },
873
- "notifications": {
874
- "unread": unread_notifications
875
- },
876
- "expenses": {
877
- "total_amount": total_expenses,
878
- "pending_approval": pending_expenses
879
- },
880
- "sales_orders": {
881
- "total": total_orders,
882
- "pending": pending_orders
883
- },
884
- "inventory": {
885
- "total_value": total_inventory_value,
886
- "active_assignments": active_assignments
887
- },
888
- "generated_at": datetime.utcnow().isoformat() + "Z"
889
- }
890
-
891
- # Add field agent specific data
892
- if field_agent_stats:
893
- response["field_agent_stats"] = field_agent_stats
894
-
895
- if work_queue:
896
- response["work_queue"] = work_queue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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