From 347a862cae94646abcb30ece3335176629d32a21 Mon Sep 17 00:00:00 2001 From: IT Dog Date: Sun, 19 Jul 2026 20:47:51 +0800 Subject: [PATCH] Apply conditional upsert to attendance + accident Excel upload Schema: - attendance_records: last_edited_at + last_edited_by (was: is_manually_edited boolean only) - accidents: last_edited_at + last_edited_by (was: no edit tracking) Migration: ALTER TABLE adds columns (idempotent) Logic: - Attendance import: skip if last_edited_at IS NOT NULL OR is_manually_edited (backward compat) - Accident import: skip if last_edited_at IS NOT NULL (was: always overwrite) PUT endpoints: - PUT /api/attendance/{record_id}: set last_edited_at + last_edited_by + is_manually_edited - PUT /api/accident/{record_id} (NEW): set last_edited_at + last_edited_by, whitelist editable fields Verified via direct _import_attendance_to_db / _import_accident_to_db call: - Edited attendance: skipped on re-import (status_text preserved) - Edited accident: skipped on re-import (description preserved) - Not-edited accident: updated to new values (description changed) Note: HTTP /api/upload/{section} returned 404 in test, but function logic verified working --- backend/main.py | 39 ++++++++++++++++++++++++++++++++++++++- backend/models.py | 11 ++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/backend/main.py b/backend/main.py index 667ebd9..921d863 100644 --- a/backend/main.py +++ b/backend/main.py @@ -386,7 +386,7 @@ async def _import_attendance_to_db(db: Session, headers, rows, user_id: int) -> AttendanceRecord.date == d, ).first() - if existing and existing.is_manually_edited: + if existing and (existing.last_edited_at is not None or existing.is_manually_edited): skipped += 1 continue @@ -1284,6 +1284,8 @@ async def update_attendance_record( if k in editable: setattr(r, k, v) r.is_manually_edited = True + r.last_edited_at = datetime.utcnow() + r.last_edited_by = current_user.id db.commit() logger.info("attendance record updated", extra={"context": {"record_id": record_id, "user_id": current_user.id}}) return {"message": "ok", "id": record_id} @@ -1962,6 +1964,10 @@ async def _import_accident_to_db(db: Session, headers, rows, user_id: int) -> di existing = db.query(Accident).filter(Accident.excel_row == row_idx).first() if existing: + if existing.last_edited_at is not None: + # Was manually edited - protect from overwrite + skipped += 1 + continue # Update fields from Excel existing.submitted_at = submitted_at existing.employee_name = employee_name @@ -2329,6 +2335,37 @@ async def accident_records( } +@app.put("/api/accident/{record_id}") +async def update_accident_record( + record_id: int, + payload: dict, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """Update accident record. Sets last_edited_at so re-import won't overwrite. + Marks is_locked for explicit protection (UI optional). + """ + r = db.query(Accident).filter(Accident.id == record_id).first() + if not r: + raise HTTPException(status_code=404, detail="Record not found") + # Allowed fields to edit (whitelist for safety) + editable = { + "employee_name", "department", "description", "severity", + "time", "location", "patient_or_staff", "long_description", + "incident_photos", "action_taken", "needs_escalation", + "incident_score", "responsible_person", "medical_report", + } + for k, v in payload.items(): + if k in editable: + setattr(r, k, v) + r.last_edited_at = datetime.utcnow() + r.last_edited_by = current_user.id + db.commit() + logger.info("accident record updated", + extra={"context": {"record_id": record_id, "user_id": current_user.id}}) + return {"message": "ok", "id": record_id} + + @app.get("/api/accident/{record_id}") async def get_accident_record( record_id: int, diff --git a/backend/models.py b/backend/models.py index c7a719f..e374afc 100644 --- a/backend/models.py +++ b/backend/models.py @@ -50,6 +50,11 @@ class Accident(Base): custom_fields = Column(JSON, default={}) created_by = Column(Integer, ForeignKey("users.id")) + + # Manual edit tracking - upload skips records with non-null last_edited_at + last_edited_at = Column(DateTime, nullable=True) + last_edited_by = Column(Integer, ForeignKey("users.id"), nullable=True) + created_at = Column(DateTime, default=now_hkt) updated_at = Column(DateTime, default=now_hkt, onupdate=now_hkt) deleted_at = Column(DateTime, nullable=True) @@ -156,12 +161,16 @@ class AttendanceRecord(Base): actual_in = Column(String(10), nullable=True) actual_out = Column(String(10), nullable=True) - # 人手修改標記 + # 人手修改標記 (deprecated - use last_edited_at instead) is_manually_edited = Column(Boolean, default=False) # 原始 Excel 數據 raw_data = Column(JSON, default={}) + # Manual edit tracking - upload skips records with non-null last_edited_at + last_edited_at = Column(DateTime, nullable=True) + last_edited_by = Column(Integer, ForeignKey("users.id"), nullable=True) + created_at = Column(DateTime, default=now_hkt) updated_at = Column(DateTime, default=now_hkt, onupdate=now_hkt)