kamau1 commited on
Commit
e02cc07
·
1 Parent(s): 80fa958

fix(tickets): resolve 500/422 errors by correcting response schema, timezone handling, and route ordering

Browse files
docs/agent/thoughts/frontend.md CHANGED
@@ -1,433 +1,731 @@
1
- # Frontend Team - Navigation & App Context Requirements
2
 
3
- **Date:** 2025-11-21
4
- **Topic:** Project Selection & Context-Aware App Navigation
5
- **Status:** Pending Backend Implementation
6
 
7
  ---
8
 
9
- ## Problem Statement
10
 
11
- We've implemented a project selection system for project-based roles (Project Manager, Sales Manager, Dispatcher, Field Agent). These users now have two distinct contexts:
 
 
 
 
 
12
 
13
- 1. **Outside Project Context**: `/dashboard` - Project selection view
14
- 2. **Inside Project Context**: `/project/:projectId` - Project-specific dashboard
15
 
16
- Currently, the navigation system shows all available apps regardless of context. This creates a poor UX because:
17
- - Users see project-specific apps (Tickets, Inventory, Map, etc.) when they're on the project selection screen
18
- - These apps are not functional without a project context
19
- - It's confusing and cluttered
20
 
21
- ## Proposed Solution
 
 
 
22
 
23
- We need the **backend to provide context-aware app lists** based on user role and current context.
 
 
 
 
24
 
25
- ---
 
 
 
26
 
27
- ## Backend API Requirements
 
28
 
29
- ### Current API Response Structure
30
- ```json
31
- GET /api/v1/users/me/preferences
32
 
33
- {
34
- "available_apps": [
35
- { "code": "dashboard", "name": "Dashboard", "route": "/dashboard" },
36
- { "code": "tickets", "name": "Tickets", "route": "/tickets" },
37
- { "code": "settings", "name": "Settings", "route": "/settings" },
38
- // ... all apps
39
- ],
40
- "current_favorites": ["dashboard", "tickets", "inventory"],
41
- "default_favorites": ["dashboard", "tickets", "map"]
42
- }
43
  ```
44
 
45
- ### Proposed API Response Structure
 
 
 
 
 
 
 
 
 
 
 
46
  ```json
47
- GET /api/v1/users/me/preferences
48
-
49
  {
50
- "available_apps": [
51
- { "code": "dashboard", "name": "Dashboard", "route": "/dashboard" },
52
- { "code": "tickets", "name": "Tickets", "route": "/tickets" },
53
- { "code": "inventory", "name": "Inventory", "route": "/inventory" },
54
- { "code": "settings", "name": "Settings", "route": "/settings" },
55
- { "code": "help", "name": "Help", "route": "/help" },
56
- // ... all apps user has access to
57
- ],
58
-
59
- "meta_apps": [
60
- "dashboard",
61
- "settings",
62
- "help",
63
- "notifications",
64
- "search"
65
- ],
66
-
67
- "current_favorites": ["dashboard", "tickets", "inventory"],
68
- "default_favorites": ["dashboard", "tickets", "map"]
 
 
 
 
 
 
 
 
 
69
  }
70
  ```
71
 
72
  ---
73
 
74
- ## App Categorization by Role
75
-
76
- ### For Project-Based Roles (PM, Sales Manager, Dispatcher, Field Agent)
77
-
78
- **Meta Apps** (Available outside project context):
79
- - `dashboard` - For returning to project selection
80
- - `settings` - User preferences
81
- - `help` - Help & support
82
- - `notifications` - User notifications
83
- - `search` - Global search (if applicable)
84
-
85
- **Project Apps** (Only available inside project context):
86
- - `tickets` - Work orders/tickets
87
- - `inventory` - Inventory management
88
- - `map` - Map view
89
- - `finance` - Financial data
90
- - `expenses` - Expense tracking
91
- - `payroll` - Payroll (if applicable)
92
- - `team` - Team management
93
- - `projects` - Project list (might be meta for some roles)
94
- - `reports` - Reporting
95
- - `customers` - Customer management
96
- - `documents` - Document management
97
-
98
- ### For Admin Roles (Platform Admin, Client Admin, Contractor Admin)
99
-
100
- **All apps are always available** - No context switching needed.
101
-
102
- Their apps might include:
103
- - `dashboard` - Admin dashboard
104
- - `organizations` - Organization management (Platform Admin)
105
- - `users` - User management
106
- - `activity` - Activity logs
107
- - `billing` - Billing (Platform Admin)
108
- - `invitations` - User invitations
109
- - `team` - Team management (Org Admins)
110
- - `projects` - Project management (Org Admins)
111
- - `settings` - Settings
112
- - `help` - Help
113
 
114
  ---
115
 
116
- ## Backend Implementation Requirements
 
 
 
117
 
118
- ### 1. Add `meta_apps` Field to User Preferences Response
 
 
 
 
 
 
 
 
 
 
119
 
120
- The backend should return a `meta_apps` array that lists app codes that are available **outside** of a project context.
121
 
