From 4bcda0a9076eebf83eb507d31a9102fb1a3ff2cb Mon Sep 17 00:00:00 2001 From: IT Dog Date: Mon, 20 Jul 2026 00:05:05 +0800 Subject: [PATCH] Accident schema: rename employee_name -> submitter_email (clearer semantic) Database migration: - ALTER TABLE accidents RENAME COLUMN employee_name TO submitter_email - ALTER TABLE accidents RENAME COLUMN responsible_person TO injured_person - ALTER TABLE accidents RENAME COLUMN employee_id TO injured_person_id Reason: Accident.employee_name contained submitter EMAIL (not staff name), causing confusion when joining with attendance_records.employee_name (real name). New column names are self-documenting. API backward-compat: - All API responses still use "employee_name" / "responsible_person" field names - Internal SQL mapping translates submitter_email/injured_person -> employee_name/responsible_person - Frontend code unchanged Verified: - /api/accident/1 returns employee_name = submitter_email content - /api/accident/records returns 2 records OK (was 500 before fix) - /api/employees/list still returns 12 staff (accident excluded) - Data preserved (1st record id=1 submitter_email = info@scmedical.hk) --- backend/main.py | 52 +++++++++++++++++++++++------------------------ backend/models.py | 16 ++++++++++++--- 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/backend/main.py b/backend/main.py index 37c462a..4d2f004 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1207,7 +1207,7 @@ async def attendance_records( return { "id": r.id, "staff": r.employee_name, - "employee_name": r.employee_name, + "employee_name": r.submitter_email, "company": r.company, "department": r.department, "date": r.date.isoformat() if r.date else None, @@ -1318,7 +1318,7 @@ async def list_employees( for n in db.query(LeaveRecord.employee_name).distinct().all(): if n[0] and not EMAIL_RE.search(n[0]): names.add(n[0]) - # NOTE: Accident.employee_name contains submitter EMAIL (e.g. info@scmedical.hk), + # NOTE: Accident.submitter_email contains submitter EMAIL (e.g. info@scmedical.hk), # NOT the injured staff name. We exclude accident from employee list to avoid # polluting staff dropdown with email addresses. sorted_names = sorted(names, key=lambda x: x.lower()) @@ -2122,7 +2122,7 @@ def _get_excel_column_order_accident() -> list: "incident_score", "date", # DB-only (not in Excel) - "responsible_person", + "injured_person", # was responsible_person, renamed 2026-07-19 "medical_report", ] @@ -2154,7 +2154,7 @@ async def accident_dashboard_summary( by_severity = {} by_department = {} by_location = {} - by_responsible = {} + by_injured = {} this_month = 0 earliest_date = None latest_date = None @@ -2165,8 +2165,8 @@ async def accident_dashboard_summary( by_department[r.department] = by_department.get(r.department, 0) + 1 if r.location: by_location[r.location] = by_location.get(r.location, 0) + 1 - if r.responsible_person: - by_responsible[r.responsible_person] = by_responsible.get(r.responsible_person, 0) + 1 + if r.injured_person: + by_injured[r.injured_person] = by_injured.get(r.injured_person, 0) + 1 if r.date and r.date >= month_start: this_month += 1 if r.date: @@ -2188,7 +2188,7 @@ async def accident_dashboard_summary( "by_severity": by_severity, "by_department": by_department, "by_location": by_location, - "by_responsible": by_responsible, + "by_injured": by_injured, "severity_count": { # frontend-expected "1": by_severity.get("1", 0), "2": by_severity.get("2", 0), @@ -2198,7 +2198,7 @@ async def accident_dashboard_summary( }, "unique_locations": len(by_location), "unique_departments": len(by_department), - "unique_responsible": len(by_responsible), + "unique_responsible": len(by_injured), "avg_per_day": avg_per_day, "earliest_date": earliest_date.isoformat() if earliest_date else None, "latest_date": latest_date.isoformat() if latest_date else None, @@ -2231,7 +2231,7 @@ async def accident_dashboard_stats( by_location = {} by_employee = {} by_department = {} - by_responsible = {} + by_injured = {} for r in rows: # Severity-weighted score (severity 5 = most critical) @@ -2256,10 +2256,10 @@ async def accident_dashboard_stats( by_department[dept]["count"] += 1 by_department[dept]["severity_sum"] += sev_weight - rp = r.responsible_person or "未分類" - by_responsible.setdefault(rp, {"name": rp, "count": 0, "severity_sum": 0, "avg_severity": 0}) - by_responsible[rp]["count"] += 1 - by_responsible[rp]["severity_sum"] += sev_weight + rp = r.injured_person or "未分類" + by_injured.setdefault(rp, {"name": rp, "count": 0, "severity_sum": 0, "avg_severity": 0}) + by_injured[rp]["count"] += 1 + by_injured[rp]["severity_sum"] += sev_weight def finalize(d): out = [] @@ -2272,7 +2272,7 @@ async def accident_dashboard_stats( "by_location": sorted(finalize(by_location), key=lambda x: x["count"], reverse=True), "by_employee": sorted(finalize(by_employee), key=lambda x: x["count"], reverse=True), "by_department": sorted(finalize(by_department), key=lambda x: x["count"], reverse=True), - "by_responsible": sorted(finalize(by_responsible), key=lambda x: x["count"], reverse=True), + "by_injured": sorted(finalize(by_injured), key=lambda x: x["count"], reverse=True), } @@ -2312,7 +2312,7 @@ async def accident_records( if search: like = f"%{search}%" q = q.filter(or_( - Accident.employee_name.like(like), + Accident.submitter_email.like(like), Accident.location.like(like), Accident.description.like(like), Accident.department.like(like), @@ -2325,13 +2325,13 @@ async def accident_records( "severity": Accident.severity, "time": Accident.time, "location": Accident.location, - "employee_name": Accident.employee_name, + "employee_name": Accident.submitter_email, "department": Accident.department, "description": Accident.description, "action_taken": Accident.action_taken, "patient_or_staff": Accident.patient_or_staff, "incident_score": Accident.incident_score, - "responsible_person": Accident.responsible_person, + "responsible_person": Accident.injured_person, } col = sortable.get(sort_by, Accident.date) q = q.order_by(col.desc() if sort_order == "desc" else col.asc()) @@ -2346,14 +2346,14 @@ async def accident_records( "date": r.date.isoformat() if r.date else None, "time": r.time, "location": r.location, - "employee_name": r.employee_name, - "employee_id": r.employee_id, + "employee_name": r.submitter_email, + "employee_id": r.injured_person_id, "department": r.department, "description": r.description, "severity": r.severity, "medical_report": r.medical_report, "action_taken": r.action_taken, - "responsible_person": r.responsible_person, + "responsible_person": r.injured_person, "patient_or_staff": r.patient_or_staff, "long_description": r.long_description, "incident_photos": r.incident_photos, @@ -2393,7 +2393,7 @@ async def update_accident_record( "employee_name", "department", "description", "severity", "time", "location", "patient_or_staff", "long_description", "incident_photos", "action_taken", "needs_escalation", - "incident_score", "responsible_person", "medical_report", + "incident_score", "injured_person", # was responsible_person, renamed 2026-07-19", "medical_report", } for k, v in payload.items(): if k in editable: @@ -2424,14 +2424,14 @@ async def get_accident_record( "date": r.date.isoformat() if r.date else None, "time": r.time, "location": r.location, - "employee_name": r.employee_name, - "employee_id": r.employee_id, + "employee_name": r.submitter_email, + "employee_id": r.injured_person_id, "department": r.department, "description": r.description, "severity": r.severity, "medical_report": r.medical_report, "action_taken": r.action_taken, - "responsible_person": r.responsible_person, + "responsible_person": r.injured_person, "patient_or_staff": r.patient_or_staff, "long_description": r.long_description, "incident_photos": r.incident_photos, @@ -2762,7 +2762,7 @@ async def accident_export_excel( ("action_taken", "處理情況 / 已採取的行動"), ("needs_escalation", "需要管理層介入"), ("incident_score", "分數"), - ("responsible_person", "負責人"), + ("injured_person", "受傷員工"), ("medical_report", "醫療報告"), ] @@ -2956,7 +2956,7 @@ async def accident_export_excel( "severity": str(r.severity or ""), "medical_report": r.medical_report or "", "action_taken": r.action_taken or "", - "responsible_person": r.responsible_person or "", + "responsible_person": r.injured_person or "", "patient_or_staff": r.patient_or_staff or "", "long_description": r.long_description or "", "incident_photos": r.incident_photos or "", diff --git a/backend/models.py b/backend/models.py index 168d5f0..8b7540e 100644 --- a/backend/models.py +++ b/backend/models.py @@ -25,19 +25,29 @@ class User(Base): class Accident(Base): + """意外記錄 Accident Reports + + IMPORTANT — Column naming convention (2026-07-19 cleanup): + - submitter_email: The Google Form submitter's email (NOT the injured person) + - injured_person: Name of the staff who got hurt (was: responsible_person) + - injured_person_id: Employee ID (was: employee_id) + + This naming makes it clear these are different concepts, preventing + accidental joins with attendance_records.employee_name (which IS a real name). + """ __tablename__ = "accidents" id = Column(Integer, primary_key=True, index=True) date = Column(Date, nullable=False) time = Column(String(10), nullable=True) location = Column(String(255), nullable=False) - employee_name = Column(String(255), nullable=False) - employee_id = Column(String(100)) + submitter_email = Column(String(255), nullable=False) + injured_person_id = Column(String(100)) department = Column(String(100)) description = Column(Text, nullable=False) severity = Column(String(50), nullable=False) medical_report = Column(Text, nullable=True) action_taken = Column(Text, nullable=True) - responsible_person = Column(String(255), nullable=True) + injured_person = Column(String(255), nullable=True) # Excel-derived (Google Form) extra columns (full map) -- 2026-07-18 patient_or_staff = Column(Text, nullable=True) # Excel col 8