diff --git a/backend/app/api/system_config.py b/backend/app/api/system_config.py index 5894f31..860965d 100644 --- a/backend/app/api/system_config.py +++ b/backend/app/api/system_config.py @@ -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} diff --git a/backend/app/services/dashboard.py b/backend/app/services/dashboard.py index e072e5a..8c7feb0 100644 --- a/backend/app/services/dashboard.py +++ b/backend/app/services/dashboard.py @@ -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), diff --git a/backend/app/services/scheduler.py b/backend/app/services/scheduler.py index 5a40788..7c202dd 100644 --- a/backend/app/services/scheduler.py +++ b/backend/app/services/scheduler.py @@ -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( diff --git a/backend/app/utils/timezone.py b/backend/app/utils/timezone.py index 3ae4de2..7afc14a 100644 --- a/backend/app/utils/timezone.py +++ b/backend/app/utils/timezone.py @@ -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 diff --git a/frontend/src/views/desktop/Dashboard.vue b/frontend/src/views/desktop/Dashboard.vue index fcdee82..a8716c4 100644 --- a/frontend/src/views/desktop/Dashboard.vue +++ b/frontend/src/views/desktop/Dashboard.vue @@ -76,7 +76,8 @@ async function handleExport() { } catch (e: any) { ElMessage.error('导出失败') } } -function rowState(p: any): 'full' | 'catching' | 'missing' | 'on_leave' { +function rowState(p: any): 'full' | 'catching' | 'missing' | 'on_leave' | 'rest_day' { + if (p.is_rest_day) return 'rest_day' if (p.on_leave) return 'on_leave' if (p.has_reported_today && p.completed) return 'full' if (p.has_reported_today && !p.completed) return 'catching' @@ -206,26 +207,27 @@ function rowState(p: any): 'full' | 'catching' | 'missing' | 'on_leave' { >
配置后,仪表盘和催办提醒在这些日期将跳过填报检查(周末自动跳过无需配置)
+