from __future__ import annotations import csv import io from datetime import datetime, timezone from typing import Any, Callable from .db import Database Clock = Callable[[], datetime] def _required_text(value: Any, field_name: str) -> str: text = str(value or "").strip() if not text: raise ValueError(f"{field_name} is required") return text def _optional_text(value: Any) -> str: return str(value or "").strip() def _report_status(value: Any) -> str: status = str(value or "normal").strip() allowed = {"normal", "risk", "need_help"} if status not in allowed: raise ValueError("report_status is invalid") return status class ReportService: def __init__(self, database: Database, clock: Clock | None = None): self.database = database self.clock = clock or (lambda: datetime.now(timezone.utc)) def upsert_report(self, data: dict[str, Any]) -> dict[str, Any]: feishu_user_id = _required_text(data.get("feishu_user_id"), "feishu_user_id") report_date = _required_text(data.get("report_date"), "report_date") today_done = _required_text(data.get("today_done"), "today_done") tomorrow_plan = _required_text(data.get("tomorrow_plan"), "tomorrow_plan") employee = self.database.find_employee(feishu_user_id) if not employee or employee.get("active") != 1: raise ValueError("employee is not active") now = self.clock().isoformat() self.database.upsert_report( { "feishu_user_id": feishu_user_id, "employee_name": employee["name"], "report_date": report_date, "report_status": _report_status(data.get("report_status")), "today_done": today_done, "tomorrow_plan": tomorrow_plan, "blockers": _optional_text(data.get("blockers")), "help_needed": _optional_text(data.get("help_needed")), "submitted_at": now, "updated_at": now, } ) return next( report for report in self.list_reports_for_date(report_date)["reports"] if report["feishu_user_id"] == feishu_user_id ) def list_reports_for_date(self, report_date: str) -> dict[str, Any]: date = _required_text(report_date, "report_date") employees = self.database.list_active_employees() reports = self.database.list_reports_for_date(date) submitted_ids = {report["feishu_user_id"] for report in reports} missing = [employee for employee in employees if employee["feishu_user_id"] not in submitted_ids] return { "date": date, "expectedCount": len(employees), "submittedCount": len(reports), "missingCount": len(missing), "reports": reports, "missing": missing, } def export_reports_csv(self, report_date: str) -> str: result = self.list_reports_for_date(report_date) output = io.StringIO() fieldnames = [ "employee_name", "report_date", "today_done", "tomorrow_plan", "blockers", "help_needed", "report_status", "submitted_at", ] writer = csv.DictWriter(output, fieldnames=fieldnames, extrasaction="ignore") writer.writeheader() writer.writerows(result["reports"]) return output.getvalue() def list_employee_history(self, feishu_user_id: str, limit: int = 10) -> dict[str, Any]: user_id = _required_text(feishu_user_id, "feishu_user_id") employee = self.database.find_employee(user_id) if not employee or employee.get("active") != 1: raise ValueError("employee is not active") return { "employee": employee, "reports": self.database.list_reports_for_employee(user_id, limit), } def previous_report_reference(self, feishu_user_id: str, before_date: str) -> dict[str, Any]: user_id = _required_text(feishu_user_id, "feishu_user_id") date = _required_text(before_date, "report_date") employee = self.database.find_employee(user_id) if not employee or employee.get("active") != 1: raise ValueError("employee is not active") return { "employee": employee, "report": self.database.find_previous_report(user_id, date), } def is_admin(self, feishu_user_id: str) -> bool: user_id = _required_text(feishu_user_id, "feishu_user_id") employee = self.database.find_employee(user_id) return bool(employee and employee.get("active") == 1 and employee.get("role") == "admin")