0xarchit commited on
Commit
9bf5220
·
1 Parent(s): c2c654e

added demo worker file and refactor code for minor improvements

Browse files
Backend/database/seed.py CHANGED
@@ -22,7 +22,7 @@ async def seed_data(engine: AsyncEngine):
22
  code="PWD",
23
  description="Roads, Potholes, Infrastructure",
24
  default_sla_hours=48,
25
- escalation_email="pwd_head@city.gov"
26
  ),
27
  Department(
28
  id=sanitation_id,
@@ -30,7 +30,7 @@ async def seed_data(engine: AsyncEngine):
30
  code="SANITATION",
31
  description="Garbage, Cleaning, Waste",
32
  default_sla_hours=24,
33
- escalation_email="sanitation_head@city.gov"
34
  ),
35
  Department(
36
  id=traffic_id,
@@ -38,7 +38,7 @@ async def seed_data(engine: AsyncEngine):
38
  code="TRAFFIC",
39
  description="Signals, Signs, Illegal Parking",
40
  default_sla_hours=12,
41
- escalation_email="traffic_head@city.gov"
42
  )
43
  ]
44
 
 
22
  code="PWD",
23
  description="Roads, Potholes, Infrastructure",
24
  default_sla_hours=48,
25
+ escalation_email="zrxarchit+pwd_head@gmail.com"
26
  ),
27
  Department(
28
  id=sanitation_id,
 
30
  code="SANITATION",
31
  description="Garbage, Cleaning, Waste",
32
  default_sla_hours=24,
33
+ escalation_email="zrxarchit+sanitation_head@gmail.com"
34
  ),
35
  Department(
36
  id=traffic_id,
 
38
  code="TRAFFIC",
39
  description="Signals, Signs, Illegal Parking",
40
  default_sla_hours=12,
41
+ escalation_email="zrxarchit+traffic_head@gmail.com"
42
  )
43
  ]
44
 
Frontend/components/DashboardSidebar.tsx CHANGED
@@ -65,15 +65,17 @@ export default function DashboardSidebar({
65
  "fixed inset-y-0 left-0 z-50 bg-white/80 backdrop-blur-xl border-r border-slate-200/60 transition-all duration-300 ease-in-out shadow-urban-lg",
66
  "lg:relative lg:translate-x-0 lg:shadow-none",
67
  mobileOpen ? "translate-x-0" : "-translate-x-full",
68
- desktopOpen ? "lg:w-72" : "lg:w-0 lg:overflow-hidden lg:border-r-0"
69
  )}
70
  >
71
  <div className="flex h-16 items-center px-6 border-b border-slate-100/50">
72
  <div className="flex items-center gap-2">
73
  <div className="h-8 w-8 rounded-lg bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center shadow-lg shadow-blue-500/20">
74
- <span className="text-white font-bold font-mono">U</span>
75
  </div>
76
- <span className="text-xl font-bold tracking-tight text-slate-900">UrbanLens</span>
 
 
77
  </div>
78
  <span className="ml-auto rounded-full bg-blue-50 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-blue-600 ring-1 ring-blue-100">
79
  {role}
@@ -94,13 +96,20 @@ export default function DashboardSidebar({
94
  "flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium transition-all duration-200 group relative overflow-hidden",
95
  isActive
96
  ? "bg-blue-50 text-blue-700 shadow-sm ring-1 ring-blue-100"
97
- : "text-slate-500 hover:bg-slate-50 hover:text-slate-900"
98
  )}
99
  >
100
  {isActive && (
101
- <div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-blue-500 rounded-r-full" />
102
  )}
103
- <Icon className={cn("h-5 w-5 transition-transform group-hover:scale-110", isActive ? "text-blue-600" : "text-slate-400 group-hover:text-slate-600")} />
 
 
 
 
 
 
 
104
  <span className={isActive ? "ml-1.5" : ""}>{link.label}</span>
105
  </Link>
106
  );
 
65
  "fixed inset-y-0 left-0 z-50 bg-white/80 backdrop-blur-xl border-r border-slate-200/60 transition-all duration-300 ease-in-out shadow-urban-lg",
66
  "lg:relative lg:translate-x-0 lg:shadow-none",
