from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, Date, JSON, ForeignKey, UniqueConstraint, Numeric from sqlalchemy.orm import relationship from sqlalchemy.sql import func from database import Base from datetime import datetime, timezone def now_hkt(): return datetime.now(timezone.utc) # ============ EXISTING MODELS (kept) ============ class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) email = Column(String(255), unique=True, index=True, nullable=False) password_hash = Column(String(255), nullable=False) name = Column(String(255), nullable=False) role = Column(String(50), default="user") is_active = Column(Boolean, default=True) created_at = Column(DateTime, default=now_hkt) updated_at = Column(DateTime, default=now_hkt, onupdate=now_hkt) deleted_at = Column(DateTime, nullable=True) class Accident(Base): __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)) 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) # Excel-derived (Google Form) extra columns (full map) -- 2026-07-18 patient_or_staff = Column(Text, nullable=True) # Excel col 8 long_description = Column(Text, nullable=True) # Excel col 9 incident_photos = Column(Text, nullable=True) # Excel col 10 (URL) needs_escalation = Column(String(20), nullable=True) # Excel col 12 incident_score = Column(Integer, nullable=True) # Excel col 14 submitted_at = Column(DateTime, nullable=True) # Excel col 1 (timestamp) excel_row = Column(Integer, nullable=True) # original Excel row (1-based) custom_fields = Column(JSON, default={}) created_by = Column(Integer, ForeignKey("users.id")) created_at = Column(DateTime, default=now_hkt) updated_at = Column(DateTime, default=now_hkt, onupdate=now_hkt) deleted_at = Column(DateTime, nullable=True) # ============ NEW MODELS ============ class Shift(Base): """班次定義""" __tablename__ = "shifts" id = Column(Integer, primary_key=True, index=True) shift_code = Column(String(20), unique=True, nullable=False, index=True) description = Column(String(100), nullable=True) # 每天的上班時間 (HH:MM format as String) mon_start = Column(String(5), nullable=True) mon_end = Column(String(5), nullable=True) tue_start = Column(String(5), nullable=True) tue_end = Column(String(5), nullable=True) wed_start = Column(String(5), nullable=True) wed_end = Column(String(5), nullable=True) thu_start = Column(String(5), nullable=True) thu_end = Column(String(5), nullable=True) fri_start = Column(String(5), nullable=True) fri_end = Column(String(5), nullable=True) sat_start = Column(String(5), nullable=True) sat_end = Column(String(5), nullable=True) sun_start = Column(String(5), nullable=True) sun_end = Column(String(5), nullable=True) created_at = Column(DateTime, default=now_hkt) updated_at = Column(DateTime, default=now_hkt, onupdate=now_hkt) def get_schedule(self, weekday: str): """取得指定星期幾的上班時間 (start, end)""" day_map = { 'monday': ('mon_start', 'mon_end'), 'tuesday': ('tue_start', 'tue_end'), 'wednesday': ('wed_start', 'wed_end'), 'thursday': ('thu_start', 'thu_end'), 'friday': ('fri_start', 'fri_end'), 'saturday': ('sat_start', 'sat_end'), 'sunday': ('sun_start', 'sun_end'), # 中文 '星期一': ('mon_start', 'mon_end'), '星期二': ('tue_start', 'tue_end'), '星期三': ('wed_start', 'wed_end'), '星期四': ('thu_start', 'thu_end'), '星期五': ('fri_start', 'fri_end'), '星期六': ('sat_start', 'sat_end'), '星期日': ('sun_start', 'sun_end'), '星期天': ('sun_start', 'sun_end'), } key = weekday.lower().strip() if key not in day_map: return None, None start_attr, end_attr = day_map[key] start = getattr(self, start_attr, None) end = getattr(self, end_attr, None) # 休息標記 if start in ('-', '休息', None, ''): return None, None if end in ('-', '休息', None, ''): return None, None return start, end class AttendanceRecord(Base): """出勤記錄""" __tablename__ = "attendance_records" __table_args__ = ( UniqueConstraint('employee_name', 'date', name='uix_employee_date'), ) id = Column(Integer, primary_key=True, index=True) # 員工信息 employee_name = Column(String(255), nullable=False, index=True) company = Column(String(100), nullable=True) department = Column(String(100), nullable=True) # 日期 date = Column(Date, nullable=False, index=True) weekday = Column(String(20), nullable=True) # 打卡時間 check_in = Column(DateTime, nullable=True) check_out = Column(DateTime, nullable=True) # 班次 shift_code = Column(String(20), nullable=True) # 計算結果 status_code = Column(String(20), nullable=False, default='normal') status_text = Column(String(100), nullable=True) late_minutes = Column(Integer, default=0) early_minutes = Column(Integer, default=0) ot_minutes = Column(Integer, default=0) # Expected times expected_in = Column(String(10), nullable=True) expected_out = Column(String(10), nullable=True) actual_in = Column(String(10), nullable=True) actual_out = Column(String(10), nullable=True) # 人手修改標記 is_manually_edited = Column(Boolean, default=False) # 原始 Excel 數據 raw_data = Column(JSON, default={}) created_at = Column(DateTime, default=now_hkt) updated_at = Column(DateTime, default=now_hkt, onupdate=now_hkt) # Foreign key shift_id = Column(Integer, ForeignKey("shifts.id"), nullable=True) shift = relationship("Shift", foreign_keys=[shift_id]) class Holiday(Base): """公眾假期 / Public Holidays Source: HK Gov (https://www.gov.hk/en/about/abouthk/holiday/{year}.htm) Auto-imported by /api/holidays/refresh endpoint, manually editable. """ __tablename__ = "holidays" id = Column(Integer, primary_key=True, index=True) date = Column(Date, nullable=False, unique=True, index=True) name = Column(String(100), nullable=False) # 中文 e.g. "元旦" name_en = Column(String(200), nullable=True) # English e.g. "The first day of January" region = Column(String(20), nullable=True, default="HK") # "HK" / "CN" is_mandatory = Column(Boolean, default=True) # 勞工假 vs 公假 (Sunday replacement) source = Column(String(50), nullable=True) # "gov_hk_1823" / "manual" notes = Column(Text, nullable=True) # 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) class LeaveRecord(Base): """員工請假 / Employee Leave Records Per-employee, per-day, per-type leave. Hours-based granularity (0.5 ~ 8.0). HR uploads via CSV/Excel; manually editable. """ __tablename__ = "leave_records" id = Column(Integer, primary_key=True, index=True) employee_name = Column(String(255), nullable=False, index=True) employee_id = Column(String(100), nullable=True) # future, currently NULL date = Column(Date, nullable=False, index=True) leave_type = Column(String(20), nullable=False) # "SL" / "CL" / "AL" hours = Column(Numeric(4, 2), nullable=False) # 0.5 ~ 8.0 reason = Column(Text, nullable=True) approved_by = Column(String(100), nullable=True) source = Column(String(50), nullable=True) # "csv_upload" / "manual" notes = Column(Text, nullable=True) # 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_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)