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)
This commit is contained in:
IT Dog
2026-07-20 00:05:05 +08:00
parent c0076a125c
commit 4bcda0a907
2 changed files with 39 additions and 29 deletions
+26 -26
View File
@@ -1207,7 +1207,7 @@ async def attendance_records(
return { return {
"id": r.id, "id": r.id,
"staff": r.employee_name, "staff": r.employee_name,
"employee_name": r.employee_name, "employee_name": r.submitter_email,
"company": r.company, "company": r.company,
"department": r.department, "department": r.department,
"date": r.date.isoformat() if r.date else None, "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(): for n in db.query(LeaveRecord.employee_name).distinct().all():
if n[0] and not EMAIL_RE.search(n[0]): if n[0] and not EMAIL_RE.search(n[0]):
names.add(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 # NOT the injured staff name. We exclude accident from employee list to avoid
# polluting staff dropdown with email addresses. # polluting staff dropdown with email addresses.
sorted_names = sorted(names, key=lambda x: x.lower()) sorted_names = sorted(names, key=lambda x: x.lower())
@@ -2122,7 +2122,7 @@ def _get_excel_column_order_accident() -> list:
"incident_score", "incident_score",
"date", "date",
# DB-only (not in Excel) # DB-only (not in Excel)
"responsible_person", "injured_person", # was responsible_person, renamed 2026-07-19
"medical_report", "medical_report",
] ]
@@ -2154,7 +2154,7 @@ async def accident_dashboard_summary(
by_severity = {} by_severity = {}
by_department = {} by_department = {}
by_location = {} by_location = {}
by_responsible = {} by_injured = {}
this_month = 0 this_month = 0
earliest_date = None earliest_date = None
latest_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 by_department[r.department] = by_department.get(r.department, 0) + 1
if r.location: if r.location:
by_location[r.location] = by_location.get(r.location, 0) + 1 by_location[r.location] = by_location.get(r.location, 0) + 1
if r.responsible_person: if r.injured_person:
by_responsible[r.responsible_person] = by_responsible.get(r.responsible_person, 0) + 1 by_injured[r.injured_person] = by_injured.get(r.injured_person, 0) + 1
if r.date and r.date >= month_start: if r.date and r.date >= month_start:
this_month += 1 this_month += 1
if r.date: if r.date:
@@ -2188,7 +2188,7 @@ async def accident_dashboard_summary(
"by_severity": by_severity, "by_severity": by_severity,
"by_department": by_department, "by_department": by_department,
"by_location": by_location, "by_location": by_location,
"by_responsible": by_responsible, "by_injured": by_injured,
"severity_count": { # frontend-expected "severity_count": { # frontend-expected
"1": by_severity.get("1", 0), "1": by_severity.get("1", 0),
"2": by_severity.get("2", 0), "2": by_severity.get("2", 0),
@@ -2198,7 +2198,7 @@ async def accident_dashboard_summary(
}, },
"unique_locations": len(by_location), "unique_locations": len(by_location),
"unique_departments": len(by_department), "unique_departments": len(by_department),
"unique_responsible": len(by_responsible), "unique_responsible": len(by_injured),
"avg_per_day": avg_per_day, "avg_per_day": avg_per_day,
"earliest_date": earliest_date.isoformat() if earliest_date else None, "earliest_date": earliest_date.isoformat() if earliest_date else None,
"latest_date": latest_date.isoformat() if latest_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_location = {}
by_employee = {} by_employee = {}
by_department = {} by_department = {}
by_responsible = {} by_injured = {}
for r in rows: for r in rows:
# Severity-weighted score (severity 5 = most critical) # Severity-weighted score (severity 5 = most critical)
@@ -2256,10 +2256,10 @@ async def accident_dashboard_stats(
by_department[dept]["count"] += 1 by_department[dept]["count"] += 1
by_department[dept]["severity_sum"] += sev_weight by_department[dept]["severity_sum"] += sev_weight
rp = r.responsible_person or "未分類" rp = r.injured_person or "未分類"
by_responsible.setdefault(rp, {"name": rp, "count": 0, "severity_sum": 0, "avg_severity": 0}) by_injured.setdefault(rp, {"name": rp, "count": 0, "severity_sum": 0, "avg_severity": 0})
by_responsible[rp]["count"] += 1 by_injured[rp]["count"] += 1
by_responsible[rp]["severity_sum"] += sev_weight by_injured[rp]["severity_sum"] += sev_weight
def finalize(d): def finalize(d):
out = [] out = []
@@ -2272,7 +2272,7 @@ async def accident_dashboard_stats(
"by_location": sorted(finalize(by_location), key=lambda x: x["count"], reverse=True), "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_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_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: if search:
like = f"%{search}%" like = f"%{search}%"
q = q.filter(or_( q = q.filter(or_(
Accident.employee_name.like(like), Accident.submitter_email.like(like),
Accident.location.like(like), Accident.location.like(like),
Accident.description.like(like), Accident.description.like(like),
Accident.department.like(like), Accident.department.like(like),
@@ -2325,13 +2325,13 @@ async def accident_records(
"severity": Accident.severity, "severity": Accident.severity,
"time": Accident.time, "time": Accident.time,
"location": Accident.location, "location": Accident.location,
"employee_name": Accident.employee_name, "employee_name": Accident.submitter_email,
"department": Accident.department, "department": Accident.department,
"description": Accident.description, "description": Accident.description,
"action_taken": Accident.action_taken, "action_taken": Accident.action_taken,
"patient_or_staff": Accident.patient_or_staff, "patient_or_staff": Accident.patient_or_staff,
"incident_score": Accident.incident_score, "incident_score": Accident.incident_score,
"responsible_person": Accident.responsible_person, "responsible_person": Accident.injured_person,
} }
col = sortable.get(sort_by, Accident.date) col = sortable.get(sort_by, Accident.date)
q = q.order_by(col.desc() if sort_order == "desc" else col.asc()) 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, "date": r.date.isoformat() if r.date else None,
"time": r.time, "time": r.time,
"location": r.location, "location": r.location,
"employee_name": r.employee_name, "employee_name": r.submitter_email,
"employee_id": r.employee_id, "employee_id": r.injured_person_id,
"department": r.department, "department": r.department,
"description": r.description, "description": r.description,
"severity": r.severity, "severity": r.severity,
"medical_report": r.medical_report, "medical_report": r.medical_report,
"action_taken": r.action_taken, "action_taken": r.action_taken,
"responsible_person": r.responsible_person, "responsible_person": r.injured_person,
"patient_or_staff": r.patient_or_staff, "patient_or_staff": r.patient_or_staff,
"long_description": r.long_description, "long_description": r.long_description,
"incident_photos": r.incident_photos, "incident_photos": r.incident_photos,
@@ -2393,7 +2393,7 @@ async def update_accident_record(
"employee_name", "department", "description", "severity", "employee_name", "department", "description", "severity",
"time", "location", "patient_or_staff", "long_description", "time", "location", "patient_or_staff", "long_description",
"incident_photos", "action_taken", "needs_escalation", "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(): for k, v in payload.items():
if k in editable: if k in editable:
@@ -2424,14 +2424,14 @@ async def get_accident_record(
"date": r.date.isoformat() if r.date else None, "date": r.date.isoformat() if r.date else None,
"time": r.time, "time": r.time,
"location": r.location, "location": r.location,
"employee_name": r.employee_name, "employee_name": r.submitter_email,
"employee_id": r.employee_id, "employee_id": r.injured_person_id,
"department": r.department, "department": r.department,
"description": r.description, "description": r.description,
"severity": r.severity, "severity": r.severity,
"medical_report": r.medical_report, "medical_report": r.medical_report,
"action_taken": r.action_taken, "action_taken": r.action_taken,
"responsible_person": r.responsible_person, "responsible_person": r.injured_person,
"patient_or_staff": r.patient_or_staff, "patient_or_staff": r.patient_or_staff,
"long_description": r.long_description, "long_description": r.long_description,
"incident_photos": r.incident_photos, "incident_photos": r.incident_photos,
@@ -2762,7 +2762,7 @@ async def accident_export_excel(
("action_taken", "處理情況 / 已採取的行動"), ("action_taken", "處理情況 / 已採取的行動"),
("needs_escalation", "需要管理層介入"), ("needs_escalation", "需要管理層介入"),
("incident_score", "分數"), ("incident_score", "分數"),
("responsible_person", "負責人"), ("injured_person", "受傷員工"),
("medical_report", "醫療報告"), ("medical_report", "醫療報告"),
] ]
@@ -2956,7 +2956,7 @@ async def accident_export_excel(
"severity": str(r.severity or ""), "severity": str(r.severity or ""),
"medical_report": r.medical_report or "", "medical_report": r.medical_report or "",
"action_taken": r.action_taken 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 "", "patient_or_staff": r.patient_or_staff or "",
"long_description": r.long_description or "", "long_description": r.long_description or "",
"incident_photos": r.incident_photos or "", "incident_photos": r.incident_photos or "",
+13 -3
View File
@@ -25,19 +25,29 @@ class User(Base):
class Accident(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" __tablename__ = "accidents"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
date = Column(Date, nullable=False) date = Column(Date, nullable=False)
time = Column(String(10), nullable=True) time = Column(String(10), nullable=True)
location = Column(String(255), nullable=False) location = Column(String(255), nullable=False)
employee_name = Column(String(255), nullable=False) submitter_email = Column(String(255), nullable=False)
employee_id = Column(String(100)) injured_person_id = Column(String(100))
department = Column(String(100)) department = Column(String(100))
description = Column(Text, nullable=False) description = Column(Text, nullable=False)
severity = Column(String(50), nullable=False) severity = Column(String(50), nullable=False)
medical_report = Column(Text, nullable=True) medical_report = Column(Text, nullable=True)
action_taken = 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 # Excel-derived (Google Form) extra columns (full map) -- 2026-07-18
patient_or_staff = Column(Text, nullable=True) # Excel col 8 patient_or_staff = Column(Text, nullable=True) # Excel col 8