rairo commited on
Commit
2aa3865
·
verified ·
1 Parent(s): ba126ae

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +156 -3
main.py CHANGED
@@ -801,11 +801,15 @@ def add_user(org_id):
801
  fb_user = auth.create_user(email=email, password=password, display_name=display_name)
802
  except Exception as e:
803
  if "EMAIL_EXISTS" in str(e):
804
- # User exists in Auth — try to get their uid and add to org
 
 
805
  try:
806
  fb_user = auth.get_user_by_email(email)
807
- except Exception:
808
- return jsonify({"error": "Email already registered to another account."}), 409
 
 
809
  else:
810
  raise
811
 
@@ -968,6 +972,131 @@ def list_kpi_models(org_id):
968
  return jsonify({"error": str(e)}), 403
969
 
970
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
971
  @app.route("/api/org/<string:org_id>/tasks", methods=["POST"])
972
  def create_task(org_id):
973
  """
@@ -1858,6 +1987,30 @@ def mark_notification_read(org_id, notif_id):
1858
  return jsonify({"error": str(e)}), 403
1859
 
1860
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1861
  @app.route("/api/org/<string:org_id>/notifications/broadcast", methods=["POST"])
1862
  def broadcast_notification(org_id):
1863
  """
 
801
  fb_user = auth.create_user(email=email, password=password, display_name=display_name)
802
  except Exception as e:
803
  if "EMAIL_EXISTS" in str(e):
804
+ # User already exists in Firebase Auth — look them up and add to this org.
805
+ # This is intentional: an employee may have an account from another org
806
+ # or a pre-existing Firebase account. We enrol them here explicitly.
807
  try:
808
  fb_user = auth.get_user_by_email(email)
809
+ logger.info(f"add_user | Email {email} exists in Auth — enrolling uid={fb_user.uid} into org={org_id}")
810
+ except Exception as lookup_err:
811
+ logger.error(f"add_user | Could not look up existing user {email}: {lookup_err}")
812
+ return jsonify({"error": "Email already registered and could not be retrieved."}), 409
813
  else:
814
  raise
815
 
 
972
  return jsonify({"error": str(e)}), 403
973
 
974
 