67
  mobileOpen ? "translate-x-0" : "-translate-x-full",
68
+ desktopOpen ? "lg:w-72" : "lg:w-0 lg:overflow-hidden lg:border-r-0",
69
  )}
70
  >
71
  <div className="flex h-16 items-center px-6 border-b border-slate-100/50">
72
  <div className="flex items-center gap-2">
73
  <div className="h-8 w-8 rounded-lg bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center shadow-lg shadow-blue-500/20">
74
+ <span className="text-white font-bold font-mono">U</span>
75
  </div>
76
+ <span className="text-xl font-bold tracking-tight text-slate-900">
77
+ CityTracker
78
+ </span>
79
  </div>
80
  <span className="ml-auto rounded-full bg-blue-50 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-blue-600 ring-1 ring-blue-100">
81
  {role}
 
96
  "flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium transition-all duration-200 group relative overflow-hidden",
97
  isActive
98
  ? "bg-blue-50 text-blue-700 shadow-sm ring-1 ring-blue-100"
99
+ : "text-slate-500 hover:bg-slate-50 hover:text-slate-900",
100
  )}
101
  >
102
  {isActive && (
103
+ <div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-blue-500 rounded-r-full" />
104
  )}
105
+ <Icon
106
+ className={cn(
107
+ "h-5 w-5 transition-transform group-hover:scale-110",
108
+ isActive
109
+ ? "text-blue-600"
110
+ : "text-slate-400 group-hover:text-slate-600",
111
+ )}
112
+ />
113
  <span className={isActive ? "ml-1.5" : ""}>{link.label}</span>
114
  </Link>
115
  );
User/src/screens/capture/ProcessingScreen.tsx CHANGED
@@ -41,18 +41,6 @@ interface AgentStep {
41
  }
42
 
