from __future__ import annotations import json import sqlite3 import threading from pathlib import Path from typing import Any class Database: def __init__(self, database_path: Path, employee_seed_path: Path): database_path.parent.mkdir(parents=True, exist_ok=True) self.connection = sqlite3.connect(database_path, check_same_thread=False) self.connection.row_factory = sqlite3.Row self._lock = threading.RLock() self._migrate() self.load_employees(employee_seed_path) def close(self) -> None: with self._lock: self.connection.close() def _migrate(self) -> None: with self._lock: self.connection.executescript( """ CREATE TABLE IF NOT EXISTS employees ( id INTEGER PRIMARY KEY AUTOINCREMENT, feishu_user_id TEXT NOT NULL UNIQUE, name TEXT NOT NULL, department TEXT NOT NULL DEFAULT '', manager TEXT NOT NULL DEFAULT '', active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS daily_reports ( id INTEGER PRIMARY KEY AUTOINCREMENT, feishu_user_id TEXT NOT NULL, employee_name TEXT NOT NULL, report_date TEXT NOT NULL, today_done TEXT NOT NULL, tomorrow_plan TEXT NOT NULL, blockers TEXT NOT NULL DEFAULT '', help_needed TEXT NOT NULL DEFAULT '', submitted_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(feishu_user_id, report_date) ); """ ) self.connection.commit() def load_employees(self, employee_seed_path: Path) -> None: if not employee_seed_path.exists(): return employees = json.loads(employee_seed_path.read_text(encoding="utf-8")) self.upsert_employees(employees) def upsert_employees(self, employees: list[dict[str, Any]]) -> None: with self._lock, self.connection: for employee in employees: self.connection.execute( """ INSERT INTO employees (feishu_user_id, name, department, manager, active, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ON CONFLICT(feishu_user_id) DO UPDATE SET name = excluded.name, department = excluded.department, manager = excluded.manager, active = excluded.active, updated_at = CURRENT_TIMESTAMP """, ( employee["feishu_user_id"], employee["name"], employee.get("department", ""), employee.get("manager", ""), 0 if employee.get("active") is False else 1, ), ) def upsert_employee(self, employee: dict[str, Any]) -> None: self.upsert_employees([employee]) def list_active_employees(self) -> list[dict[str, Any]]: with self._lock: rows = self.connection.execute( """ SELECT feishu_user_id, name, department, manager, active FROM employees WHERE active = 1 ORDER BY department, name """ ).fetchall() return [dict(row) for row in rows] def find_employee(self, feishu_user_id: str) -> dict[str, Any] | None: with self._lock: row = self.connection.execute( """ SELECT feishu_user_id, name, department, manager, active FROM employees WHERE feishu_user_id = ? """, (feishu_user_id,), ).fetchone() return dict(row) if row else None def upsert_report(self, report: dict[str, Any]) -> None: with self._lock, self.connection: self.connection.execute( """ INSERT INTO daily_reports ( feishu_user_id, employee_name, report_date, today_done, tomorrow_plan, blockers, help_needed, submitted_at, updated_at ) VALUES ( :feishu_user_id, :employee_name, :report_date, :today_done, :tomorrow_plan, :blockers, :help_needed, :submitted_at, :updated_at ) ON CONFLICT(feishu_user_id, report_date) DO UPDATE SET employee_name = excluded.employee_name, today_done = excluded.today_done, tomorrow_plan = excluded.tomorrow_plan, blockers = excluded.blockers, help_needed = excluded.help_needed, updated_at = excluded.updated_at """, report, ) def list_reports_for_date(self, report_date: str) -> list[dict[str, Any]]: with self._lock: rows = self.connection.execute( """ SELECT id, feishu_user_id, employee_name, report_date, today_done, tomorrow_plan, blockers, help_needed, submitted_at, updated_at FROM daily_reports WHERE report_date = ? ORDER BY employee_name """, (report_date,), ).fetchall() return [dict(row) for row in rows] def list_reports_for_employee(self, feishu_user_id: str, limit: int = 10) -> list[dict[str, Any]]: with self._lock: rows = self.connection.execute( """ SELECT id, feishu_user_id, employee_name, report_date, today_done, tomorrow_plan, blockers, help_needed, submitted_at, updated_at FROM daily_reports WHERE feishu_user_id = ? ORDER BY report_date DESC, updated_at DESC LIMIT ? """, (feishu_user_id, limit), ).fetchall() return [dict(row) for row in rows]