Initial commit: AARS backend + frontend + holiday/leave phase 1+2
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user