feat(time-tracking): 添加个人工时记录系统设计文档
- 完成系统架构和数据模型设计,包括项目、工时记录、休息日和周期表模型 - 设计项目管理模块,支持传统项目与PSI项目管理及批量导入功能 - 规划工时记录模块,含日期、事件描述、项目选择及工时计算规则 - 定义休息日分类,支持周末、国定节假日、个人假期及调休工时管理 - 制定统计分析模块设计,支持按Cut-Off周期的周统计与项目工时分布 - 设计周期管理模块,提供周期设置及预设模板功能 - 制定用户界面布局及各页面表单、样式设计方案 - 规划RESTful API端点,涵盖项目、工时记录、休息日、周期及统计数据操作 - 设计数据流示意,阐明操作流程及前后端交互逻辑 - 制定数据存储方案,包括SQLite数据库配置及备份导出机制
This commit is contained in:
1
backend/models/__init__.py
Normal file
1
backend/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# 模型包初始化文件
|
||||
137
backend/models/models.py
Normal file
137
backend/models/models.py
Normal file
@@ -0,0 +1,137 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text, Date, Time, ForeignKey, Enum
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
import enum
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
class ProjectType(enum.Enum):
|
||||
TRADITIONAL = "traditional" # 传统项目
|
||||
PSI = "psi" # PSI项目
|
||||
|
||||
class Project(Base):
|
||||
"""项目表模型"""
|
||||
__tablename__ = 'projects'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
project_name = Column(String(200), nullable=False)
|
||||
project_type = Column(Enum(ProjectType), nullable=False)
|
||||
|
||||
# 通用字段
|
||||
project_code = Column(String(50), nullable=False)
|
||||
customer_name = Column(String(200), nullable=False)
|
||||
|
||||
# PSI项目特有字段
|
||||
contract_number = Column(String(100)) # 合同号,PSI项目必填
|
||||
|
||||
description = Column(Text)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# 关联关系
|
||||
time_records = relationship("TimeRecord", back_populates="project")
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'project_name': self.project_name,
|
||||
'project_type': self.project_type.value if self.project_type else None,
|
||||
'project_code': self.project_code,
|
||||
'customer_name': self.customer_name,
|
||||
'contract_number': self.contract_number,
|
||||
'description': self.description,
|
||||
'is_active': self.is_active,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
|
||||
class TimeRecord(Base):
|
||||
"""工时记录表模型"""
|
||||
__tablename__ = 'time_records'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
date = Column(Date, nullable=False)
|
||||
event_description = Column(String(500)) # 事件描述
|
||||
project_id = Column(Integer, ForeignKey('projects.id'))
|
||||
start_time = Column(Time) # 可为空,支持"-"占位符
|
||||
end_time = Column(Time) # 可为空,支持"-"占位符
|
||||
activity_num = Column(String(100)) # Activity Num
|
||||
hours = Column(String(20)) # 工时,支持"2:42"或"8:00:00"格式
|
||||
is_holiday = Column(Boolean, default=False) # 是否为休息日
|
||||
is_working_on_holiday = Column(Boolean, default=False) # 休息日是否工作
|
||||
holiday_type = Column(String(50)) # 休息日类型
|
||||
week_info = Column(String(50)) # 周信息
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# 关联关系
|
||||
project = relationship("Project", back_populates="time_records")
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'date': self.date.isoformat() if self.date else None,
|
||||
'event_description': self.event_description,
|
||||
'project_id': self.project_id,
|
||||
'start_time': self.start_time.strftime('%H:%M') if self.start_time else None,
|
||||
'end_time': self.end_time.strftime('%H:%M') if self.end_time else None,
|
||||
'activity_num': self.activity_num,
|
||||
'hours': self.hours,
|
||||
'is_holiday': self.is_holiday,
|
||||
'is_working_on_holiday': self.is_working_on_holiday,
|
||||
'holiday_type': self.holiday_type,
|
||||
'week_info': self.week_info,
|
||||
'project': self.project.to_dict() if self.project else None,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
|
||||
class Holiday(Base):
|
||||
"""休息日配置表模型"""
|
||||
__tablename__ = 'holidays'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
date = Column(Date, nullable=False, unique=True)
|
||||
holiday_name = Column(String(100)) # 节假日名称
|
||||
holiday_type = Column(String(50), nullable=False) # weekend/national_holiday/personal_leave/makeup_day
|
||||
is_working_day = Column(Boolean, default=False) # 调休工作日标记
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'date': self.date.isoformat() if self.date else None,
|
||||
'holiday_name': self.holiday_name,
|
||||
'holiday_type': self.holiday_type,
|
||||
'is_working_day': self.is_working_day,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
|
||||
class CutoffPeriod(Base):
|
||||
"""Cut-Off周期表模型"""
|
||||
__tablename__ = 'cutoff_periods'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
period_name = Column(String(50), nullable=False)
|
||||
start_date = Column(Date, nullable=False)
|
||||
end_date = Column(Date, nullable=False)
|
||||
target_hours = Column(Integer, default=160)
|
||||
weeks = Column(Integer, default=4)
|
||||
year = Column(Integer, nullable=False)
|
||||
month = Column(Integer, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'period_name': self.period_name,
|
||||
'start_date': self.start_date.isoformat() if self.start_date else None,
|
||||
'end_date': self.end_date.isoformat() if self.end_date else None,
|
||||
'target_hours': self.target_hours,
|
||||
'weeks': self.weeks,
|
||||
'year': self.year,
|
||||
'month': self.month,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
113
backend/models/utils.py
Normal file
113
backend/models/utils.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import datetime
|
||||
from typing import Optional, Dict, Any, List
|
||||
from backend.models.models import Holiday
|
||||
|
||||
def is_weekend(date: datetime.date) -> bool:
|
||||
"""判断是否为周末"""
|
||||
return date.weekday() >= 5 # 周六=5, 周日=6
|
||||
|
||||
def is_holiday(date: datetime.date, holidays: List[Holiday] = None) -> Dict[str, Any]:
|
||||
"""检测指定日期是否为休息日"""
|
||||
day_of_week = date.weekday() # 0=周一, 6=周日
|
||||
is_weekend_day = day_of_week >= 5 # 周六、周日
|
||||
|
||||
# 检查是否为配置的节假日
|
||||
configured_holiday = None
|
||||
if holidays:
|
||||
for holiday in holidays:
|
||||
if holiday.date == date:
|
||||
configured_holiday = holiday
|
||||
break
|
||||
|
||||
is_configured_holiday = configured_holiday is not None
|
||||
|
||||
return {
|
||||
'is_holiday': is_weekend_day or is_configured_holiday,
|
||||
'holiday_type': 'weekend' if is_weekend_day else (configured_holiday.holiday_type if configured_holiday else None),
|
||||
'holiday_name': configured_holiday.holiday_name if configured_holiday else None,
|
||||
'is_working_day': configured_holiday.is_working_day if configured_holiday else False
|
||||
}
|
||||
|
||||
def calculate_hours(start_time: Optional[str], end_time: Optional[str], is_holiday_flag: bool = False) -> str:
|
||||
"""工时计算函数"""
|
||||
if not start_time or not end_time or start_time == '-' or end_time == '-':
|
||||
return '0:00' if is_holiday_flag else '-' # 休息日默认显示0:00而不是-
|
||||
|
||||
try:
|
||||
start = datetime.datetime.strptime(start_time, '%H:%M')
|
||||
end = datetime.datetime.strptime(end_time, '%H:%M')
|
||||
|
||||
# 处理跨日情况
|
||||
if end < start:
|
||||
end += datetime.timedelta(days=1)
|
||||
|
||||
diff = end - start
|
||||
total_minutes = int(diff.total_seconds() / 60)
|
||||
|
||||
hours = total_minutes // 60
|
||||
minutes = total_minutes % 60
|
||||
|
||||
return f"{hours}:{minutes:02d}"
|
||||
except ValueError:
|
||||
return '0:00'
|
||||
|
||||
def format_hours_to_decimal(hours: str) -> float:
|
||||
"""工时格式转换函数:将"2:42"格式转换为小数格式用于计算"""
|
||||
if hours == '-' or not hours:
|
||||
return 0.0
|
||||
|
||||
if ':' in hours:
|
||||
try:
|
||||
parts = hours.split(':')
|
||||
h = int(parts[0])
|
||||
m = int(parts[1]) if len(parts) > 1 else 0
|
||||
return h + (m / 60)
|
||||
except ValueError:
|
||||
return 0.0
|
||||
|
||||
try:
|
||||
return float(hours)
|
||||
except ValueError:
|
||||
return 0.0
|
||||
|
||||
def format_decimal_to_hours(decimal_hours: float) -> str:
|
||||
"""将小数工时转换回"HH:MM"格式"""
|
||||
hours = int(decimal_hours)
|
||||
minutes = int((decimal_hours - hours) * 60)
|
||||
return f"{hours}:{minutes:02d}"
|
||||
|
||||
def calculate_weekly_hours(records: list) -> Dict[str, float]:
|
||||
"""工时统计函数(区分工作日和休息日)"""
|
||||
workday_hours = sum([
|
||||
format_hours_to_decimal(r.get('hours', '0:00'))
|
||||
for r in records
|
||||
if not r.get('is_holiday', False) and r.get('hours') not in ['-', '0:00', None]
|
||||
])
|
||||
|
||||
holiday_hours = sum([
|
||||
format_hours_to_decimal(r.get('hours', '0:00'))
|
||||
for r in records
|
||||
if r.get('is_holiday', False) and r.get('hours') not in ['-', '0:00', None]
|
||||
])
|
||||
|
||||
return {
|
||||
'workday_hours': workday_hours,
|
||||
'holiday_hours': holiday_hours,
|
||||
'total_hours': workday_hours + holiday_hours
|
||||
}
|
||||
|
||||
def get_week_info(date: datetime.date) -> str:
|
||||
"""获取周信息,如"51周/53周" """
|
||||
year = date.year
|
||||
week_num = date.isocalendar()[1]
|
||||
|
||||
# 计算该年总共有多少周
|
||||
last_day = datetime.date(year, 12, 31)
|
||||
total_weeks = last_day.isocalendar()[1]
|
||||
|
||||
return f"{week_num}周/{total_weeks}周"
|
||||
|
||||
def get_day_of_week_chinese(date: datetime.date) -> str:
|
||||
"""获取中文星期"""
|
||||
weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
|
||||
return weekdays[date.weekday()]
|
||||
Reference in New Issue
Block a user