feat: 仪表盘+催办在休息日跳过填报检查

- 新增 is_working_day() 工具函数,自动跳过周末 + 可配置法定假日
- 仪表盘填报进度新增 is_rest_day 字段,休息日显示灰「休」印章
- 催办调度改用 is_working_day() 替代原始 weekday 检查
- 系统设置新增「法定节假日」配置卡片,支局长可管理假日日期

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-12 14:13:32 +08:00
parent dcb0b826de
commit a63bd6874c
6 changed files with 129 additions and 14 deletions
+15 -3
View File
@@ -65,7 +65,7 @@ async def update_config(
):
"""Update a single config key. Only directors can change settings."""
value = body.get("value", "")
valid_keys = {"notification_time"}
valid_keys = {"notification_time", "holidays"}
if key not in valid_keys:
raise HTTPException(status_code=400, detail=f"不支持的配置项: {key}")
@@ -75,11 +75,23 @@ async def update_config(
h, m = _parse_time(value)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
# Persist to DB
await _set_config(db, key, value)
# Reschedule the APScheduler job
await reschedule_daily_check(h, m)
return {"key": key, "value": value, "scheduled": f"{h:02d}:{m:02d}"}
if key == "holidays":
# Validate: comma-separated YYYY-MM-DD dates
if value.strip():
for s in value.split(","):
s = s.strip()
if s:
try:
from datetime import date
date.fromisoformat(s)
except ValueError:
raise HTTPException(status_code=400, detail=f"日期格式错误: {s},应为 YYYY-MM-DD")
await _set_config(db, key, value)
return {"key": key, "value": value}
await _set_config(db, key, value)
return {"key": key, "value": value}
+17 -1
View File
@@ -11,7 +11,8 @@ from app.models.mini_business import MiniBusiness
from app.models.key_visit import KeyVisit
from app.models.daily_note import DailyNote
from app.models.leave import Leave
from app.utils.timezone import today_cst
from app.models.system_config import SystemConfig
from app.utils.timezone import today_cst, is_working_day
def get_week_range(reference_date: date | None = None):
@@ -78,8 +79,22 @@ async def get_reporting_progress(db: AsyncSession, reference_date: date | None =
)
visit_map = {str(uid): cnt for uid, cnt in visits_result.all()}
# Load holiday config
holiday_row = await db.get(SystemConfig, "holidays")
holidays: set[date] = set()
if holiday_row and holiday_row.value:
for s in holiday_row.value.split(","):
s = s.strip()
if s:
try:
holidays.add(date.fromisoformat(s))
except ValueError:
pass
# Query leave records for today — build a set of managers on leave
today = today_cst()
is_rest = not is_working_day(today, holidays)
leaves_result = await db.execute(
select(Leave).where(and_(
Leave.start_date <= today,
@@ -107,6 +122,7 @@ async def get_reporting_progress(db: AsyncSession, reference_date: date | None =
"completed": count >= expected,
"has_reported_today": False, # Will be set below
"on_leave": leave is not None,
"is_rest_day": is_rest,
"leave_info": {
"leave_type": leave.leave_type,
"start_date": str(leave.start_date),
+17 -4
View File
@@ -7,16 +7,29 @@ from app.models.user import User
from app.models.leave import Leave
from app.models.work_plan import WorkPlan
from app.models.customer import Customer
from app.models.system_config import SystemConfig
from app.services.wecom import wecom_client
from app.utils.timezone import today_cst
from app.utils.timezone import today_cst, is_working_day
async def check_daily_reporting(db: AsyncSession) -> dict:
"""Check today's reporting progress and send wecom reminders to managers who haven't reported."""
today = today_cst()
weekday = today.weekday()
if weekday >= 5: # Skip weekends
return {"status": "weekend", "date": str(today)}
# Load holiday config
holiday_row = await db.get(SystemConfig, "holidays")
holidays: set[date] = set()
if holiday_row and holiday_row.value:
for s in holiday_row.value.split(","):
s = s.strip()
if s:
try:
holidays.add(date.fromisoformat(s))
except ValueError:
pass
if not is_working_day(today, holidays):
return {"status": "rest_day", "date": str(today)}
# Get all users who need to report
result = await db.execute(
+18
View File
@@ -11,3 +11,21 @@ def today_cst() -> date:
def parse_date(s: str) -> date:
"""Parse a date string that may be YYYY-MM-DD or a full ISO datetime."""
return date.fromisoformat(s[:10])
def is_working_day(d: date | None = None, holidays: set[date] | None = None) -> bool:
"""Check if a date is a Chinese working day.
Returns False for:
- Saturdays and Sundays (weekday >= 5)
- Dates in the holidays set (configurable via system_config)
Note: makeup workdays (调休, where Saturday/Sunday becomes a workday)
are not currently handled. If needed, add a 'workdays' config set.
"""
d = d or today_cst()
if d.weekday() >= 5: # Saturday or Sunday
return False
if holidays and d in holidays:
return False
return True