Spaces:
Running
Running
Update main.py
Browse files
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 —
|
|
|
|
|
|
|
| 805 |
try:
|
| 806 |
fb_user = auth.get_user_by_email(email)
|
| 807 |
-
|
| 808 |
-
|
|
|
|
|
|
|
| 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 |
"""
|