rairo commited on
Commit
784e1ec
·
verified ·
1 Parent(s): 8223456

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +564 -39
main.py CHANGED
@@ -56,14 +56,83 @@ Emails.api_key = RESEND_API_KEY
56
 
57
  def send_email(to, subject, html):
58
  try:
59
- Emails.send({
60
  "from": "Admin <admin@resend.dev>",
61
  "to": to,
62
  "subject": subject,
63
  "html": html
64
  })
 
 
65
  except Exception as e:
66
- print("Email error:", e)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
  # === Auth Middleware ===
69
  def verify_token(req):
@@ -88,56 +157,151 @@ setup_admins()
88
 
89
  # === Core Functions ===
90
  def assign_roster(job_id):
91
- """Assign roster for a job - now a regular function instead of Celery task"""
92
  try:
93
  # Get job details
94
  job_ref = db.reference(f"jobs/{job_id}")
95
  job_data = job_ref.get()
96
 
97
- if not job_data:
98
  print(f"Job {job_id} not found")
99
  return
100
 
101
- # Get all members
102
- members_ref = db.reference("members")
103
- members = members_ref.get()
104
-
105
- if not members:
106
- print("No members found")
107
  return
108
 
109
- # Convert members to list
110
- members_list = list(members.values()) if isinstance(members, dict) else members
 
 
111
 
112
- # Simple rotation logic (you can enhance this)
 
 
 
 
 
 
 
 
113
  current_assignments = job_data.get("assignments", [])
114
- last_assigned_index = job_data.get("last_assigned_index", -1)
 
 
 
 
 
 
 
 
 
 
115
 
116
- # Find next member to assign
117
- next_index = (last_assigned_index + 1) % len(members_list)
118
- assigned_member = members_list[next_index]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
- # Create assignment record
121
- assignment = {
122
- "member": assigned_member,
123
  "assigned_at": datetime.datetime.now().isoformat(),
124
- "assignment_id": str(uuid.uuid4())
 
125
  }
126
 
127
- # Update assignments
128
- current_assignments.append(assignment)
129
 
130
- # Update job with new assignment and index
131
  job_ref.update({
132
  "assignments": current_assignments,
133
- "last_assigned_index": next_index,
134
  "last_updated": datetime.datetime.now().isoformat()
135
  })
136
 
137
- print(f"Assigned {assigned_member.get('name', 'Unknown')} to job {job_id}")
 
 
 
 
 
 
 
138
 
139
- # Send notification email (optional)
140
- # send_email(assigned_member.get('email'), "New Assignment", f"You have been assigned to job {job_id}")
 
 
 
 
 
 
 
141
 
142
  except Exception as e:
143
  print(f"Error in assign_roster for job {job_id}: {e}")
@@ -155,23 +319,32 @@ def check_and_rotate_guards():
155
  print("No jobs found")
156
  return
157
 
158
- # Check each job for rotation needs
159
  for job_id, job_data in jobs.items():
160
- # Check if job needs rotation (example logic - customize as needed)
 
 
 
 
 
 
 
 
161
  assignments = job_data.get("assignments", [])
162
 
163
  if not assignments:
164
- # If no assignments yet, start the first one
165
- print(f"Starting first assignment for job {job_id}")
166
- assign_roster(job_id)
167
  else:
168
- # Check if current assignment is expired (example: 8 hours)
169
  last_assignment = assignments[-1]
170
  assigned_at = datetime.datetime.fromisoformat(last_assignment["assigned_at"].replace("Z", "+00:00"))
171
  now = datetime.datetime.now(assigned_at.tzinfo)
172
 
173
- # Rotate every 8 hours (customize as needed)
174
- if (now - assigned_at).total_seconds() > 28800: # 8 hours in seconds
 
 
175
  print(f"Rotating guard for job {job_id}")
176
  assign_roster(job_id)
177
 
@@ -217,15 +390,117 @@ def create_job():
217
 
218
  data = request.json
219
  job_id = str(uuid.uuid4())
