All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 1m13s
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 15s
Workspace beantwortet die Leitfrage mit prominenten Next-Action- und Heute-Widgets sowie erweitertem Steuerungszustand pro Vorhaben. Co-authored-by: Cursor <cursoragent@cursor.com>
120 lines
4.1 KiB
Python
120 lines
4.1 KiB
Python
"""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]
|