216 lines
8.5 KiB
Python
216 lines
8.5 KiB
Python
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)
|
|
|
|
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)
|
|
|
|
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)
|