43
  const initialAgents: AgentStep[] = [
44
- {
45
- name: "LocationStep",
46
- iconName: "location",
47
- label: "Verifying Location",
48
- status: "pending",
49
- },
50
- {
51
- name: "UploadStep",
52
- iconName: "cloud-upload",
53
- label: "Secure Upload",
54
- status: "pending",
55
- },
56
  {
57
  name: "VisionAgent",
58
  iconName: "eye",
@@ -105,7 +93,7 @@ export function ProcessingScreen() {
105
 
106
  useEffect(() => {
107
  submitIssue();
108
-
109
  Animated.loop(
110
  Animated.sequence([
111
  Animated.timing(scanLineAnim, {
@@ -117,8 +105,8 @@ export function ProcessingScreen() {
117
  toValue: 0,
118
  duration: 0,
119
  useNativeDriver: true,
120
- })
121
- ])
122
  ).start();
123
  }, []);
124
 
@@ -262,7 +250,12 @@ export function ProcessingScreen() {
262
 
263
  const submitIssue = async () => {
264
  try {
265
- console.log("[ProcessingScreen] Submitting issue - session present:", !!session, "access_token present:", !!session?.access_token);
 
 
 
 
 
266
  const result = await issueService.createIssue(
267
  imageUri,
268
  location,
@@ -302,14 +295,15 @@ export function ProcessingScreen() {
302
  outputRange: [0, 180], // Match image height
303
  });
304
 
305
-
306
  return (
307
  <LinearGradient
308
  colors={[colors.background.primary, colors.background.secondary]}
309
  style={styles.container}
310
  >
311
  <View style={styles.header}>
312
- <View style={[styles.headerIcon, isComplete && styles.headerIconComplete]}>
 
 
313
  {isComplete ? (
314
  <Ionicons name="sparkles" size={32} color={colors.secondary.main} />
315
  ) : (
@@ -334,11 +328,11 @@ export function ProcessingScreen() {
334
  <Image source={{ uri: imageUri }} style={styles.image} />
335
  {!isComplete && (
336
  <View style={styles.imageOverlay}>
337
- <Animated.View
338
  style={[
339
- styles.scanLine,
340
- { transform: [{ translateY: scanTranslateY }] }
341
- ]}
342
  />
343
  <View style={styles.gridOverlay} />
344
  </View>
@@ -352,7 +346,11 @@ export function ProcessingScreen() {
352
  />
353
  </View>
354
  <Text style={styles.progressText}>
355
- {Math.round((agents.filter(a => a.status === 'done').length / agents.length) * 100)}% Complete
 
 
 
 
356
  </Text>
357
  </View>
358
 
@@ -390,8 +388,8 @@ export function ProcessingScreen() {
390
  agent.status === "done"
391
  ? colors.secondary.main
392
  : agent.status === "running"
393
- ? colors.primary.main
394
- : colors.text.tertiary
395
  }
396
  />
397
  </View>
@@ -407,7 +405,9 @@ export function ProcessingScreen() {
407
  </Text>
408
 
409
  {agent.decision ? (
410
- <Text style={styles.agentDecision}>Result: {agent.decision}</Text>
 
 
411
  ) : null}
412
  </View>
413
 
@@ -441,7 +441,12 @@ export function ProcessingScreen() {
441
 
442
  {isComplete && (
443
  <View style={styles.actions}>
444
- <Button title="View Receipt" onPress={handleViewDetails} fullWidth size="lg" />
 
 
 
 
 
445
  <Button
446
  title="Return Home"
447
  variant="ghost"
@@ -490,7 +495,7 @@ const styles = StyleSheet.create({
490
  fontSize: 14,
491
  color: colors.text.secondary,
492
  textAlign: "center",
493
- maxWidth: '80%',
494
  },
495
  imageContainer: {
496
  height: 200,
@@ -514,11 +519,11 @@ const styles = StyleSheet.create({
514
  imageOverlay: {
515
  ...StyleSheet.absoluteFillObject,
516
  backgroundColor: "transparent",
517
- overflow: 'hidden',
518
  },
519
  gridOverlay: {
520
  ...StyleSheet.absoluteFillObject,
521
- backgroundColor: "transparent",
522
  // Add grid background image or implementation if feasible, otherwise keep transparent
523
  },
524
  scanLine: {
@@ -532,8 +537,8 @@ const styles = StyleSheet.create({
532
  },
533
  progressContainer: {
534
  marginBottom: spacing.lg,
535
- flexDirection: 'row',
536
- alignItems: 'center',
537
  gap: 12,
538
  },
539
  progressTrack: {
@@ -553,7 +558,7 @@ const styles = StyleSheet.create({
553
  color: colors.text.secondary,
554
  fontWeight: "600",
555
  width: 90,
556
- textAlign: 'right',
557
  },
558
  agentsContainer: {
559
  flex: 1,
@@ -624,7 +629,7 @@ const styles = StyleSheet.create({
624
  fontWeight: "500",
625
  },
626
  actions: {
627
- position: 'absolute',
628
  bottom: 40,
629
  left: 20,
630
  right: 20,
 
41
  }
42
 
43
  const initialAgents: AgentStep[] = [
 
 
 
 
 
 
 
 
 
 
 
 
44
  {
45
  name: "VisionAgent",
46
  iconName: "eye",
 
93
 
94
  useEffect(() => {
95
  submitIssue();
96
+
97
  Animated.loop(
98
  Animated.sequence([
99
  Animated.timing(scanLineAnim, {
 
105
  toValue: 0,
106
  duration: 0,
107
  useNativeDriver: true,
108
+ }),
109
+ ]),
110
  ).start();
111
  }, []);
112
 
 
250
 
251
  const submitIssue = async () => {
252
  try {
253
+ console.log(
254
+ "[ProcessingScreen] Submitting issue - session present:",
255
+ !!session,
256
+ "access_token present:",
257
+ !!session?.access_token,
258
+ );
259
  const result = await issueService.createIssue(
260
  imageUri,
261
  location,
 
295
  outputRange: [0, 180], // Match image height
296
  });
297
 
 
298
  return (
299
  <LinearGradient
300
  colors={[colors.background.primary, colors.background.secondary]}
301
  style={styles.container}
302
  >
303
  <View style={styles.header}>
304
+ <View
305
+ style={[styles.headerIcon, isComplete && styles.headerIconComplete]}
306
+ >
307
  {isComplete ? (
308
  <Ionicons name="sparkles" size={32} color={colors.secondary.main} />
309
  ) : (
 
328
  <Image source={{ uri: imageUri }} style={styles.image} />
329
  {!isComplete && (
330
  <View style={styles.imageOverlay}>
331
+ <Animated.View
332
  style={[
333
+ styles.scanLine,
334
+ { transform: [{ translateY: scanTranslateY }] },
335
+ ]}
336
  />
337
  <View style={styles.gridOverlay} />
338
  </View>
 
346
  />
347
  </View>
348
  <Text style={styles.progressText}>
349
+ {Math.round(
350
+ (agents.filter((a) => a.status === "done").length / agents.length) *
351
+ 100,
352
+ )}
353
+ % Complete
354
  </Text>
355
  </View>
356
 
 
388
  agent.status === "done"
389
  ? colors.secondary.main
390
  : agent.status === "running"
391
+ ? colors.primary.main
392
+ : colors.text.tertiary
393
  }
394
  />
395
  </View>
 
405
  </Text>
406
 
407
  {agent.decision ? (
408
+ <Text style={styles.agentDecision}>
409
+ Result: {agent.decision}
410
+ </Text>
411
  ) : null}
412
  </View>
413
 
 
441
 
442
  {isComplete && (
443
  <View style={styles.actions}>
444
+ <Button
445
+ title="View Receipt"
446
+ onPress={handleViewDetails}
447
+ fullWidth
448
+ size="lg"
449
+ />
450
  <Button
451
  title="Return Home"
452
  variant="ghost"
 
495
  fontSize: 14,
496
  color: colors.text.secondary,
497
  textAlign: "center",
498
+ maxWidth: "80%",
499
  },
500
  imageContainer: {
501
  height: 200,
 
519
  imageOverlay: {
520
  ...StyleSheet.absoluteFillObject,
521
  backgroundColor: "transparent",
522
+ overflow: "hidden",
523
  },
524
  gridOverlay: {
525
  ...StyleSheet.absoluteFillObject,
526
+ backgroundColor: "transparent",
527
  // Add grid background image or implementation if feasible, otherwise keep transparent
528
  },
529
  scanLine: {
 
537
  },
538
  progressContainer: {
539
  marginBottom: spacing.lg,
540
+ flexDirection: "row",
541
+ alignItems: "center",
542
  gap: 12,
543
  },
544
  progressTrack: {
 
558
  color: colors.text.secondary,
559
  fontWeight: "600",
560
  width: 90,
561
+ textAlign: "right",
562
  },
563
  agentsContainer: {
564
  flex: 1,
 
629
  fontWeight: "500",
630
  },
631
  actions: {
632
+ position: "absolute",
633
  bottom: 40,
634
  left: 20,
635
  right: 20,
demoWorker.py ADDED
@@ -0,0 +1,448 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import json
3
+ from typing import Optional, Dict, Any
4
+ import logging
5
+
6
+ logging.basicConfig(level=logging.INFO)
7
+ logger = logging.getLogger(__name__)
8
+
9
+ API_URL = "https://0xarchit-citytrack.hf.space"
10
+
11
+ class CityTrackAPIClient:
12
+ def __init__(self, base_url: str = API_URL):
13
+ self.base_url = base_url.rstrip("/")
14
+ self.session = requests.Session()
15
+ self.access_token: Optional[str] = None
16
+
17
+ def admin_login(self, email: str, password: str, expected_role: Optional[str] = "admin") -> Dict[str, Any]:
18
+ try:
19
+ payload: Dict[str, Any] = {
20
+ "email": email,
21
+ "password": password,
22
+ "expected_role": expected_role,
23
+ }
24
+ response = self.session.post(
25
+ f"{self.base_url}/admin/login",
26
+ json=payload,
27
+ headers={"Content-Type": "application/json"},
28
+ )
29
+ response.raise_for_status()
30
+ data = response.json()
31
+ token = data.get("access_token")
32
+ if not token:
33
+ return {"error": "Missing access_token", "response": data}
34
+ self.access_token = token
35
+ self.session.headers.update({"Authorization": f"Bearer {token}"})
36
+ return data
37
+ except Exception as e:
38
+ logger.error(f"Admin login failed: {e}")
39
+ return {"error": str(e)}
40
+
41
+ def health_check(self) -> Dict[str, Any]:
42
+ """Check API health status"""
43
+ try:
44
+ response = self.session.get(f"{self.base_url}/health/health")
45
+ response.raise_for_status()
46
+ return response.json()
47
+ except Exception as e:
48
+ logger.error(f"Health check failed: {e}")
49
+ return {"status": "error", "message": str(e)}
50
+
51
+ def get_issues(self, limit: int = 10, skip: int = 0) -> Dict[str, Any]:
52
+ """Fetch all issues"""
53
+ try:
54
+ response = self.session.get(
55
+ f"{self.base_url}/issues",
56
+ params={"limit": limit, "skip": skip}
57
+ )
58
+ response.raise_for_status()
59
+ return response.json()
60
+ except Exception as e:
61
+ logger.error(f"Failed to fetch issues: {e}")
62
+ return {"error": str(e)}
63
+
64
+ def get_issue_by_id(self, issue_id: str) -> Dict[str, Any]:
65
+ """Fetch a specific issue"""
66
+ try:
67
+ response = self.session.get(f"{self.base_url}/issues/{issue_id}")
68
+ response.raise_for_status()
69
+ return response.json()
70
+ except Exception as e:
71
+ logger.error(f"Failed to fetch issue {issue_id}: {e}")
72
+ return {"error": str(e)}
73
+
74
+ def create_issue(self, payload: Dict[str, Any]) -> Dict[str, Any]:
75
+ """Create a new issue"""
76
+ try:
77
+ response = self.session.post(
78
+ f"{self.base_url}/issues",
79
+ json=payload,
80
+ headers={"Content-Type": "application/json"}
81
+ )
82
+ response.raise_for_status()
83
+ return response.json()
84
+ except Exception as e:
85
+ logger.error(f"Failed to create issue: {e}")
86
+ return {"error": str(e)}
87
+
88
+ def update_issue(self, issue_id: str, payload: Dict[str, Any]) -> Dict[str, Any]:
89
+ """Update an existing issue"""
90
+ try:
91
+ response = self.session.put(
92
+ f"{self.base_url}/issues/{issue_id}",
93
+ json=payload,
94
+ headers={"Content-Type": "application/json"}
95
+ )
96
+ response.raise_for_status()
97
+ return response.json()
98
+ except Exception as e:
99
+ logger.error(f"Failed to update issue {issue_id}: {e}")
100
+ return {"error": str(e)}
101
+
102
+ def get_departments(self) -> Dict[str, Any]:
103
+ """Fetch all departments"""
104
+ try:
105
+ response = self.session.get(f"{self.base_url}/admin/departments")
106
+ response.raise_for_status()
107
+ return response.json()
108
+ except Exception as e:
109
+ logger.error(f"Failed to fetch departments: {e}")
110
+ return {"error": str(e)}
111
+
112
+ def create_department(self, name: str, code: str, description: Optional[str] = None,
113
+ categories: Optional[str] = None, default_sla_hours: int = 48,
114
+ escalation_email: Optional[str] = None) -> Dict[str, Any]:
115
+ try:
116
+ payload: Dict[str, Any] = {
117
+ "name": name,
118
+ "code": code,
119
+ "description": description,
120
+ "categories": categories,
121
+ "default_sla_hours": default_sla_hours,
122
+ "escalation_email": escalation_email,
123
+ }
124
+ response = self.session.post(
125
+ f"{self.base_url}/admin/departments",
126
+ json=payload,
127
+ headers={"Content-Type": "application/json"},
128
+ )
129
+ response.raise_for_status()
130
+ return response.json()
131
+ except Exception as e:
132
+ logger.error(f"Failed to create department {code}: {e}")
133
+ return {"error": str(e)}
134
+
135
+ def get_workers(self, department_id: Optional[str] = None) -> Dict[str, Any]:
136
+ """Fetch all workers or workers from a specific department"""
137
+ try:
138
+ params = {}
139
+ if department_id:
140
+ params["department_id"] = department_id
141
+
142
+ response = self.session.get(
143
+ f"{self.base_url}/admin/members",
144
+ params=params
145
+ )
146
+ response.raise_for_status()
147
+ data = response.json()
148
+ if isinstance(data, list):
149
+ return [m for m in data if isinstance(m, dict) and m.get("role") == "worker"]
150
+ return data
151
+ except Exception as e:
152
+ logger.error(f"Failed to fetch workers: {e}")
153
+ return {"error": str(e)}
154
+
155
+ def get_worker_tasks(self) -> Dict[str, Any]:
156
+ """Fetch tasks assigned to the current worker (from JWT)"""
157
+ try:
158
+ response = self.session.get(f"{self.base_url}/worker/tasks")
159
+ response.raise_for_status()
160
+ return response.json()
161
+ except Exception as e:
162
+ logger.error(f"Failed to fetch worker tasks: {e}")
163
+ return {"error": str(e)}
164
+
165
+ def assign_issue_to_worker(self, issue_id: str, worker_id: str) -> Dict[str, Any]:
166
+ """Assign an issue to a worker"""
167
+ try:
168
+ response = self.session.post(
169
+ f"{self.base_url}/issues/{issue_id}/assign",
170
+ json={"worker_id": worker_id},
171
+ headers={"Content-Type": "application/json"}
172
+ )
173
+ response.raise_for_status()
174
+ return response.json()
175
+ except Exception as e:
176
+ logger.error(f"Failed to assign issue: {e}")
177
+ return {"error": str(e)}
178
+
179
+ def resolve_issue(self, issue_id: str, payload: Dict[str, Any]) -> Dict[str, Any]:
180
+ """Mark an issue as resolved"""
181
+ try:
182
+ response = self.session.put(
183
+ f"{self.base_url}/issues/{issue_id}/resolve",
184
+ json=payload,
185
+ headers={"Content-Type": "application/json"}
186
+ )
187
+ response.raise_for_status()
188
+ return response.json()
189
+ except Exception as e:
190
+ logger.error(f"Failed to resolve issue {issue_id}: {e}")
191
+ return {"error": str(e)}
192
+
193
+ def get_issue_stats(self) -> Dict[str, Any]:
194
+ """Get issue statistics"""
195
+ try:
196
+ response = self.session.get(f"{self.base_url}/admin/stats")
197
+ response.raise_for_status()
198
+ return response.json()
199
+ except Exception as e:
200
+ logger.error(f"Failed to fetch stats: {e}")
201
+ return {"error": str(e)}
202
+
203
+ def get_heatmap_data(self, city: Optional[str] = None) -> Dict[str, Any]:
204
+ """Get heatmap data for geospatial visualization"""
205
+ try:
206
+ params = {}
207
+ if city:
208
+ params["city"] = city
209
+
210
+ response = self.session.get(
211
+ f"{self.base_url}/admin/stats/heatmap",
212
+ params=params
213
+ )
214
+ response.raise_for_status()
215
+ return response.json()
216
+ except Exception as e:
217
+ logger.error(f"Failed to fetch heatmap data: {e}")
218
+ return {"error": str(e)}
219
+
220
+ def create_worker(self, department_id: str, name: str, email: str,
221
+ locality: str, city: str = "Himachal Pradesh",
222
+ max_workload: int = 10, password: str = "12345678",
223
+ phone: Optional[str] = None, role: str = "worker") -> Dict[str, Any]:
224
+ try:
225
+ payload = {
226
+ "department_id": department_id,
227
+ "name": name,
228
+ "email": email,
229
+ "phone": phone,
230
+ "role": role,
231
+ "city": city,
232
+ "locality": locality,
233
+ "max_workload": max_workload,
234
+ "password": password,
235
+ }
236
+ response = self.session.post(
237
+ f"{self.base_url}/admin/members",
238
+ json=payload,
239
+ headers={"Content-Type": "application/json"}
240
+ )
241
+ response.raise_for_status()
242
+ return response.json()
243
+ except Exception as e:
244
+ logger.error(f"Failed to create worker {name}: {e}")
245
+ return {"error": str(e)}
246
+
247
+ def bulk_create_workers(self, department_code: str, department_id: str,
248
+ worker_names: list, locations: list) -> Dict[str, Any]:
249
+ """Bulk create workers with Gmail alias format: zrxarchit+<name>.<department>@gmail.com"""
250
+ created_workers = []
251
+ failed_workers = []
252
+
253
+ if len(worker_names) != len(locations):
254
+ return {"error": "Number of names and locations must match"}
255
+
256
+ phone_prefix_by_department = {
257
+ "PWD": "101",
258
+ "SANITATION": "202",
259
+ "TRAFFIC": "303",
260
+ }
261
+
262
+ for idx, name in enumerate(worker_names):
263
+ location = locations[idx % len(locations)]
264
+ name_parts = name.lower().replace(" ", ".")
265
+ email = f"zrxarchit+{name_parts}.{department_code.lower()}@gmail.com"
266
+ prefix = phone_prefix_by_department.get(department_code.upper(), "999")
267
+ phone = f"9{prefix}{idx + 1:06d}"
268
+
269
+ response = self.create_worker(
270
+ department_id=department_id,
271
+ name=name,
272
+ email=email,
273
+ locality=location,
274
+ phone=phone,
275
+ password="12345678",
276
+ role="worker",
277
+ )
278
+
279
+ if "error" not in response:
280
+ created_workers.append({
281
+ "name": name,
282
+ "email": email,
283
+ "locality": location,
284
+ "status": "created"
285
+ })
286
+ logger.info(f"✓ Created: {name} ({email}) - {location}")
287
+ else:
288
+ failed_workers.append({
289
+ "name": name,
290
+ "email": email,
291
+ "error": response.get("error")
292
+ })
293
+ logger.warning(f"✗ Failed: {name} ({email}) - {response.get('error')}")
294
+
295
+ return {
296
+ "department": department_code,
297
+ "total": len(worker_names),
298
+ "created": len(created_workers),
299
+ "failed": len(failed_workers),
300
+ "workers": created_workers,
301
+ "failures": failed_workers
302
+ }
303
+
304
+
305
+ def main():
306
+ """Example usage of the API client"""
307
+ print("=" * 80)
308
+ print("CityTrack API Client - Worker Bulk Creation")
309
+ print("=" * 80)
310
+ print(f"Base URL: {API_URL}")
311
+ print("=" * 80)
312
+
313
+ client = CityTrackAPIClient()
314
+
315
+ NEARBY_LOCATIONS = [
316
+ "Una", "Haroli", "Amb", "Kasauli", "Baddi",
317
+ "Nalagarh", "Solan", "Parwanoo", "Kalka", "Kurali"
318
+ ]
319
+
320
+ PWD_WORKERS = [
321
+ "Ramesh Kumar", "Sukesh Singh", "Harish Patel", "Vikram Sharma", "Ajay Kumar",
322
+ "Rajesh Tiwari", "Manoj Singh", "Arjun Verma", "Deepak Yadav", "Sandeep Gupta"
323
+ ]
324
+
325
+ SANITATION_WORKERS = [
326
+ "Suresh Singh", "Mohan Lal", "Ravi Kumar", "Anita Devi", "Asha Sharma",
327
+ "Priya Singh", "Meera Patel", "Kavya Reddy", "Neha Verma", "Pooja Kumari"
328
+ ]
329
+
330
+ TRAFFIC_WORKERS = [
331
+ "Priya Sharma", "Anil Kumar", "Bhavna Singh", "Nitin Patel", "Sanjay Verma",
332
+ "Rohit Sharma", "Dinesh Kumar", "Sachin Singh", "Amit Patel", "Vishal Reddy"
333
+ ]
334
+
335
+ print("\n[1] Health Check:")
336
+ health = client.health_check()
337
+ print(json.dumps(health, indent=2))
338
+
339
+ print("\n[2] Admin Login:")
340
+ login = client.admin_login(email="zrxarchit@gmail.com", password="12345678", expected_role="admin")
341
+ if "error" in login:
342
+ print(json.dumps(login, indent=2))
343
+ return
344
+ print(json.dumps({"token_type": login.get("token_type"), "user": login.get("user")}, indent=2))
345
+
346
+ print("\n[3] Fetching Departments:")
347
+ departments_response = client.get_departments()
348
+
349
+ departments_map = {}
350
+ if isinstance(departments_response, list):
351
+ for dept in departments_response:
352
+ departments_map[dept["code"]] = dept["id"]
353
+ print(f" - {dept['name']} (Code: {dept['code']}, ID: {dept['id']})")
354
+ elif isinstance(departments_response, dict) and "departments" in departments_response:
355
+ for dept in departments_response["departments"]:
356
+ departments_map[dept["code"]] = dept["id"]
357
+ print(f" - {dept['name']} (Code: {dept['code']}, ID: {dept['id']})")
358
+ else:
359
+ print(" Error fetching departments:", departments_response)
360
+ return
361
+
362
+ if not departments_map:
363
+ print(" No departments found. Creating baseline departments...")
364
+ baseline = [
365
+ ("Public Works Department", "PWD"),
366
+ ("Sanitation Department", "SANITATION"),
367
+ ("Traffic Department", "TRAFFIC"),
368
+ ]
369
+ for name, code in baseline:
370
+ created = client.create_department(name=name, code=code)
371
+ if "error" in created:
372
+ print(json.dumps({"department": code, "result": created}, indent=2))
373
+
374
+ departments_response = client.get_departments()
375
+ departments_map = {}
376
+ if isinstance(departments_response, list):
377
+ for dept in departments_response:
378
+ departments_map[dept["code"]] = dept["id"]
379
+ print(f" - {dept['name']} (Code: {dept['code']}, ID: {dept['id']})")
380
+ elif isinstance(departments_response, dict) and "departments" in departments_response:
381
+ for dept in departments_response["departments"]:
382
+ departments_map[dept["code"]] = dept["id"]
383
+ print(f" - {dept['name']} (Code: {dept['code']}, ID: {dept['id']})")
384
+ else:
385
+ print(" Error fetching departments:", departments_response)
386
+ return
387
+
388
+ if not departments_map:
389
+ print(" ERROR: Failed to create/fetch departments.")
390
+ return
391
+
392
+ print("\n[4] Creating PWD Workers (10 workers):")
393
+ print("-" * 80)
394
+ if "PWD" in departments_map:
395
+ pwd_result = client.bulk_create_workers(
396
+ department_code="PWD",
397
+ department_id=departments_map["PWD"],
398
+ worker_names=PWD_WORKERS,
399
+ locations=NEARBY_LOCATIONS
400
+ )
401
+ print(json.dumps(pwd_result, indent=2))
402
+ else:
403
+ print(" ERROR: PWD department not found")
404
+
405
+ print("\n[5] Creating Sanitation Workers (10 workers):")
406
+ print("-" * 80)
407
+ if "SANITATION" in departments_map:
408
+ sanitation_result = client.bulk_create_workers(
409
+ department_code="SANITATION",
410
+ department_id=departments_map["SANITATION"],
411
+ worker_names=SANITATION_WORKERS,
412
+ locations=NEARBY_LOCATIONS
413
+ )
414
+ print(json.dumps(sanitation_result, indent=2))
415
+ else:
416
+ print(" ERROR: SANITATION department not found")
417
+
418
+ print("\n[6] Creating Traffic Workers (10 workers):")
419
+ print("-" * 80)
420
+ if "TRAFFIC" in departments_map:
421
+ traffic_result = client.bulk_create_workers(
422
+ department_code="TRAFFIC",
423
+ department_id=departments_map["TRAFFIC"],
424
+ worker_names=TRAFFIC_WORKERS,
425
+ locations=NEARBY_LOCATIONS
426
+ )
427
+ print(json.dumps(traffic_result, indent=2))
428
+ else:
429
+ print(" ERROR: TRAFFIC department not found")
430
+
431
+ print("\n[7] Fetching All Workers:")
432
+ workers = client.get_workers()
433
+ if isinstance(workers, list):
434
+ print(f" Total workers: {len(workers)}")
435
+ for worker in workers[:5]:
436
+ print(f" - {worker.get('name')} ({worker.get('email')})")
437
+ if len(workers) > 5:
438
+ print(f" ... and {len(workers) - 5} more")
439
+ else:
440
+ print(json.dumps(workers, indent=2)[:500] + "...")
441
+
442
+ print("\n" + "=" * 80)
443
+ print("Bulk Worker Creation Complete!")
444
+ print("=" * 80)
445
+
446
+
447
+ if __name__ == "__main__":
448
+ main()