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:
+26
-26
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user