220
- db.reference(f"jobs/{job_id}").set(data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  return jsonify({"message": "Job created", "job_id": job_id}), 200
222
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  @app.route("/start_job/<job_id>", methods=["POST"])
224
  def start_job(job_id):
225
  user = verify_token(request)
226
  if not user:
227
  return jsonify({"error": "Unauthorized"}), 401
228
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  # Schedule the first assignment immediately
230
  scheduler.add_job(
231
  func=assign_roster,
@@ -235,7 +510,258 @@ def start_job(job_id):
235
  id=f"start_{job_id}_{uuid.uuid4().hex[:8]}"
236
  )
237
 
238
- return jsonify({"message": f"Roster assignment started for job {job_id}"}), 202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
  # === Schedule periodic guard rotation ===
241
  # Run every 5 minutes to check for rotations
@@ -245,7 +771,6 @@ scheduler.add_job(
245
  minutes=5,
246
  id="guard_rotation_job"
247
  )
248
-
249
  # === Run Server ===
250
  if __name__ == "__main__":
251
  app.run(debug=True, host="0.0.0.0", port=7860)
 
56
 
57
  def send_email(to, subject, html):
58
  try:
59
+ response = Emails.send({
60
  "from": "Admin <admin@resend.dev>",
61
  "to": to,
62
  "subject": subject,
63
  "html": html
64
  })
65
+ print(f"Email sent successfully to {to}")
66
+ return response
67
  except Exception as e:
68
+ print(f"Email error to {to}: {e}")
69
+ return None
70
+
71
+ def send_rotation_notification(job_id, shift_record):
72
+ """Send email notification for rotation to all admins"""
73
+ try:
74
+ job_ref = db.reference(f"jobs/{job_id}")
75
+ job_data = job_ref.get()
76
+
77
+ if not job_data:
78
+ return
79
+
80
+ job_name = job_data.get("name", "Unknown Job")
81
+ shift_number = shift_record.get("shift_number", "Unknown")
82
+
83
+ # Create HTML email content
84
+ html_content = f"""
85
+ <h2>Guard Rotation Notification</h2>
86
+ <p><strong>Job:</strong> {job_name}</p>
87
+ <p><strong>Job ID:</strong> {job_id}</p>
88
+ <p><strong>Shift Number:</strong> {shift_number}</p>
89
+ <p><strong>Rotation Time:</strong> {shift_record.get('assigned_at', 'Unknown')}</p>
90
+
91
+ <h3>Assignments:</h3>
92
+ <table border="1" style="border-collapse: collapse; width: 100%;">
93
+ <thead>
94
+ <tr>
95
+ <th>Guarding Point</th>
96
+ <th>Assigned Member</th>
97
+ <th>Special Case</th>
98
+ </tr>
99
+ </thead>
100
+ <tbody>
101
+ """
102
+
103
+ for assignment in shift_record.get("assignments", []):
104
+ point_name = assignment.get("point", {}).get("name", "Unknown Point")
105
+ member_name = assignment.get("member", {}).get("name", "Unknown Member")
106
+ is_special = "Yes" if assignment.get("is_special_case", False) else "No"
107
+ special_reason = assignment.get("special_case_reason", "") if assignment.get("is_special_case", False) else ""
108
+
109
+ if special_reason:
110
+ member_info = f"{member_name} ({special_reason})"
111
+ else:
112
+ member_info = member_name
113
+
114
+ html_content += f"""
115
+ <tr>
116
+ <td>{point_name}</td>
117
+ <td>{member_info}</td>
118
+ <td>{is_special}</td>
119
+ </tr>
120
+ """
121
+
122
+ html_content += """
123
+ </tbody>
124
+ </table>
125
+ <p><em>This is an automated notification from the Guard Rotation System.</em></p>
126
+ """
127
+
128
+ # Send email to all admin emails
129
+ subject = f"Guard Rotation - {job_name} (Shift {shift_number})"
130
+
131
+ for admin_email in ADMIN_EMAILS:
132
+ send_email(admin_email, subject, html_content)
133
+
134
+ except Exception as e:
135
+ print(f"Error sending rotation notification for job {job_id}: {e}")
136
 
137
  # === Auth Middleware ===
138
  def verify_token(req):
 
157
 
158
  # === Core Functions ===
159
  def assign_roster(job_id):
160
+ """Assign roster for a job - implements the rotation algorithm"""
161
  try:
162
  # Get job details
163
  job_ref = db.reference(f"jobs/{job_id}")
164
  job_data = job_ref.get()
165
 
166
+ if not job_
167
  print(f"Job {job_id} not found")
168
  return
169
 
170
+ # Check if job is paused
171
+ if job_data.get("status") == "paused":
172
+ print(f"Job {job_id} is paused, skipping assignment")
 
 
 
173
  return
174
 
175
+ # Get guarding points and assigned members
176
+ guarding_points = job_data.get("guarding_points", [])
177
+ assigned_members = job_data.get("assigned_members", [])
178
+ special_cases = job_data.get("special_cases", [])
179
 
180
+ if not guarding_points:
181
+ print(f"No guarding points defined for job {job_id}")
182
+ return
183
+
184
+ if not assigned_members:
185
+ print(f"No members assigned to job {job_id}")
186
+ return
187
+
188
+ # Get current assignments and rotation history
189
  current_assignments = job_data.get("assignments", [])
190
+ rotation_history = job_data.get("rotation_history", {})
191
+
192
+ # Determine next shift
193
+ shift_number = len(current_assignments) + 1
194
+
195
+ # Apply special cases if any match current shift
196
+ special_assignment = None
197
+ for case in special_cases:
198
+ if case.get("shift_number") == shift_number or case.get("active", False):
199
+ special_assignment = case
200
+ break
201
 
202
+ # Generate assignments for this shift
203
+ shift_assignments = []
204
+
205
+ if special_assignment:
206
+ # Apply special case assignment
207
+ assigned_point = next((p for p in guarding_points if p["id"] == special_assignment["point_id"]), None)
208
+ assigned_member = next((m for m in assigned_members if m["id"] == special_assignment["member_id"]), None)
209
+
210
+ if assigned_point and assigned_member:
211
+ shift_assignments.append({
212
+ "point": assigned_point,
213
+ "member": assigned_member,
214
+ "is_special_case": True,
215
+ "special_case_reason": special_assignment.get("reason", "Special Assignment")
216
+ })
217
+ else:
218
+ # Standard rotation algorithm
219
+ # Create point-specific rotation history
220
+ point_assignments = {}
221
+
222
+ for point in guarding_points:
223
+ point_id = point["id"]
224
+ point_history = rotation_history.get(point_id, [])
225
+
226
+ # Find member who hasn't been assigned to this point recently
227
+ available_members = assigned_members.copy()
228
+
229
+ # Remove recently assigned members (no immediate repetition)
230
+ recently_assigned_member_ids = set()
231
+ for recent_assignment in point_history[-len(guarding_points):]: # Look back N assignments
232
+ recently_assigned_member_ids.add(recent_assignment["member_id"])
233
+
234
+ # Filter available members
235
+ available_members = [m for m in available_members if m["id"] not in recently_assigned_member_ids]
236
+
237
+ # If all members have been recently assigned, use all members
238
+ if not available_members:
239
+ available_members = assigned_members
240
+
241
+ # Select next member (simple round-robin from available)
242
+ if point_history:
243
+ last_assigned_member_id = point_history[-1]["member_id"]
244
+ last_member_index = next((i for i, m in enumerate(available_members) if m["id"] == last_assigned_member_id), -1)
245
+ next_member_index = (last_member_index + 1) % len(available_members)
246
+ else:
247
+ next_member_index = 0
248
+
249
+ selected_member = available_members[next_member_index]
250
+
251
+ # Record assignment
252
+ shift_assignments.append({
253
+ "point": point,
254
+ "member": selected_member,
255
+ "is_special_case": False
256
+ })
257
+
258
+ # Update rotation history
259
+ point_history.append({
260
+ "member_id": selected_member["id"],
261
+ "member_name": selected_member.get("name", "Unknown"),
262
+ "assigned_at": datetime.datetime.now().isoformat(),
263
+ "shift_number": shift_number
264
+ })
265
+ rotation_history[point_id] = point_history
266
+
267
+ # Update rotation history in database
268
+ job_ref.update({"rotation_history": rotation_history})
269
 
270
+ # Create shift record
271
+ shift_record = {
272
+ "shift_number": shift_number,
273
  "assigned_at": datetime.datetime.now().isoformat(),
274
+ "assignments": shift_assignments,
275
+ "shift_id": str(uuid.uuid4())
276
  }
277
 
278
+ # Add to current assignments
279
+ current_assignments.append(shift_record)
280
 
281
+ # Update job with new assignment
282
  job_ref.update({
283
  "assignments": current_assignments,
 
284
  "last_updated": datetime.datetime.now().isoformat()
285
  })
286
 
287
+ print(f"Shift {shift_number} assigned for job {job_id} with {len(shift_assignments)} assignments")
288
+
289
+ # Send email notification to admins
290
+ send_rotation_notification(job_id, shift_record)
291
+
292
+ # Schedule next rotation based on job's rotation period
293
+ rotation_period = job_data.get("rotation_period", 28800) # Default 8 hours
294
+ next_run_time = datetime.datetime.now() + datetime.timedelta(seconds=rotation_period)
295
 
296
+ scheduler.add_job(
297
+ func=assign_roster,
298
+ trigger="date",
299
+ run_date=next_run_time,
300
+ args=[job_id],
301
+ id=f"rotate_{job_id}_{uuid.uuid4().hex[:8]}"
302
+ )
303
+
304
+ print(f"Scheduled next rotation for job {job_id} in {rotation_period} seconds")
305
 
306
  except Exception as e:
307
  print(f"Error in assign_roster for job {job_id}: {e}")
 
319
  print("No jobs found")
320
  return
321
 
322
+ # Check each job for rotation needs (for jobs that don't have scheduled rotations)
323
  for job_id, job_data in jobs.items():
324
+ # Skip paused jobs
325
+ if job_data.get("status") == "paused":
326
+ continue
327
+
328
+ # Skip jobs that have scheduled rotations
329
+ if job_data.get("has_scheduled_rotation", False):
330
+ continue
331
+
332
+ # For jobs without scheduled rotations, check manually
333
  assignments = job_data.get("assignments", [])
334
 
335
  if not assignments:
336
+ # If no assignments yet, this job hasn't started manually
337
+ continue
 
338
  else:
339
+ # Check if current assignment is expired
340
  last_assignment = assignments[-1]
341
  assigned_at = datetime.datetime.fromisoformat(last_assignment["assigned_at"].replace("Z", "+00:00"))
342
  now = datetime.datetime.now(assigned_at.tzinfo)
343
 
344
+ # Get rotation period from job or default to 8 hours
345
+ rotation_period = job_data.get("rotation_period", 28800)
346
+
347
+ if (now - assigned_at).total_seconds() > rotation_period:
348
  print(f"Rotating guard for job {job_id}")
349
  assign_roster(job_id)
350
 
 
390
 
391
  data = request.json
392
  job_id = str(uuid.uuid4())
393
+
394
+ # Validate guarding points (5-15 points)
395
+ guarding_points = data.get("guarding_points", [])
396
+ if len(guarding_points) < 5 or len(guarding_points) > 15:
397
+ return jsonify({"error": "Guarding points must be between 5 and 15"}), 400
398
+
399
+ # Add IDs to guarding points if not present
400
+ for i, point in enumerate(guarding_points):
401
+ if "id" not in point:
402
+ point["id"] = f"point_{i+1}"
403
+
404
+ # Add default fields
405
+ job_data = {
406
+ **data,
407
+ "guarding_points": guarding_points,
408
+ "created_at": datetime.datetime.now().isoformat(),
409
+ "status": "created", # New status: created, active, paused
410
+ "assignments": [],
411
+ "rotation_history": {},
412
+ "rotation_period": data.get("rotation_period", 28800), # Default 8 hours (28800 seconds)
413
+ "scheduled_start_time": data.get("scheduled_start_time"), # ISO format datetime string
414
+ "has_scheduled_rotation": False,
415
+ "special_cases": data.get("special_cases", [])
416
+ }
417
+
418
+ db.reference(f"jobs/{job_id}").set(job_data)
419
  return jsonify({"message": "Job created", "job_id": job_id}), 200
420
 
421
+ @app.route("/schedule_job/<job_id>", methods=["POST"])
422
+ def schedule_job(job_id):
423
+ """Schedule a job to start at a specific time"""
424
+ user = verify_token(request)
425
+ if not user:
426
+ return jsonify({"error": "Unauthorized"}), 401
427
+
428
+ data = request.json
429
+ start_time_iso = data.get("start_time") # ISO format datetime string
430
+
431
+ if not start_time_iso:
432
+ return jsonify({"error": "start_time is required (ISO format)"}), 400
433
+
434
+ job_ref = db.reference(f"jobs/{job_id}")
435
+ job_data = job_ref.get()
436
+
437
+ if not job_
438
+ return jsonify({"error": "Job not found"}), 404
439
+
440
+ # Parse the start time
441
+ try:
442
+ start_time = datetime.datetime.fromisoformat(start_time_iso.replace("Z", "+00:00"))
443
+ except ValueError:
444
+ return jsonify({"error": "Invalid start_time format. Use ISO format (e.g., 2023-12-01T10:00:00)"}), 400
445
+
446
+ # Update job with scheduled start time
447
+ job_ref.update({
448
+ "scheduled_start_time": start_time_iso,
449
+ "status": "scheduled"
450
+ })
451
+
452
+ # Schedule the job to start at the specified time
453
+ scheduler.add_job(
454
+ func=start_scheduled_job,
455
+ trigger="date",
456
+ run_date=start_time,
457
+ args=[job_id],
458
+ id=f"start_{job_id}_{uuid.uuid4().hex[:8]}"
459
+ )
460
+
461
+ return jsonify({"message": f"Job {job_id} scheduled to start at {start_time_iso}"}), 200
462
+
463
+ def start_scheduled_job(job_id):
464
+ """Function to start a scheduled job"""
465
+ try:
466
+ job_ref = db.reference(f"jobs/{job_id}")
467
+ job_data = job_ref.get()
468
+
469
+ if not job_
470
+ print(f"Job {job_id} not found for scheduled start")
471
+ return
472
+
473
+ # Update job status to active
474
+ job_ref.update({
475
+ "status": "active",
476
+ "started_at": datetime.datetime.now().isoformat()
477
+ })
478
+
479
+ # Start first assignment
480
+ assign_roster(job_id)
481
+
482
+ print(f"Job {job_id} started via schedule")
483
+ except Exception as e:
484
+ print(f"Error starting scheduled job {job_id}: {e}")
485
+
486
  @app.route("/start_job/<job_id>", methods=["POST"])
487
  def start_job(job_id):
488
  user = verify_token(request)
489
  if not user:
490
  return jsonify({"error": "Unauthorized"}), 401
491
 
492
+ job_ref = db.reference(f"jobs/{job_id}")
493
+ job_data = job_ref.get()
494
+
495
+ if not job_
496
+ return jsonify({"error": "Job not found"}), 404
497
+
498
+ # Update job status to active
499
+ job_ref.update({
500
+ "status": "active",
501
+ "started_at": datetime.datetime.now().isoformat()
502
+ })
503
+
504
  # Schedule the first assignment immediately
505
  scheduler.add_job(
506
  func=assign_roster,
 
510
  id=f"start_{job_id}_{uuid.uuid4().hex[:8]}"
511
  )
512
 
513
+ return jsonify({"message": f"Job {job_id} started"}), 202
514
+
515
+ @app.route("/pause_job/<job_id>", methods=["POST"])
516
+ def pause_job(job_id):
517
+ user = verify_token(request)
518
+ if not user:
519
+ return jsonify({"error": "Unauthorized"}), 401
520
+
521
+ job_ref = db.reference(f"jobs/{job_id}")
522
+ job_data = job_ref.get()
523
+
524
+ if not job_
525
+ return jsonify({"error": "Job not found"}), 404
526
+
527
+ job_ref.update({"status": "paused"})
528
+ return jsonify({"message": f"Job {job_id} paused"}), 200
529
+
530
+ @app.route("/delete_job/<job_id>", methods=["DELETE"])
531
+ def delete_job(job_id):
532
+ user = verify_token(request)
533
+ if not user:
534
+ return jsonify({"error": "Unauthorized"}), 401
535
+
536
+ job_ref = db.reference(f"jobs/{job_id}")
537
+ job_data = job_ref.get()
538
+
539
+ if not job_
540
+ return jsonify({"error": "Job not found"}), 404
541
+
542
+ job_ref.delete()
543
+ return jsonify({"message": f"Job {job_id} deleted"}), 200
544
+
545
+ @app.route("/jobs", methods=["GET"])
546
+ def get_jobs():
547
+ user = verify_token(request)
548
+ if not user:
549
+ return jsonify({"error": "Unauthorized"}), 401
550
+
551
+ jobs_ref = db.reference("jobs")
552
+ jobs = jobs_ref.get()
553
+
554
+ if not jobs:
555
+ return jsonify({"jobs": []}), 200
556
+
557
+ # Convert to list format
558
+ jobs_list = []
559
+ for job_id, job_data in jobs.items():
560
+ job_data["id"] = job_id
561
+ jobs_list.append(job_data)
562
+
563
+ return jsonify({"jobs": jobs_list}), 200
564
+
565
+ @app.route("/job/<job_id>", methods=["GET"])
566
+ def get_job(job_id):
567
+ user = verify_token(request)
568
+ if not user:
569
+ return jsonify({"error": "Unauthorized"}), 401
570
+
571
+ job_ref = db.reference(f"jobs/{job_id}")
572
+ job_data = job_ref.get()
573
+
574
+ if not job_
575
+ return jsonify({"error": "Job not found"}), 404
576
+
577
+ job_data["id"] = job_id
578
+ return jsonify({"job": job_data}), 200
579
+
580
+ @app.route("/members", methods=["GET"])
581
+ def get_members():
582
+ user = verify_token(request)
583
+ if not user:
584
+ return jsonify({"error": "Unauthorized"}), 401
585
+
586
+ members_ref = db.reference("members")
587
+ members = members_ref.get()
588
+
589
+ if not members:
590
+ return jsonify({"members": []}), 200
591
+
592
+ # Convert to list format
593
+ members_list = []
594
+ for member_id, member_data in members.items():
595
+ member_data["id"] = member_id
596
+ members_list.append(member_data)
597
+
598
+ return jsonify({"members": members_list}), 200
599
+
600
+ @app.route("/assign_members_to_job", methods=["POST"])
601
+ def assign_members_to_job():
602
+ """Bulk assign members to a job"""
603
+ user = verify_token(request)
604
+ if not user:
605
+ return jsonify({"error": "Unauthorized"}), 401
606
+
607
+ data = request.json
608
+ job_id = data.get("job_id")
609
+ member_ids = data.get("member_ids", [])
610
+
611
+ if not job_id or not member_ids:
612
+ return jsonify({"error": "job_id and member_ids are required"}), 400
613
+
614
+ job_ref = db.reference(f"jobs/{job_id}")
615
+ job_data = job_ref.get()
616
+
617
+ if not job_
618
+ return jsonify({"error": "Job not found"}), 404
619
+
620
+ # Get members
621
+ members_ref = db.reference("members")
622
+ all_members = members_ref.get()
623
+
624
+ if not all_members:
625
+ return jsonify({"error": "No members found"}), 404
626
+
627
+ # Filter requested members (ensure at least 25)
628
+ selected_members = []
629
+ for member_id in member_ids:
630
+ if member_id in all_members:
631
+ member_data = all_members[member_id]
632
+ member_data["id"] = member_id
633
+ selected_members.append(member_data)
634
+
635
+ if len(selected_members) < 25:
636
+ return jsonify({"error": "At least 25 members must be assigned to the job"}), 400
637
+
638
+ # Update job with assigned members
639
+ job_ref.update({"assigned_members": selected_members})
640
+
641
+ return jsonify({
642
+ "message": f"{len(selected_members)} members assigned to job {job_id}",
643
+ "assigned_members": selected_members
644
+ }), 200
645
+
646
+ @app.route("/update_job_settings/<job_id>", methods=["POST"])
647
+ def update_job_settings(job_id):
648
+ """Update job settings including rotation period"""
649
+ user = verify_token(request)
650
+ if not user:
651
+ return jsonify({"error": "Unauthorized"}), 401
652
+
653
+ data = request.json
654
+ rotation_period = data.get("rotation_period") # In seconds
655
+
656
+ if rotation_period is None:
657
+ return jsonify({"error": "rotation_period is required (in seconds)"}), 400
658
+
659
+ job_ref = db.reference(f"jobs/{job_id}")
660
+ job_data = job_ref.get()
661
+
662
+ if not job_
663
+ return jsonify({"error": "Job not found"}), 404
664
+
665
+ # Update rotation period
666
+ job_ref.update({"rotation_period": rotation_period})
667
+
668
+ return jsonify({
669
+ "message": f"Job {job_id} rotation period updated to {rotation_period} seconds",
670
+ "rotation_period": rotation_period
671
+ }), 200
672
+
673
+ @app.route("/add_special_case/<job_id>", methods=["POST"])
674
+ def add_special_case(job_id):
675
+ """Add a special case assignment for a job"""
676
+ user = verify_token(request)
677
+ if not user:
678
+ return jsonify({"error": "Unauthorized"}), 401
679
+
680
+ data = request.json
681
+ point_id = data.get("point_id")
682
+ member_id = data.get("member_id")
683
+ reason = data.get("reason", "Special Assignment")
684
+ shift_number = data.get("shift_number")
685
+ active = data.get("active", False)
686
+
687
+ if not point_id or not member_id:
688
+ return jsonify({"error": "point_id and member_id are required"}), 400
689
+
690
+ job_ref = db.reference(f"jobs/{job_id}")
691
+ job_data = job_ref.get()
692
+
693
+ if not job_
694
+ return jsonify({"error": "Job not found"}), 404
695
+
696
+ # Verify point and member exist
697
+ guarding_points = job_data.get("guarding_points", [])
698
+ assigned_members = job_data.get("assigned_members", [])
699
+
700
+ point_exists = any(p["id"] == point_id for p in guarding_points)
701
+ member_exists = any(m["id"] == member_id for m in assigned_members)
702
+
703
+ if not point_exists:
704
+ return jsonify({"error": "Guarding point not found in job"}), 404
705
+ if not member_exists:
706
+ return jsonify({"error": "Member not assigned to job"}), 404
707
+
708
+ # Add special case
709
+ special_case = {
710
+ "id": str(uuid.uuid4()),
711
+ "point_id": point_id,
712
+ "member_id": member_id,
713
+ "reason": reason,
714
+ "shift_number": shift_number,
715
+ "active": active,
716
+ "created_at": datetime.datetime.now().isoformat()
717
+ }
718
+
719
+ special_cases = job_data.get("special_cases", [])
720
+ special_cases.append(special_case)
721
+
722
+ job_ref.update({"special_cases": special_cases})
723
+
724
+ return jsonify({
725
+ "message": "Special case added",
726
+ "special_case": special_case
727
+ }), 200
728
+
729
+ @app.route("/get_roster/<job_id>", methods=["GET"])
730
+ def get_roster(job_id):
731
+ """Get formatted roster for a job"""
732
+ user = verify_token(request)
733
+ if not user:
734
+ return jsonify({"error": "Unauthorized"}), 401
735
+
736
+ job_ref = db.reference(f"jobs/{job_id}")
737
+ job_data = job_ref.get()
738
+
739
+ if not job_
740
+ return jsonify({"error": "Job not found"}), 404
741
+
742
+ assignments = job_data.get("assignments", [])
743
+
744
+ # Format roster as table data
745
+ roster_data = []
746
+ for shift in assignments:
747
+ shift_number = shift["shift_number"]
748
+ assigned_at = shift["assigned_at"]
749
+
750
+ for assignment in shift["assignments"]:
751
+ roster_data.append({
752
+ "shift_number": shift_number,
753
+ "assigned_at": assigned_at,
754
+ "point_name": assignment["point"]["name"],
755
+ "member_name": assignment["member"].get("name", "Unknown"),
756
+ "is_special_case": assignment.get("is_special_case", False),
757
+ "special_case_reason": assignment.get("special_case_reason", "")
758
+ })
759
+
760
+ return jsonify({
761
+ "roster": roster_data,
762
+ "job_name": job_data.get("name", "Unknown Job"),
763
+ "total_shifts": len(assignments)
764
+ }), 200
765
 
766
  # === Schedule periodic guard rotation ===
767
  # Run every 5 minutes to check for rotations
 
771
  minutes=5,
772
  id="guard_rotation_job"
773
  )
 
774
  # === Run Server ===
775
  if __name__ == "__main__":
776
  app.run(debug=True, host="0.0.0.0", port=7860)