"""Action read-models for workspace — tenant-scoped, actor-assignment based.""" from __future__ import annotations from typing import Any from psycopg2.extras import RealDictCursor from db import get_connection from services.actions import OPEN_ACTION_STATUSES, _attach_assignments, _serialize_row from tenant_context import TenantContext def _require_actor(ctx: TenantContext) -> str: if not ctx.actor_id: raise ValueError("Kein Actor im TenantContext") return ctx.actor_id def _list_assigned_actions( *, tenant_id: str, actor_id: str, statuses: list[str], order_today: bool = False, ) -> list[dict[str, Any]]: conn = get_connection() order_clause = """ ORDER BY CASE WHEN a.due_at IS NOT NULL AND a.due_at < NOW() THEN 0 ELSE 1 END, CASE a.status WHEN 'blocked' THEN 0 WHEN 'review_required' THEN 1 ELSE 2 END, CASE a.priority WHEN 'high' THEN 0 WHEN 'normal' THEN 1 WHEN 'low' THEN 2 END, a.due_at ASC NULLS LAST, a.updated_at DESC """ if order_today else """ ORDER BY CASE a.priority WHEN 'high' THEN 0 WHEN 'normal' THEN 1 WHEN 'low' THEN 2 END, a.updated_at DESC """ try: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute( f""" SELECT a.id, a.tenant_id, a.initiative_id, a.title, a.description, a.status, a.priority, a.due_at, a.created_at, a.updated_at, i.title AS initiative_title, (a.due_at IS NOT NULL AND a.due_at < NOW()) AS is_overdue FROM actions a JOIN action_assignments aa ON aa.action_id = a.id AND aa.tenant_id = a.tenant_id JOIN initiatives i ON i.id = a.initiative_id AND i.tenant_id = a.tenant_id WHERE a.tenant_id = %s AND aa.actor_id = %s AND a.status = ANY(%s) {order_clause} """, (tenant_id, actor_id, statuses), ) actions = [_serialize_row(dict(row)) for row in cur.fetchall()] for action in actions: action["is_overdue"] = bool(action.pop("is_overdue", False)) finally: conn.close() return _attach_assignments(actions, tenant_id=tenant_id) def get_my_open_actions(ctx: TenantContext) -> list[dict[str, Any]]: """Open + in_progress actions assigned to current actor (excludes blocked).""" actor_id = _require_actor(ctx) return _list_assigned_actions( tenant_id=ctx.tenant_id, actor_id=actor_id, statuses=["open", "in_progress"], ) def get_my_blocked_actions(ctx: TenantContext) -> list[dict[str, Any]]: """Blocked actions assigned to current actor.""" actor_id = _require_actor(ctx) return _list_assigned_actions( tenant_id=ctx.tenant_id, actor_id=actor_id, statuses=["blocked"], ) def get_all_my_open_actions(ctx: TenantContext) -> list[dict[str, Any]]: """All non-terminal open actions (open, in_progress, blocked) — legacy compat.""" actor_id = _require_actor(ctx) return _list_assigned_actions( tenant_id=ctx.tenant_id, actor_id=actor_id, statuses=list(OPEN_ACTION_STATUSES), ) def get_my_today_actions(ctx: TenantContext, *, limit: int = 15) -> list[dict[str, Any]]: """Maßnahmen für heute — überfällig zuerst, dann blockiert/review, dann Priorität.""" if limit < 1: limit = 1 if limit > 50: limit = 50 actor_id = _require_actor(ctx) actions = _list_assigned_actions( tenant_id=ctx.tenant_id, actor_id=actor_id, statuses=["open", "ready", "in_progress", "blocked", "review_required"], order_today=True, ) return actions[:limit]