975
+ @app.route("/api/org/<string:org_id>/kpi-models/preview", methods=["POST"])
976
+ def preview_kpi_model(org_id):
977
+ """
978
+ Gemini synthesises a KPI model from natural language and returns it for admin
979
+ review WITHOUT saving it. Admin confirms by calling POST /kpi-models with the
980
+ same payload (or the returned kpi_categories directly).
981
+ Body: { role_name, natural_language }
982
+ """
983
+ try:
984
+ uid, role, _ = verify_org_admin(request.headers.get("Authorization"), org_id)
985
+ data = request.get_json() or {}
986
+ role_name = data.get("role_name", "").strip()
987
+ nl = data.get("natural_language", "").strip()
988
+ if not role_name or not nl:
989
+ return jsonify({"error": "role_name and natural_language required."}), 400
990
+ synthesized = gemini_synthesize_kpi(nl, role_name)
991
+ return jsonify({"preview": synthesized, "confirmed": False}), 200
992
+ except PermissionError as e:
993
+ return jsonify({"error": str(e)}), 403
994
+ except Exception as e:
995
+ logger.error(f"preview_kpi_model failed: {e}")
996
+ return jsonify({"error": str(e)}), 500
997
+
998
+
999
+ @app.route("/api/org/<string:org_id>/kpi-models/<string:model_id>", methods=["PUT"])
1000
+ def update_kpi_model(org_id, model_id):
1001
+ """
1002
+ Update an existing KPI model.
1003
+ Body: { role_name?, kpi_categories?, natural_language? }
1004
+ If natural_language provided, Gemini re-synthesises. Otherwise accepts kpi_categories directly.
1005
+ """
1006
+ try:
1007
+ uid, role, _ = verify_org_admin(request.headers.get("Authorization"), org_id)
1008
+ model_ref = db_ref.child(f"kpi_models/{org_id}/{model_id}")
1009
+ model_data = model_ref.get()
1010
+ if not model_data:
1011
+ return jsonify({"error": "KPI model not found."}), 404
1012
+ data = request.get_json() or {}
1013
+ update = {"updatedAt": _now_iso(), "updatedBy": uid}
1014
+ if data.get("role_name"):
1015
+ update["role"] = data["role_name"]
1016
+ if data.get("natural_language"):
1017
+ synthesized = gemini_synthesize_kpi(data["natural_language"], data.get("role_name", model_data.get("role", "")))
1018
+ update["kpi_categories"] = synthesized["kpi_categories"]
1019
+ update["synthesized_from"] = data["natural_language"]
1020
+ elif data.get("kpi_categories"):
1021
+ update["kpi_categories"] = data["kpi_categories"]
1022
+ update["synthesized_from"] = "manual"
1023
+ model_ref.update(update)
1024
+ _write_audit(org_id, uid, "kpi_model_updated", {"model_id": model_id})
1025
+ return jsonify({**model_data, **update}), 200
1026
+ except PermissionError as e:
1027
+ return jsonify({"error": str(e)}), 403
1028
+ except Exception as e:
1029
+ logger.error(f"update_kpi_model failed: {e}")
1030
+ return jsonify({"error": str(e)}), 500
1031
+
1032
+
1033
+ @app.route("/api/org/<string:org_id>/kpi-models/<string:model_id>", methods=["DELETE"])
1034
+ def delete_kpi_model(org_id, model_id):
1035
+ """
1036
+ Delete a KPI model. Org admin only.
1037
+ Models in active use (referenced by submissions) are soft-deleted — marked inactive
1038
+ rather than removed, so historical alignment scores remain meaningful.
1039
+ """
1040
+ try:
1041
+ uid, role, _ = verify_org_admin(request.headers.get("Authorization"), org_id)
1042
+ if role != "org_admin":
1043
+ return jsonify({"error": "Only org_admin can delete KPI models."}), 403
1044
+ model_ref = db_ref.child(f"kpi_models/{org_id}/{model_id}")
1045
+ model_data = model_ref.get()
1046
+ if not model_data:
1047
+ return jsonify({"error": "KPI model not found."}), 404
1048
+ # Check if any submission references this model
1049
+ all_subs = db_ref.child(f"submissions/{org_id}").get() or {}
1050
+ in_use = any(
1051
+ isinstance(week_sub, dict) and week_sub.get("kpiModelId") == model_id
1052
+ for user_subs in all_subs.values() if isinstance(user_subs, dict)
1053
+ for week_sub in user_subs.values() if isinstance(week_sub, dict)
1054
+ )
1055
+ if in_use:
1056
+ # Soft delete — keep data, mark inactive
1057
+ model_ref.update({"active": False, "deletedAt": _now_iso(), "deletedBy": uid})
1058
+ _write_audit(org_id, uid, "kpi_model_soft_deleted", {"model_id": model_id})
1059
+ return jsonify({"success": True, "soft_deleted": True, "reason": "Model has historical submissions — marked inactive rather than removed."}), 200
1060
+ model_ref.delete()
1061
+ _write_audit(org_id, uid, "kpi_model_deleted", {"model_id": model_id})
1062
+ return jsonify({"success": True, "soft_deleted": False}), 200
1063
+ except PermissionError as e:
1064
+ return jsonify({"error": str(e)}), 403
1065
+ except Exception as e:
1066
+ logger.error(f"delete_kpi_model failed: {e}")
1067
+ return jsonify({"error": str(e)}), 500
1068
+
1069
+
1070
+ @app.route("/api/org/<string:org_id>/tasks/preview", methods=["POST"])
1071
+ def preview_task(org_id):
1072
+ """
1073
+ Gemini structures a task from natural language and returns it for admin review
1074
+ WITHOUT saving. Admin confirms by calling POST /tasks with the same payload.
1075
+ Body: { natural_language, kpi_model_id? }
1076
+ """
1077
+ try:
1078
+ uid, role, _ = verify_org_admin(request.headers.get("Authorization"), org_id)
1079
+ data = request.get_json() or {}
1080
+ nl = data.get("natural_language", "").strip()
1081
+ if not nl:
1082
+ return jsonify({"error": "natural_language required."}), 400
1083
+ org = get_org(org_id)
1084
+ org_context = (org.get("name", "") + " — " + org.get("industry", "")) if org else org_id
1085
+ kpi_cats = []
1086
+ if data.get("kpi_model_id"):
1087
+ model = db_ref.child(f"kpi_models/{org_id}/{data['kpi_model_id']}").get()
1088
+ if model:
1089
+ kpi_cats = [c["name"] for c in model.get("kpi_categories", [])]
1090
+ structured = gemini_synthesize_task(nl, org_context, kpi_cats)
1091
+ due_date = (date.today() + timedelta(days=structured.get("suggested_due_days", 7))).isoformat()
1092
+ return jsonify({"preview": {**structured, "resolved_due_date": due_date}, "confirmed": False}), 200
1093
+ except PermissionError as e:
1094
+ return jsonify({"error": str(e)}), 403
1095
+ except Exception as e:
1096
+ logger.error(f"preview_task failed: {e}")
1097
+ return jsonify({"error": str(e)}), 500
1098
+
1099
+
1100
  @app.route("/api/org/<string:org_id>/tasks", methods=["POST"])
1101
  def create_task(org_id):
1102
  """
 
1987
  return jsonify({"error": str(e)}), 403
1988
 
1989
 
1990
+ @app.route("/api/org/<string:org_id>/notifications/mark-all-read", methods=["PUT"])
1991
+ def mark_all_notifications_read(org_id):
1992
+ """
1993
+ Marks all unread notifications for the calling user as read in a single write.
1994
+ Far cheaper than fanning out individual PUTs from the frontend.
1995
+ """
1996
+ try:
1997
+ uid, _ = verify_org_member(request.headers.get("Authorization"), org_id)
1998
+ notifs = db_ref.child(f"notifications/{org_id}/{uid}").get() or {}
1999
+ updates = {
2000
+ notif_id: {**notif_data, "read": True}
2001
+ for notif_id, notif_data in notifs.items()
2002
+ if isinstance(notif_data, dict) and not notif_data.get("read", False)
2003
+ }
2004
+ if updates:
2005
+ db_ref.child(f"notifications/{org_id}/{uid}").update(updates)
2006
+ return jsonify({"success": True, "marked": len(updates)}), 200
2007
+ except PermissionError as e:
2008
+ return jsonify({"error": str(e)}), 403
2009
+ except Exception as e:
2010
+ logger.error(f"mark_all_notifications_read failed: {e}")
2011
+ return jsonify({"error": str(e)}), 500
2012
+
2013
+
2014
  @app.route("/api/org/<string:org_id>/notifications/broadcast", methods=["POST"])
2015
  def broadcast_notification(org_id):
2016
  """