122
- **Logic:**
123
- - For **project-based roles**: Return a curated list of meta apps
124
- - For **admin roles**: Return empty array `[]` or all apps (since they don't have context switching)
 
125
 
126
- ### 2. Role-Based Meta Apps Configuration
127
 
128
- Create a configuration mapping for each role:
 
 
 
129
 
130
- ```python
131
- # Example backend configuration
132
- META_APPS_BY_ROLE = {
133
- 'project_manager': ['dashboard', 'settings', 'help', 'notifications', 'search'],
134
- 'sales_manager': ['dashboard', 'settings', 'help', 'notifications', 'search'],
135
- 'dispatcher': ['dashboard', 'settings', 'help', 'notifications', 'search'],
136
- 'field_agent': ['dashboard', 'settings', 'help', 'notifications', 'search'],
137
-
138
- # Admin roles - no context switching, so empty or all apps
139
- 'platform_admin': [],
140
- 'client_admin': [],
141
- 'contractor_admin': [],
142
  }
143
  ```
144
 
145
- ### 3. Update Preferences Endpoint
146
 
147
- **Endpoint:** `GET /api/v1/users/me/preferences`
 
 
 
 
 
148
 
149
- **Response Schema:**
150
- ```typescript
151
  {
152
- available_apps: Array<{
153
- code: string;
154
- name: string;
155
- route: string;
156
- }>;
157
-
158
- meta_apps: string[]; // NEW FIELD - Array of app codes available outside project context
159
-
160
- current_favorites: string[];
161
- default_favorites: string[];
162
  }
163
  ```
164
 
165
- ### 4. Validation Rules
166
 
167
- - `meta_apps` should only contain codes that exist in `available_apps`
168
- - For admin roles, `meta_apps` can be empty or contain all app codes
169
- - For project-based roles, `meta_apps` should be a subset of `available_apps`
170
 
171
  ---
172
 
173
- ## Frontend Implementation Plan
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
- Once the backend provides `meta_apps`, we'll implement the following:
176
 
177
- ### 1. Detect Project Context
 
 
 
178
 
179
- ```typescript
180
- // In a new hook or context
181
- const location = useLocation();
182
- const isInProjectContext = location.pathname.startsWith('/project/');
 
 
 
183
  ```
184
 
185
- ### 2. Filter Apps in StickyHeader
186
 
187
- ```typescript
188
- const visibleFavoriteApps = useMemo(() => {
189
- const { meta_apps } = availableApps;
190
- const isProjectBasedRole = !['platform_admin', 'client_admin', 'contractor_admin'].includes(user.role);
191
-
192
- // If outside project context and user is project-based role
193
- if (!isInProjectContext && isProjectBasedRole) {
194
- // Show only meta apps from favorites
195
- return accessibleFavoriteApps.filter(app => meta_apps.includes(app.code));
196
- }
197
-
198
- // Otherwise show all favorites
199
- return accessibleFavoriteApps;
200
- }, [isInProjectContext, user.role, accessibleFavoriteApps, availableApps.meta_apps]);
201
- ```
202
-
203
- ### 3. Filter Apps in AppLauncher
204
-
205
- ```typescript
206
- const visibleApps = useMemo(() => {
207
- const { meta_apps } = availableApps;
208
- const isProjectBasedRole = !['platform_admin', 'client_admin', 'contractor_admin'].includes(user.role);
209
-
210
- // If outside project context and user is project-based role
211
- if (!isInProjectContext && isProjectBasedRole) {
212
- // Show only meta apps
213
- return accessibleApps.filter(app => meta_apps.includes(app.code));
214
- }
215
-
216
- // Otherwise show all apps
217
- return accessibleApps;
218
- }, [isInProjectContext, user.role, accessibleApps, availableApps.meta_apps]);
219
  ```
220
 
221
- ### 4. Handle Favorites
222
 
223
- **Important:** When a user is outside a project context, their favorite apps should still be **saved** but just **not displayed** if they're project apps.
 
 
 
224
 
225
- **Behavior:**
226
- - User sets favorites: `['dashboard', 'tickets', 'inventory', 'settings']`
227
- - Outside project context: Only show `['dashboard', 'settings']` (meta apps)
228
- - Inside project context: Show all `['dashboard', 'tickets', 'inventory', 'settings']`
 
 
229
 
230
- This ensures when they enter a project, their favorites are immediately available.
231
 
232
  ---
233
 
234
- ## User Experience Flow
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
 
236
- ### Scenario 1: PM Logs In (Multiple Projects)
237
 
238
- 1. **Login** → Lands on `/dashboard` (project selection)
239
- 2. **Header Pills**: Shows only meta apps from favorites (e.g., Dashboard, Settings)
240
- 3. **App Launcher**: Shows only meta apps (Dashboard, Settings, Help, Notifications)
241
- 4. **User selects project** → Navigates to `/project/abc-123`
242
- 5. **Header Pills**: Now shows ALL favorites (Dashboard, Tickets, Inventory, Settings)
243
- 6. **App Launcher**: Shows ALL available apps
244
 
245
- ### Scenario 2: PM with Single Project (Auto-Navigate)
 
 
 
246
 
247
- 1. **Login** → Lands on `/dashboard`
248
- 2. **Auto-navigates** to `/project/abc-123` (single project)
249
- 3. **Header Pills**: Shows all favorites immediately
250
- 4. **App Launcher**: Shows all apps immediately
251
- 5. **User never sees the restricted view** (seamless experience)
 
 
 
 
252
 
253
- ### Scenario 3: Admin Logs In
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
- 1. **Login** → Lands on `/dashboard` (admin dashboard)
256
- 2. **Header Pills**: Shows all favorites (no filtering)
257
- 3. **App Launcher**: Shows all apps (no filtering)
258
- 4. **No context switching** - always sees everything
 
259
 
260
  ---
261
 
262
- ## Benefits of Backend-Driven Approach
 
 
 
 
 
 
 
 
 
 
 
 
263
 
264
- ### 1. **Centralized Logic**
265
- - App categorization lives in one place (backend)
266
- - Easy to update which apps are meta vs project-specific
267
- - No hardcoded lists in frontend
 
 
 
 
 
 
 
 
 
 
268
 
269
- ### 2. **Role Flexibility**
270
- - Different roles can have different meta apps
271
- - Easy to add new roles with custom meta app lists
272
- - Backend controls access, frontend just displays
273
 
274
- ### 3. **Consistency**
275
- - Same logic applies to all frontend components
276
- - No risk of frontend/backend mismatch
277
- - Single source of truth
278
 
279
- ### 4. **Scalability**
280
- - Easy to add new app types in the future
281
- - Can introduce more context levels if needed
282
- - Frontend code stays simple
283
 
284
- ### 5. **Security**
285
- - Backend enforces what apps are available
286
- - Frontend can't accidentally show unauthorized apps
287
- - Access control remains server-side
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
 
289
  ---
290
 
291
- ## Migration & Backward Compatibility
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
 
293
- ### Phase 1: Backend Update
294
- - Add `meta_apps` field to preferences response
295
- - Populate based on user role
296
- - Ensure existing fields remain unchanged
297
 
298
- ### Phase 2: Frontend Update
299
- - Update `useUserPreferences` types to include `meta_apps`
300
- - Implement filtering logic in StickyHeader
301
- - Implement filtering logic in AppLauncher
302
- - Add context detection
303
 
304
- ### Phase 3: Testing
305
- - Test all role types
306
- - Test project context switching
307
- - Test favorite app persistence
308
- - Test edge cases (direct URLs, bookmarks)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
 
310
- ### Backward Compatibility
311
- - If `meta_apps` is missing or empty, show all apps (current behavior)
312
- - No breaking changes for existing users
313
- - Gradual rollout possible
 
 
314
 
315
  ---
316
 
317
- ## Open Questions for Backend Team
 
 
 
318
 
319
- 1. **Should `meta_apps` be role-based or user-specific?**
320
- - Recommendation: Role-based for consistency, but allow user overrides if needed
 
 
 
 
 
321
 
322
- 2. **Should `projects` app be meta or project-specific?**
323
- - For PM: Probably meta (they manage multiple projects)
324
- - For Field Agent: Probably project-specific (they work in one project at a time)
 
 
 
 
325
 
326
- 3. **Should `dashboard` always be in meta apps?**
327
- - Yes - users need a way to return to project selection
328
 
329
- 4. **How to handle apps that work in both contexts?**
330
- - Example: `search` might work globally or within a project
331
- - Recommendation: If it works outside project, include in meta apps
 
332
 
333
- 5. **Should we support per-user customization of meta apps?**
334
- - Or is role-based sufficient?
335
- - Recommendation: Start with role-based, add user customization later if needed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
 
337
  ---
338
 
339
- ## Example API Responses by Role
 
 
 
 
 
340
 
341
- ### Project Manager
342
  ```json
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  {
344
- "available_apps": [
345
- { "code": "dashboard", "name": "Dashboard", "route": "/dashboard" },
346
- { "code": "tickets", "name": "Tickets", "route": "/tickets" },
347
- { "code": "inventory", "name": "Inventory", "route": "/inventory" },
348
- { "code": "map", "name": "Map", "route": "/map" },
349
- { "code": "finance", "name": "Finance", "route": "/finance" },
350
- { "code": "team", "name": "Team", "route": "/team" },
351
- { "code": "reports", "name": "Reports", "route": "/reports" },
352
- { "code": "settings", "name": "Settings", "route": "/settings" },
353
- { "code": "help", "name": "Help", "route": "/help" },
354
- { "code": "notifications", "name": "Notifications", "route": "/notifications" }
355
- ],
356
- "meta_apps": ["dashboard", "settings", "help", "notifications"],
357
- "current_favorites": ["dashboard", "tickets", "inventory", "map"],
358
- "default_favorites": ["dashboard", "tickets", "inventory", "map"]
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  }
360
  ```
361
 
362
- ### Platform Admin
363
- ```json
364
  {
365
- "available_apps": [
366
- { "code": "dashboard", "name": "Dashboard", "route": "/dashboard" },
367
- { "code": "organizations", "name": "Organizations", "route": "/organizations" },
368
- { "code": "users", "name": "Users", "route": "/users" },
369
- { "code": "activity", "name": "Activity", "route": "/activity" },
370
- { "code": "billing", "name": "Billing", "route": "/billing" },
371
- { "code": "invitations", "name": "Invitations", "route": "/invitations" },
372
- { "code": "settings", "name": "Settings", "route": "/settings" },
373
- { "code": "help", "name": "Help", "route": "/help" }
374
- ],
375
- "meta_apps": [], // Empty - no context switching for admins
376
- "current_favorites": ["dashboard", "organizations", "users"],
377
- "default_favorites": ["dashboard", "organizations", "users"]
 
 
 
 
 
 
378
  }
379
  ```
380
 
381
- ### Field Agent
382
- ```json
383
  {
384
- "available_apps": [
385
- { "code": "dashboard", "name": "Dashboard", "route": "/dashboard" },
386
- { "code": "tickets", "name": "Tickets", "route": "/tickets" },
387
- { "code": "map", "name": "Map", "route": "/map" },
388
- { "code": "inventory", "name": "Inventory", "route": "/inventory" },
389
- { "code": "settings", "name": "Settings", "route": "/settings" },
390
- { "code": "help", "name": "Help", "route": "/help" }
391
- ],
392
- "meta_apps": ["dashboard", "settings", "help"],
393
- "current_favorites": ["tickets", "map", "inventory"],
394
- "default_favorites": ["tickets", "map", "inventory"]
 
 
 
 
 
 
 
 
 
 
 
 
 
395
  }
396
  ```
397
 
398
  ---
399
 
400
- ## Summary
401
 
402
- **What we need from backend:**
403
- 1. Add `meta_apps: string[]` field to `/api/v1/users/me/preferences` response
404
- 2. Populate `meta_apps` based on user role
405
- 3. For project-based roles: Include apps that work outside project context
406
- 4. For admin roles: Empty array or all apps (no context switching)
407
 
408
- **What frontend will do:**
409
- 1. Detect if user is in project context (via route)
410
- 2. Filter favorite apps in header based on `meta_apps` when outside project
411
- 3. Filter all apps in launcher based on `meta_apps` when outside project
412
- 4. Show all apps normally when inside project or for admin roles
413
 
414
- **Result:**
415
- - Clean UX for project selection
416
- - No irrelevant apps shown
417
- - Seamless transition when entering/exiting projects
418
- - Backend controls app categorization
419
- - Frontend just displays what backend provides
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
 
421
  ---
422
 
423
- **Next Steps:**
424
- 1. Backend team reviews this proposal
425
- 2. Backend implements `meta_apps` field
426
- 3. Frontend team updates filtering logic
427
- 4. QA tests all role types and contexts
428
- 5. Deploy and monitor
 
 
 
 
429
 
430
  ---
431
 
432
- **Contact:** Frontend Team
433
- **For Questions:** Please review and provide feedback on the proposed API changes
 
 
 
 
 
 
 
1
+ # Ticketing System - Frontend Integration Guide
2
 
3
+ Quick reference for PM, Sales Manager, and Dispatcher ticket management.
 
 
4
 
5
  ---
6
 
7
+ ## User Roles & Permissions
8
 
9
+ | Role | View Tickets | Create Tickets | Assign Tickets | View Expenses | Approve Expenses |
10
+ |------|--------------|----------------|----------------|---------------|------------------|
11
+ | **Platform Admin** | All | ✓ | ✓ | All | ✓ |
12
+ | **Project Manager** | Their projects | ✓ | ✓ | Their projects | ✓ |
13
+ | **Dispatcher** | Contractor's projects | ✓ | ✓ | Contractor's projects | ✓ |
14
+ | **Sales Manager** | Their client's projects | View only | ✗ | View only | ✗ |
15
 
16
+ ---
 
17
 
18
+ ## Ticket Basics
 
 
 
19
 
20
+ ### Ticket Types
21
+ - **installation** - New customer service setup (from sales orders)
22
+ - **support** - Fix existing customer issues (from incidents)
23
+ - **infrastructure** - Project rollout work (from tasks)
24
 
25
+ ### Ticket Status Flow
26
+ ```
27
+ open → assigned → in_progress → pending_review → completed
28
+ ↘ cancelled
29
+ ```
30
 
31
+ ### Ticket Sources
32
+ - **sales_order** - Customer wants new service
33
+ - **incident** - Customer has problem with existing service
34
+ - **task** - Infrastructure work needed
35
 
36
+ ### Priority Levels
37
+ `urgent` > `high` > `normal` > `low`
38
 
39
+ ---
 
 
40
 
41
+ ## Core Endpoints
42
+
43
+ ### 1. List Tickets
44
+ ```http
45
+ GET /api/v1/tickets?skip=0&limit=50
 
 
 
 
 
46
  ```
47
 
48
+ **Query Params:**
49
+ - `project_id` (UUID) - Filter by project
50
+ - `status` (enum) - open, assigned, in_progress, pending_review, completed, cancelled
51
+ - `ticket_type` (enum) - installation, support, infrastructure
52
+ - `priority` (enum) - urgent, high, normal, low
53
+ - `project_region_id` (UUID) - Filter by region
54
+ - `source` (enum) - sales_order, incident, task
55
+ - `from_date` (date) - Created after (YYYY-MM-DD)
56
+ - `to_date` (date) - Created before (YYYY-MM-DD)
57
+ - `is_overdue` (bool) - true/false
58
+
59
+ **Response:**
60
  ```json
 
 
61
  {
62
+ "total": 150,
63
+ "skip": 0,
64
+ "limit": 50,
65
+ "tickets": [
66
+ {
67
+ "id": "uuid",
68
+ "project_id": "uuid",
69
+ "source": "sales_order",
70
+ "source_id": "uuid",
71
+ "ticket_name": "John Doe - Fiber Installation",
72
+ "ticket_type": "installation",
73
+ "service_type": "ftth",
74
+ "work_description": "Install fiber for customer",
75
+ "status": "open",
76
+ "priority": "normal",
77
+ "scheduled_date": "2024-03-20",
78
+ "scheduled_time_slot": "morning",
79
+ "due_date": "2024-03-20T17:00:00Z",
80
+ "sla_violated": false,
81
+ "is_overdue": false,
82
+ "project_region_id": "uuid",
83
+ "notes": "Customer prefers morning",
84
+ "created_at": "2024-03-15T10:00:00Z",
85
+ "updated_at": "2024-03-15T10:00:00Z",
86
+ "is_open": true,
87
+ "can_be_assigned": true
88
+ }
89
+ ]
90
  }
91
  ```
92
 
93
  ---
94
 
95
+ ### 2. Get Single Ticket
96
+ ```http
97
+ GET /api/v1/tickets/{ticket_id}
98
+ ```
99
+
100
+ **Response:** Same as ticket object above with full details.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
  ---
103
 
104
+ ### 3. Create Ticket from Sales Order
105
+ ```http
106
+ POST /api/v1/tickets/from-sales-order
107
+ ```
108
 
109
+ **Request:**
110
+ ```json
111
+ {
112
+ "sales_order_id": "uuid",
113
+ "priority": "normal",
114
+ "scheduled_date": "2024-03-20",
115
+ "scheduled_time_slot": "morning",
116
+ "work_description": "Install fiber for customer",
117
+ "notes": "Customer prefers morning visit"
118
+ }
119
+ ```
120
 
121
+ **Response:** Ticket object
122
 
123
+ **Rules:**
124
+ - Sales order must be `pending` status
125
+ - One sales order = one active ticket (prevents duplicates)
126
+ - If cancelled ticket exists, it gets reactivated instead
127
 
128
+ ---
129
 
130
+ ### 4. Create Ticket from Task
131
+ ```http
132
+ POST /api/v1/tickets/from-task
133
+ ```
134
 
135
+ **Request:**
136
+ ```json
137
+ {
138
+ "task_id": "uuid",
139
+ "priority": "normal",
140
+ "scheduled_date": "2024-03-20",
141
+ "scheduled_time_slot": "morning",
142
+ "notes": "Infrastructure work"
 
 
 
 
143
  }
144
  ```
145
 
146
+ **Response:** Ticket object
147
 
148
+ ---
149
+
150
+ ### 5. Create Ticket from Incident
151
+ ```http
152
+ POST /api/v1/tickets/from-incident
153
+ ```
154
 
155
+ **Request:**
156
+ ```json
157
  {
158
+ "incident_id": "uuid",
159
+ "priority": "urgent",
160
+ "scheduled_date": "2024-03-20",
161
+ "scheduled_time_slot": "morning",
162
+ "notes": "Customer reports no internet"
 
 
 
 
 
163
  }
164
  ```
165
 
166
+ **Response:** Ticket object
167
 
168
+ **Important:** Support tickets don't create new subscriptions (service already exists).
 
 
169
 
170
  ---
171
 
172
+ ### 6. Bulk Create from Sales Orders
173
+ ```http
174
+ POST /api/v1/tickets/bulk-from-sales-orders
175
+ ```
176
+
177
+ **Request:**
178
+ ```json
179
+ {
180
+ "sales_order_ids": ["uuid1", "uuid2", "uuid3"],
181
+ "priority": "normal",
182
+ "notes": "Batch promotion - March installations"
183
+ }
184
+ ```
185
+
186
+ **Response:**
187
+ ```json
188
+ {
189
+ "total": 3,
190
+ "successful": 2,
191
+ "failed": 1,
192
+ "errors": ["Sales order uuid3: Already has ticket"],
193
+ "created_ticket_ids": ["ticket-uuid1", "ticket-uuid2"]
194
+ }
195
+ ```
196
 
197
+ ---
198
 
199
+ ### 7. Update Ticket
200
+ ```http
201
+ PUT /api/v1/tickets/{ticket_id}
202
+ ```
203
 
204
+ **Request:**
205
+ ```json
206
+ {
207
+ "priority": "urgent",
208
+ "work_description": "Updated description",
209
+ "notes": "Additional notes"
210
+ }
211
  ```
212
 
213
+ **Rules:** Can only update `open` or `assigned` tickets.
214
 
215
+ ---
216
+
217
+ ### 8. Reschedule Ticket
218
+ ```http
219
+ POST /api/v1/tickets/{ticket_id}/reschedule
220
+ ```
221
+
222
+ **Request:**
223
+ ```json
224
+ {
225
+ "scheduled_date": "2024-03-25",
226
+ "scheduled_time_slot": "afternoon",
227
+ "reason": "Customer requested later date"
228
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  ```
230
 
231
+ ---
232
 
233
+ ### 9. Cancel Ticket
234
+ ```http
235
+ POST /api/v1/tickets/{ticket_id}/cancel
236
+ ```
237
 
238
+ **Request:**
239
+ ```json
240
+ {
241
+ "reason": "Customer cancelled service"
242
+ }
243
+ ```
244
 
245
+ **Rules:** Can only cancel `open` or `assigned` tickets.
246
 
247
  ---
248
 
249
+ ### 10. Get Ticket Stats
250
+ ```http
251
+ GET /api/v1/tickets/stats?project_id=uuid
252
+ ```
253
+
254
+ **Response:**
255
+ ```json
256
+ {
257
+ "total_tickets": 150,
258
+ "open_tickets": 20,
259
+ "assigned_tickets": 30,
260
+ "in_progress_tickets": 15,
261
+ "completed_tickets": 80,
262
+ "cancelled_tickets": 5,
263
+ "overdue_tickets": 3,
264
+ "installation_tickets": 100,
265
+ "support_tickets": 30,
266
+ "infrastructure_tickets": 20,
267
+ "urgent_tickets": 5,
268
+ "high_tickets": 15,
269
+ "normal_tickets": 100,
270
+ "low_tickets": 30,
271
+ "avg_completion_time_hours": 4.5,
272
+ "completion_rate": 53.33
273
+ }
274
+ ```
275
 
276
+ ---
277
 
278
+ ## Ticket Assignments
 
 
 
 
 
279
 
280
+ ### 11. Assign Ticket to Agent
281
+ ```http
282
+ POST /api/v1/ticket-assignments/tickets/{ticket_id}/assign
283
+ ```
284
 
285
+ **Request:**
286
+ ```json
287
+ {
288
+ "user_id": "agent-uuid",
289
+ "execution_order": 1,
290
+ "planned_start_time": "2024-03-20T09:00:00Z",
291
+ "notes": "Customer prefers morning visit"
292
+ }
293
+ ```
294
 
295
+ **Response:**
296
+ ```json
297
+ {
298
+ "id": "assignment-uuid",
299
+ "ticket_id": "ticket-uuid",
300
+ "user_id": "agent-uuid",
301
+ "assigned_by_user_id": "dispatcher-uuid",
302
+ "action": "assigned",
303
+ "execution_order": 1,
304
+ "planned_start_time": "2024-03-20T09:00:00Z",
305
+ "assigned_at": "2024-03-19T14:00:00Z",
306
+ "responded_at": null,
307
+ "journey_started_at": null,
308
+ "arrived_at": null,
309
+ "ended_at": null,
310
+ "is_active": true,
311
+ "notes": "Customer prefers morning visit"
312
+ }
313
+ ```
314
 
315
+ **Rules:**
316
+ - Ticket must be `open` status
317
+ - Agent must be on project team
318
+ - Agent capacity: max 4 active tickets
319
+ - No duplicate assignments
320
 
321
  ---
322
 
323
+ ### 12. Assign Ticket to Team
324
+ ```http
325
+ POST /api/v1/ticket-assignments/tickets/{ticket_id}/assign-team
326
+ ```
327
+
328
+ **Request:**
329
+ ```json
330
+ {
331
+ "user_ids": ["agent-a-uuid", "agent-b-uuid", "agent-c-uuid"],
332
+ "required_team_size": 3,
333
+ "notes": "Infrastructure team for pole installation"
334
+ }
335
+ ```
336
 
337
+ **Response:**
338
+ ```json
339
+ {
340
+ "ticket_id": "ticket-uuid",
341
+ "assignments": [
342
+ { "id": "assignment-1-uuid", "user_id": "agent-a-uuid" },
343
+ { "id": "assignment-2-uuid", "user_id": "agent-b-uuid" },
344
+ { "id": "assignment-3-uuid", "user_id": "agent-c-uuid" }
345
+ ],
346
+ "required_team_size": 3,
347
+ "successful": 3,
348
+ "failed": 0
349
+ }
350
+ ```
351
 
352
+ **Team Rules:**
353
+ - Any team member can complete the ticket
354
+ - Completing closes ALL active assignments
355
+ - All agents must have capacity
356
 
357
+ ---
 
 
 
358
 
359
+ ### 13. Get Ticket Assignments
360
+ ```http
361
+ GET /api/v1/ticket-assignments/tickets/{ticket_id}/assignments
362
+ ```
363
 
364
+ **Response:**
365
+ ```json
366
+ {
367
+ "current_assignments": [
368
+ {
369
+ "id": "uuid",
370
+ "user_id": "agent-uuid",
371
+ "user_name": "John Agent",
372
+ "assigned_at": "2024-03-19T14:00:00Z",
373
+ "responded_at": "2024-03-19T14:30:00Z",
374
+ "journey_started_at": "2024-03-20T08:00:00Z",
375
+ "arrived_at": "2024-03-20T09:15:00Z",
376
+ "is_active": true
377
+ }
378
+ ],
379
+ "past_assignments": [
380
+ {
381
+ "id": "uuid",
382
+ "user_id": "previous-agent-uuid",
383
+ "user_name": "Previous Agent",
384
+ "assigned_at": "2024-03-18T10:00:00Z",
385
+ "ended_at": "2024-03-18T11:00:00Z",
386
+ "action": "dropped",
387
+ "is_active": false
388
+ }
389
+ ]
390
+ }
391
+ ```
392
 
393
  ---
394
 
395
+ ### 14. Get Agent's Work Queue
396
+ ```http
397
+ GET /api/v1/ticket-assignments/users/{user_id}/assignment-queue
398
+ ```
399
+
400
+ **Response:**
401
+ ```json
402
+ {
403
+ "user_id": "agent-uuid",
404
+ "active_count": 3,
405
+ "max_capacity": 4,
406
+ "can_accept_more": true,
407
+ "assignments": [
408
+ {
409
+ "id": "assignment-uuid",
410
+ "ticket_id": "ticket-uuid",
411
+ "ticket_name": "John Doe - Installation",
412
+ "execution_order": 1,
413
+ "planned_start_time": "2024-03-20T09:00:00Z",
414
+ "priority": "high"
415
+ }
416
+ ]
417
+ }
418
+ ```
419
+
420
+ ---
421
 
422
+ ## Ticket Expenses
 
 
 
423
 
424
+ ### 15. Get Ticket Expenses
425
+ ```http
426
+ GET /api/v1/ticket-expenses?ticket_id=uuid
427
+ ```
 
428
 
429
+ **Response:**
430
+ ```json
431
+ {
432
+ "expenses": [
433
+ {
434
+ "id": "expense-uuid",
435
+ "ticket_id": "ticket-uuid",
436
+ "ticket_assignment_id": "assignment-uuid",
437
+ "incurred_by_user_id": "agent-uuid",
438
+ "incurred_by_user_name": "John Agent",
439
+ "category": "transport",
440
+ "description": "Taxi fare to customer site",
441
+ "quantity": 1,
442
+ "unit": "trip",
443
+ "unit_cost": 500.00,
444
+ "total_cost": 500.00,
445
+ "receipt_document_id": "doc-uuid",
446
+ "is_approved": false,
447
+ "is_paid": false,
448
+ "location_verified": true,
449
+ "notes": "Shared with 2 team members",
450
+ "created_at": "2024-03-20T10:00:00Z"
451
+ }
452
+ ],
453
+ "total": 1,
454
+ "page": 1,
455
+ "page_size": 50,
456
+ "pages": 1
457
+ }
458
+ ```
459
 
460
+ **Expense Categories:**
461
+ - `transport` - Travel costs
462
+ - `materials` - Materials purchased
463
+ - `meals` - Food/drinks
464
+ - `accommodation` - Lodging
465
+ - `other` - Other expenses
466
 
467
  ---
468
 
469
+ ### 16. Approve/Reject Expense
470
+ ```http
471
+ POST /api/v1/ticket-expenses/{expense_id}/approve
472
+ ```
473
 
474
+ **Request:**
475
+ ```json
476
+ {
477
+ "is_approved": true,
478
+ "rejection_reason": null
479
+ }
480
+ ```
481
 
482
+ **Or reject:**
483
+ ```json
484
+ {
485
+ "is_approved": false,
486
+ "rejection_reason": "Receipt not clear, please resubmit"
487
+ }
488
+ ```
489
 
490
+ ---
 
491
 
492
+ ### 17. Get Expense Stats
493
+ ```http
494
+ GET /api/v1/ticket-expenses/stats?ticket_id=uuid
495
+ ```
496
 
497
+ **Response:**
498
+ ```json
499
+ {
500
+ "total_expenses": 25,
501
+ "total_amount": 12500.00,
502
+ "approved_count": 20,
503
+ "approved_amount": 10000.00,
504
+ "pending_count": 3,
505
+ "pending_amount": 1500.00,
506
+ "rejected_count": 2,
507
+ "rejected_amount": 1000.00,
508
+ "paid_count": 15,
509
+ "paid_amount": 7500.00,
510
+ "by_category": {
511
+ "transport": 5000.00,
512
+ "materials": 4000.00,
513
+ "meals": 1500.00
514
+ }
515
+ }
516
+ ```
517
 
518
  ---
519
 
520
+ ## Ticket Images/Photos
521
+
522
+ ### 18. Get Ticket Equipment
523
+ ```http
524
+ GET /api/v1/tickets/{ticket_id}/equipment
525
+ ```
526
 
527
+ **Response:**
528
  ```json
529
+ [
530
+ {
531
+ "id": "uuid",
532
+ "unit_identifier": "HW12345678",
533
+ "equipment_type": "ONT",
534
+ "equipment_name": "Huawei HG8145V5",
535
+ "status": "installed",
536
+ "issued_at": "2025-11-15T08:00:00Z",
537
+ "installed_at": "2025-11-15T14:30:00Z",
538
+ "agent_name": "John Technician",
539
+ "agent_id": "agent-uuid"
540
+ }
541
+ ]
542
+ ```
543
+
544
+ ---
545
+
546
+ ## Database Models
547
+
548
+ ### Ticket Model
549
+ ```python
550
  {
551
+ "id": UUID,
552
+ "project_id": UUID,
553
+ "source": "sales_order" | "incident" | "task",
554
+ "source_id": UUID,
555
+ "ticket_name": str,
556
+ "ticket_type": "installation" | "support" | "infrastructure",
557
+ "service_type": "ftth" | "fixed_wireless" | "dsl" | etc,
558
+ "work_description": str,
559
+ "status": "open" | "assigned" | "in_progress" | "pending_review" | "completed" | "cancelled",
560
+ "priority": "low" | "normal" | "high" | "urgent",
561
+ "scheduled_date": date,
562
+ "scheduled_time_slot": str,
563
+ "due_date": datetime,
564
+ "sla_target_date": datetime,
565
+ "sla_violated": bool,
566
+ "started_at": datetime,
567
+ "completed_at": datetime,
568
+ "is_invoiced": bool,
569
+ "project_region_id": UUID,
570
+ "work_location_latitude": decimal,
571
+ "work_location_longitude": decimal,
572
+ "work_location_verified": bool,
573
+ "dedup_key": str, // Permanent unique identifier
574
+ "required_team_size": int,
575
+ "notes": str,
576
+ "version": int, // Optimistic locking
577
+ "created_at": datetime,
578
+ "updated_at": datetime
579
  }
580
  ```
581
 
582
+ ### TicketAssignment Model
583
+ ```python
584
  {
585
+ "id": UUID,
586
+ "ticket_id": UUID,
587
+ "user_id": UUID, // Agent
588
+ "assigned_by_user_id": UUID, // Dispatcher/PM
589
+ "action": "assigned" | "accepted" | "rejected" | "dropped",
590
+ "execution_order": int,
591
+ "planned_start_time": datetime,
592
+ "assigned_at": datetime,
593
+ "responded_at": datetime, // When agent accepted/rejected
594
+ "journey_started_at": datetime,
595
+ "arrived_at": datetime,
596
+ "ended_at": datetime, // When assignment closed
597
+ "is_self_assigned": bool,
598
+ "is_active": bool, // ended_at IS NULL
599
+ "journey_start_location": {lat, lng, accuracy},
600
+ "arrival_location": {lat, lng, accuracy},
601
+ "arrival_verified": bool,
602
+ "journey_location_history": [{lat, lng, timestamp, speed}], // GPS breadcrumbs
603
+ "notes": str
604
  }
605
  ```
606
 
607
+ ### TicketExpense Model
608
+ ```python
609
  {
610
+ "id": UUID,
611
+ "ticket_assignment_id": UUID,
612
+ "ticket_id": UUID,
613
+ "incurred_by_user_id": UUID,
614
+ "category": "transport" | "materials" | "meals" | "accommodation" | "other",
615
+ "description": str,
616
+ "quantity": decimal,
617
+ "unit": str,
618
+ "unit_cost": decimal,
619
+ "total_cost": decimal,
620
+ "receipt_document_id": UUID,
621
+ "location_verified": bool,
622
+ "is_approved": bool,
623
+ "approved_by_user_id": UUID,
624
+ "approved_at": datetime,
625
+ "rejection_reason": str,
626
+ "is_paid": bool,
627
+ "paid_to_user_id": UUID,
628
+ "paid_at": datetime,
629
+ "payment_reference": str,
630
+ "payment_recipient_type": "agent" | "vendor",
631
+ "payment_method": "send_money" | "till_number" | "paybill" | etc,
632
+ "payment_details": {}, // M-Pesa/bank details
633
+ "notes": str
634
  }
635
  ```
636
 
637
  ---
638
 
639
+ ## Key Business Rules
640
 
641
+ ### Ticket Creation
642
+ - One source (sales order/incident/task) = one ticket ever (permanent deduplication)
643
+ - Cancelled tickets can be reactivated instead of creating duplicates
644
+ - Installation tickets create subscriptions on completion
645
+ - Support tickets update existing subscriptions (no new subscription)
646
 
647
+ ### Ticket Assignment
648
+ - Agent capacity: max 4 active tickets
649
+ - Team assignments: any member can complete, closes all assignments
650
+ - Self-assignment: agents can pick from available pool in their region
 
651
 
652
+ ### Ticket Status Transitions
653
+ - `open` Can assign, update, reschedule, cancel
654
+ - `assigned` Can start journey, update, reschedule, cancel
655
+ - `in_progress` Can complete, cannot cancel
656
+ - `completed` Cannot modify
657
+ - `cancelled` Can reactivate by creating new ticket from same source
658
+
659
+ ### Expenses
660
+ - Must be linked to assignment
661
+ - Require approval before payment
662
+ - Support M-Pesa, bank transfer, cash payments
663
+ - Track location verification
664
+
665
+ ---
666
+
667
+ ## Common UI Flows
668
+
669
+ ### Flow 1: Create & Assign Installation Ticket
670
+ 1. `POST /tickets/from-sales-order` - Create ticket
671
+ 2. `POST /ticket-assignments/tickets/{id}/assign` - Assign to agent
672
+ 3. Agent accepts and completes work
673
+ 4. `GET /tickets/{id}` - View completed ticket
674
+
675
+ ### Flow 2: View Ticket Details
676
+ 1. `GET /tickets/{id}` - Get ticket
677
+ 2. `GET /ticket-assignments/tickets/{id}/assignments` - Get assignments
678
+ 3. `GET /ticket-expenses?ticket_id={id}` - Get expenses
679
+ 4. `GET /tickets/{id}/equipment` - Get equipment used
680
+
681
+ ### Flow 3: Manage Agent Workload
682
+ 1. `GET /ticket-assignments/users/{agent_id}/assignment-queue` - Check capacity
683
+ 2. If capacity available: `POST /ticket-assignments/tickets/{id}/assign`
684
+ 3. `GET /tickets/stats` - Monitor overall workload
685
+
686
+ ### Flow 4: Approve Expenses
687
+ 1. `GET /ticket-expenses?ticket_id={id}` - List expenses
688
+ 2. Review receipts and details
689
+ 3. `POST /ticket-expenses/{expense_id}/approve` - Approve/reject
690
+ 4. `GET /ticket-expenses/stats` - View expense summary
691
+
692
+ ---
693
+
694
+ ## Error Handling
695
+
696
+ Common error codes:
697
+ - `400` - Validation error (missing fields, invalid data)
698
+ - `403` - Forbidden (no permission for this project/ticket)
699
+ - `404` - Not found (ticket/assignment doesn't exist)
700
+ - `409` - Conflict (duplicate ticket, agent at capacity, ticket already assigned)
701
+
702
+ Error response format:
703
+ ```json
704
+ {
705
+ "detail": "Error message explaining what went wrong"
706
+ }
707
+ ```
708
 
709
  ---
710
 
711
+ ## Tips for Frontend
712
+
713
+ 1. **Polling**: Poll ticket status every 30-60s for real-time updates
714
+ 2. **Filters**: Use query params to filter tickets by status, priority, region
715
+ 3. **Pagination**: Default limit is 50, adjust based on UI needs
716
+ 4. **Computed Properties**: Use `is_overdue`, `can_be_assigned`, etc. for UI state
717
+ 5. **Team Size**: Check `required_team_size` vs `assigned_team_size` for team tickets
718
+ 6. **GPS Tracking**: Display `journey_location_history` on map for route visualization
719
+ 7. **Expense Approval**: Filter by `is_approved: false` for pending expenses
720
+ 8. **Stats Dashboard**: Use `/stats` endpoints for analytics/charts
721
 
722
  ---
723
 
724
+ ## Quick Reference: HTTP Methods
725
+
726
+ - `GET` - Retrieve data (list, single item, stats)
727
+ - `POST` - Create new resource or trigger action
728
+ - `PUT` - Update existing resource
729
+ - `DELETE` - Not used (soft deletes via cancel/drop)
730
+
731
+ All endpoints require authentication via Bearer token in `Authorization` header.
docs/devlogs/browser/browserconsole.txt CHANGED
@@ -1,105 +1,33 @@
1
- chunk-276SZO74.js?v=c40a0948:21551 Download the React DevTools for a better development experience: https://reactjs.org/link/react-devtools
2
- core.ts:167 %cGET%c http://localhost:8000/api/v1/auth/me
3
- react-router-dom.js?v=c40a0948:4393 ⚠️ React Router Future Flag Warning: React Router will begin wrapping state updates in `React.startTransition` in v7. You can use the `v7_startTransition` future flag to opt-in early. For more information, see https://reactrouter.com/v6/upgrading/future#v7_starttransition.
4
- warnOnce @ react-router-dom.js?v=c40a0948:4393
5
- logDeprecation @ react-router-dom.js?v=c40a0948:4396
6
- logV6DeprecationWarnings @ react-router-dom.js?v=c40a0948:4399
7
- (anonymous) @ react-router-dom.js?v=c40a0948:5271
8
- commitHookEffectListMount @ chunk-276SZO74.js?v=c40a0948:16915
9
- commitPassiveMountOnFiber @ chunk-276SZO74.js?v=c40a0948:18156
10
- commitPassiveMountEffects_complete @ chunk-276SZO74.js?v=c40a0948:18129
11
- commitPassiveMountEffects_begin @ chunk-276SZO74.js?v=c40a0948:18119
12
- commitPassiveMountEffects @ chunk-276SZO74.js?v=c40a0948:18109
13
- flushPassiveEffectsImpl @ chunk-276SZO74.js?v=c40a0948:19490
14
- flushPassiveEffects @ chunk-276SZO74.js?v=c40a0948:19447
15
- (anonymous) @ chunk-276SZO74.js?v=c40a0948:19328
16
- workLoop @ chunk-276SZO74.js?v=c40a0948:197
17
- flushWork @ chunk-276SZO74.js?v=c40a0948:176
18
- performWorkUntilDeadline @ chunk-276SZO74.js?v=c40a0948:384
19
- react-router-dom.js?v=c40a0948:4393 ⚠️ React Router Future Flag Warning: Relative route resolution within Splat routes is changing in v7. You can use the `v7_relativeSplatPath` future flag to opt-in early. For more information, see https://reactrouter.com/v6/upgrading/future#v7_relativesplatpath.
20
- warnOnce @ react-router-dom.js?v=c40a0948:4393
21
- logDeprecation @ react-router-dom.js?v=c40a0948:4396
22
- logV6DeprecationWarnings @ react-router-dom.js?v=c40a0948:4402
23
- (anonymous) @ react-router-dom.js?v=c40a0948:5271
24
- commitHookEffectListMount @ chunk-276SZO74.js?v=c40a0948:16915
25
- commitPassiveMountOnFiber @ chunk-276SZO74.js?v=c40a0948:18156
26
- commitPassiveMountEffects_complete @ chunk-276SZO74.js?v=c40a0948:18129
27
- commitPassiveMountEffects_begin @ chunk-276SZO74.js?v=c40a0948:18119
28
- commitPassiveMountEffects @ chunk-276SZO74.js?v=c40a0948:18109
29
- flushPassiveEffectsImpl @ chunk-276SZO74.js?v=c40a0948:19490
30
- flushPassiveEffects @ chunk-276SZO74.js?v=c40a0948:19447
31
- (anonymous) @ chunk-276SZO74.js?v=c40a0948:19328
32
- workLoop @ chunk-276SZO74.js?v=c40a0948:197
33
- flushWork @ chunk-276SZO74.js?v=c40a0948:176
34
- performWorkUntilDeadline @ chunk-276SZO74.js?v=c40a0948:384
35
- core.ts:167 GET http://localhost:8000/api/v1/auth/me → 200 (4.15s)
36
- core.ts:167 %cGET%c http://localhost:8000/api/v1/sales-orders/stats?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a
37
- core.ts:167 %cGET%c http://localhost:8000/api/v1/sales-orders?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a&page=1&per_page=20
38
- core.ts:117 ℹ️ [11:30:52] [COMPONENT] SalesOrdersDashboard: Sales orders dashboard loaded {projectId: '0ade6bd1-e492-4e25-b681-59f42058d29a'}
39
- core.ts:167 %cGET%c http://localhost:8000/api/v1/auth/me/preferences
40
- core.ts:167 GET http://localhost:8000/api/v1/sales-orders/stats?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a → 200 (5.92s)
41
- core.ts:167 GET http://localhost:8000/api/v1/sales-orders?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a&page=1&per_page=20 → 200 (12.56s)
42
- core.ts:167 %cGET%c http://localhost:8000/api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/regions
43
- core.ts:167 GET http://localhost:8000/api/v1/auth/me/preferences → 200 (14.25s)
44
- core.ts:167 GET http://localhost:8000/api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/regions → 200 (6.33s)
45
- core.ts:117 ℹ️ [11:47:03] [COMPONENT] SalesOrdersDashboard: Sales orders dashboard loaded {projectId: '0ade6bd1-e492-4e25-b681-59f42058d29a'}
46
- core.ts:167 %cGET%c http://localhost:8000/api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/regions
47
- core.ts:167 GET http://localhost:8000/api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/regions → 200 (5.81s)
48
- client:602 GET http://localhost:8080/src/components/sales-orders/CreateSalesOrderModal.tsx?t=1764071338286 net::ERR_ABORTED 500 (Internal Server Error)
49
- importUpdatedModule @ client:602
50
- fetchUpdate @ client:211
51
- queueUpdate @ client:186
52
- (anonymous) @ client:642
53
- handleMessage @ client:640
54
- (anonymous) @ client:550
55
- client:176 [hmr] Failed to reload /src/components/sales-orders/CreateSalesOrderModal.tsx. This could be due to syntax errors or importing non-existent modules. (see errors above)
56
- warnFailedUpdate @ client:176
57
- fetchUpdate @ client:213
58
- await in fetchUpdate
59
- queueUpdate @ client:186
60
- (anonymous) @ client:642
61
- handleMessage @ client:640
62
- (anonymous) @ client:550
63
- core.ts:167 %cGET%c http://localhost:8000/api/v1/customers?search=Sarah+Wangari
64
- api-client.ts:113 GET http://localhost:8000/api/v1/customers?search=Sarah+Wangari 403 (Forbidden)
65
- request @ api-client.ts:113
66
- get @ api-client.ts:179
67
- searchCustomers @ salesOrders.service.ts:78
68
- handleSearchCustomers @ CreateSalesOrderModal.tsx:123
69
- callCallback2 @ chunk-276SZO74.js?v=c40a0948:3674
70
- invokeGuardedCallbackDev @ chunk-276SZO74.js?v=c40a0948:3699
71
- invokeGuardedCallback @ chunk-276SZO74.js?v=c40a0948:3733
72
- invokeGuardedCallbackAndCatchFirstError @ chunk-276SZO74.js?v=c40a0948:3736
73
- executeDispatch @ chunk-276SZO74.js?v=c40a0948:7014
74
- processDispatchQueueItemsInOrder @ chunk-276SZO74.js?v=c40a0948:7034
75
- processDispatchQueue @ chunk-276SZO74.js?v=c40a0948:7043
76
- dispatchEventsForPlugins @ chunk-276SZO74.js?v=c40a0948:7051
77
- (anonymous) @ chunk-276SZO74.js?v=c40a0948:7174
78
- batchedUpdates$1 @ chunk-276SZO74.js?v=c40a0948:18913
79
- batchedUpdates @ chunk-276SZO74.js?v=c40a0948:3579
80
- dispatchEventForPluginEventSystem @ chunk-276SZO74.js?v=c40a0948:7173
81
- dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay @ chunk-276SZO74.js?v=c40a0948:5478
82
- dispatchEvent @ chunk-276SZO74.js?v=c40a0948:5472
83
- dispatchDiscreteEvent @ chunk-276SZO74.js?v=c40a0948:5449
84
- core.ts:167 GET http://localhost:8000/api/v1/customers?search=Sarah+Wangari → 403 (6.10s)
85
- CreateSalesOrderModal.tsx:126 Search failed APIError: Insufficient permissions. Required: view_customers
86
- at APIClient.request (api-client.ts:141:15)
87
- at async searchCustomers (salesOrders.service.ts:78:22)
88
- at async handleSearchCustomers (CreateSalesOrderModal.tsx:123:29)
89
- handleSearchCustomers @ CreateSalesOrderModal.tsx:126
90
- await in handleSearchCustomers
91
- callCallback2 @ chunk-276SZO74.js?v=c40a0948:3674
92
- invokeGuardedCallbackDev @ chunk-276SZO74.js?v=c40a0948:3699
93
- invokeGuardedCallback @ chunk-276SZO74.js?v=c40a0948:3733
94
- invokeGuardedCallbackAndCatchFirstError @ chunk-276SZO74.js?v=c40a0948:3736
95
- executeDispatch @ chunk-276SZO74.js?v=c40a0948:7014
96
- processDispatchQueueItemsInOrder @ chunk-276SZO74.js?v=c40a0948:7034
97
- processDispatchQueue @ chunk-276SZO74.js?v=c40a0948:7043
98
- dispatchEventsForPlugins @ chunk-276SZO74.js?v=c40a0948:7051
99
- (anonymous) @ chunk-276SZO74.js?v=c40a0948:7174
100
- batchedUpdates$1 @ chunk-276SZO74.js?v=c40a0948:18913
101
- batchedUpdates @ chunk-276SZO74.js?v=c40a0948:3579
102
- dispatchEventForPluginEventSystem @ chunk-276SZO74.js?v=c40a0948:7173
103
- dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay @ chunk-276SZO74.js?v=c40a0948:5478
104
- dispatchEvent @ chunk-276SZO74.js?v=c40a0948:5472
105
- dispatchDiscreteEvent @ chunk-276SZO74.js?v=c40a0948:5449
 
1
+ index-D-VTnNjg.js:533 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/auth/me
2
+ index-D-VTnNjg.js:533 GET https://kamau1-swiftops-backend.hf.space/api/v1/auth/me → 200 (1.75s)
3
+ index-D-VTnNjg.js:533 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/projects?page=1&per_page=100
4
+ index-D-VTnNjg.js:533 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/auth/me/preferences
5
+ index-D-VTnNjg.js:533 GET https://kamau1-swiftops-backend.hf.space/api/v1/projects?page=1&per_page=100 → 200 (637ms)
6
+ index-D-VTnNjg.js:533 GET https://kamau1-swiftops-backend.hf.space/api/v1/auth/me/preferences → 200 (963ms)
7
+ index-D-VTnNjg.js:533 ℹ️ [07:45:28] [COMPONENT] ProjectList: Project selected Object
8
+ index-D-VTnNjg.js:533 ℹ️ [07:45:28] [COMPONENT] ProjectList: Switching to project Object
9
+ index-D-VTnNjg.js:533 ℹ️ [07:45:28] [AUTH] Updating user preferences Object
10
+ index-D-VTnNjg.js:533 %cPUT%c https://kamau1-swiftops-backend.hf.space/api/v1/auth/me/preferences
11
+ index-D-VTnNjg.js:533 PUT https://kamau1-swiftops-backend.hf.space/api/v1/auth/me/preferences → 200 (737ms)
12
+ index-D-VTnNjg.js:533 ℹ️ [07:45:29] [AUTH] Preferences updated successfully Object
13
+ index-D-VTnNjg.js:533 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/auth/me
14
+ index-D-VTnNjg.js:533 GET https://kamau1-swiftops-backend.hf.space/api/v1/auth/me → 200 (388ms)
15
+ index-D-VTnNjg.js:533 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/dashboard
16
+ index-D-VTnNjg.js:533 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/activity-feed?limit=20
17
+ index-D-VTnNjg.js:533 ℹ️ [07:45:29] [COMPONENT] ProjectDashboardPage: Project dashboard loaded Object
18
+ index-D-VTnNjg.js:533 GET https://kamau1-swiftops-backend.hf.space/api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/dashboard → 200 (16.38s)
19
+ index-D-VTnNjg.js:533 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/sales-orders/stats?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a
20
+ index-D-VTnNjg.js:533 GET https://kamau1-swiftops-backend.hf.space/api/v1/projects/0ade6bd1-e492-4e25-b681-59f42058d29a/activity-feed?limit=20 → 200 (16.38s)
21
+ index-D-VTnNjg.js:533 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/tickets/stats?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a
22
+ index-D-VTnNjg.js:533 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/tickets?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a&skip=0&limit=50
23
+ index-D-VTnNjg.js:533 ℹ️ [07:45:33] [COMPONENT] TicketsDashboard: Tickets dashboard loaded Object
24
+ kamau1-swiftops-backend.hf.space/api/v1/tickets/stats?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a:1 Failed to load resource: the server responded with a status of 422 ()
25
+ index-D-VTnNjg.js:533 GET https://kamau1-swiftops-backend.hf.space/api/v1/tickets/stats?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a → 422 (900ms)
26
+ kamau1-swiftops-backend.hf.space/api/v1/tickets?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a&skip=0&limit=50:1 Failed to load resource: the server responded with a status of 500 ()
27
+ index-D-VTnNjg.js:533 GET https://kamau1-swiftops-backend.hf.space/api/v1/tickets?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a&skip=0&limit=50 → 500 (1.26s)
28
+ index-D-VTnNjg.js:533 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/tickets/stats?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a
29
+ kamau1-swiftops-backend.hf.space/api/v1/tickets/stats?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a:1 Failed to load resource: the server responded with a status of 422 ()
30
+ index-D-VTnNjg.js:533 GET https://kamau1-swiftops-backend.hf.space/api/v1/tickets/stats?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a → 422 (335ms)
31
+ index-D-VTnNjg.js:533 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/tickets?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a&skip=0&limit=50
32
+ kamau1-swiftops-backend.hf.space/api/v1/tickets?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a&skip=0&limit=50:1 Failed to load resource: the server responded with a status of 500 ()
33
+ index-D-VTnNjg.js:533 GET https://kamau1-swiftops-backend.hf.space/api/v1/tickets?project_id=0ade6bd1-e492-4e25-b681-59f42058d29a&skip=0&limit=50 → 500 (376ms)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/devlogs/server/runtimeerror.txt CHANGED
The diff for this file is too large to render. See raw diff
 
src/app/api/v1/tickets.py CHANGED
@@ -299,6 +299,67 @@ def list_tickets(
299
  )
300
 
301
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  @router.get(
303
  "/{ticket_id}",
304
  response_model=TicketResponse,
@@ -452,67 +513,6 @@ def cancel_ticket(
452
  return TicketResponse.from_orm(ticket)
453
 
454
 
455
- # ============================================
456
- # ANALYTICS
457
- # ============================================
458
-
459
- @router.get(
460
- "/stats",
461
- response_model=TicketStats,
462
- summary="Get ticket analytics"
463
- )
464
- def get_ticket_stats(
465
- project_id: Optional[UUID] = None,
466
- project_region_id: Optional[UUID] = None,
467
- db: Session = Depends(get_db),
468
- current_user: User = Depends(get_current_user)
469
- ):
470
- """
471
- Get ticket analytics and statistics.
472
-
473
- **Authorization:**
474
- - Platform Admin: All projects
475
- - Project Manager: Their projects only
476
- - Dispatcher: Their contractor's projects
477
- - Client Admin: Their client's projects
478
-
479
- **Filters:**
480
- - `project_id`: Stats for specific project
481
- - `project_region_id`: Stats for specific region
482
-
483
- **Returns:**
484
- - Counts by status (open, assigned, in_progress, completed, cancelled)
485
- - Counts by type (installation, support, infrastructure)
486
- - Counts by priority (urgent, high, normal, low)
487
- - Overdue/SLA violations
488
- - Average completion time
489
- - Completion rate
490
-
491
- **Example Response:**
492
- ```json
493
- {
494
- "total_tickets": 150,
495
- "open_tickets": 20,
496
- "assigned_tickets": 30,
497
- "in_progress_tickets": 15,
498
- "completed_tickets": 80,
499
- "cancelled_tickets": 5,
500
- "overdue_tickets": 3,
501
- "avg_completion_time_hours": 4.5,
502
- "completion_rate": 53.33
503
- }
504
- ```
505
- """
506
- stats = TicketService.get_ticket_stats(
507
- db=db,
508
- current_user=current_user,
509
- project_id=project_id,
510
- project_region_id=project_region_id
511
- )
512
-
513
- return stats
514
-
515
-
516
  @router.get(
517
  "/{ticket_id}/equipment",
518
  response_model=List[dict],
 
299
  )
300
 
301
 
302
+ # ============================================
303
+ # ANALYTICS
304
+ # ============================================
305
+
306
+ @router.get(
307
+ "/stats",
308
+ response_model=TicketStats,
309
+ summary="Get ticket analytics"
310
+ )
311
+ def get_ticket_stats(
312
+ project_id: Optional[UUID] = None,
313
+ project_region_id: Optional[UUID] = None,
314
+ db: Session = Depends(get_db),
315
+ current_user: User = Depends(get_current_user)
316
+ ):
317
+ """
318
+ Get ticket analytics and statistics.
319
+
320
+ **Authorization:**
321
+ - Platform Admin: All projects
322
+ - Project Manager: Their projects only
323
+ - Dispatcher: Their contractor's projects
324
+ - Client Admin: Their client's projects
325
+
326
+ **Filters:**
327
+ - `project_id`: Stats for specific project
328
+ - `project_region_id`: Stats for specific region
329
+
330
+ **Returns:**
331
+ - Counts by status (open, assigned, in_progress, completed, cancelled)
332
+ - Counts by type (installation, support, infrastructure)
333
+ - Counts by priority (urgent, high, normal, low)
334
+ - Overdue/SLA violations
335
+ - Average completion time
336
+ - Completion rate
337
+
338
+ **Example Response:**
339
+ ```json
340
+ {
341
+ "total_tickets": 150,
342
+ "open_tickets": 20,
343
+ "assigned_tickets": 30,
344
+ "in_progress_tickets": 15,
345
+ "completed_tickets": 80,
346
+ "cancelled_tickets": 5,
347
+ "overdue_tickets": 3,
348
+ "avg_completion_time_hours": 4.5,
349
+ "completion_rate": 53.33
350
+ }
351
+ ```
352
+ """
353
+ stats = TicketService.get_ticket_stats(
354
+ db=db,
355
+ current_user=current_user,
356
+ project_id=project_id,
357
+ project_region_id=project_region_id
358
+ )
359
+
360
+ return stats
361
+
362
+
363
  @router.get(
364
  "/{ticket_id}",
365
  response_model=TicketResponse,
 
513
  return TicketResponse.from_orm(ticket)
514
 
515
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
  @router.get(
517
  "/{ticket_id}/equipment",
518
  response_model=List[dict],
src/app/models/ticket.py CHANGED
@@ -161,7 +161,10 @@ class Ticket(BaseModel):
161
  """Check if ticket is overdue (past due_date and still active)"""
162
  if not self.due_date or not self.is_active:
163
  return False
164
- return datetime.utcnow() > self.due_date
 
 
 
165
 
166
  @property
167
  def has_region(self) -> bool:
 
161
  """Check if ticket is overdue (past due_date and still active)"""
162
  if not self.due_date or not self.is_active:
163
  return False
164
+ # Make datetime timezone-aware for comparison
165
+ from datetime import timezone
166
+ now = datetime.now(timezone.utc)
167
+ return now > self.due_date
168
 
169
  @property
170
  def has_region(self) -> bool:
src/app/schemas/ticket.py CHANGED
@@ -101,7 +101,6 @@ class TicketResponse(BaseModel):
101
  """Schema for ticket response with all details"""
102
  id: UUID
103
  project_id: UUID
104
- project_subcontractor_id: Optional[UUID]
105
 
106
  # Source tracking
107
  source: TicketSource
 
101
  """Schema for ticket response with all details"""
102
  id: UUID
103
  project_id: UUID
 
104
 
105
  # Source tracking
106
  source: TicketSource