Puente Power Automate, servidor local del tablero HTML v7 y exportacion a Excel/TSV para monitoreo de proyectos en Microsoft Planner. Co-authored-by: Cursor <cursoragent@cursor.com>
187 lines
7.1 KiB
Python
187 lines
7.1 KiB
Python
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
from auth.graph_client import GraphClient, create_graph_client
|
|
from config import AUTH_MODE
|
|
|
|
|
|
def _parse_date(value: str | None) -> datetime | None:
|
|
if not value:
|
|
return None
|
|
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
|
|
|
|
def _task_status(percent: int) -> str:
|
|
if percent == 100:
|
|
return "Completada"
|
|
if percent > 0:
|
|
return "En progreso"
|
|
return "Por hacer"
|
|
|
|
|
|
def _priority_label(priority: int) -> str:
|
|
labels = {0: "Urgente", 1: "Importante", 3: "Media", 5: "Baja", 9: "Sin prioridad"}
|
|
return labels.get(priority, "Sin prioridad")
|
|
|
|
|
|
class PlannerService:
|
|
"""Extrae y normaliza datos de Microsoft Planner."""
|
|
|
|
def __init__(self, client: GraphClient | None = None) -> None:
|
|
self.client = client or create_graph_client()
|
|
self._user_cache: dict[str, str] = {}
|
|
self._team_cache: dict[str, str] = {}
|
|
|
|
def _resolve_user(self, user_id: str) -> str:
|
|
if user_id in self._user_cache:
|
|
return self._user_cache[user_id]
|
|
|
|
try:
|
|
user = self.client.get(f"/users/{user_id}")
|
|
name = user.get("displayName") or user.get("mail") or user_id
|
|
except Exception:
|
|
name = user_id
|
|
|
|
self._user_cache[user_id] = name
|
|
return name
|
|
|
|
def get_my_plans(self) -> list[dict]:
|
|
"""Planes a los que el usuario tiene acceso (modo delegado)."""
|
|
return self.client.get_all_pages("/me/planner/plans")
|
|
|
|
def get_my_teams(self) -> dict[str, str]:
|
|
"""Mapea ID de equipo -> nombre (modo delegado, sin admin)."""
|
|
if self._team_cache:
|
|
return self._team_cache
|
|
|
|
teams = self.client.get_all_pages("/me/joinedTeams")
|
|
self._team_cache = {t["id"]: t.get("displayName", t["id"]) for t in teams}
|
|
return self._team_cache
|
|
|
|
def get_groups_with_planner(self) -> list[dict]:
|
|
"""Obtiene grupos con Planner. En modo delegado usa los equipos del usuario."""
|
|
if AUTH_MODE == "delegated":
|
|
teams = self.get_my_teams()
|
|
return [{"id": tid, "displayName": name} for tid, name in teams.items()]
|
|
|
|
return self.client.get_all_pages(
|
|
"/groups",
|
|
params={
|
|
"$filter": "resourceProvisioningOptions/Any(x:x eq 'Team')",
|
|
"$select": "id,displayName,description",
|
|
},
|
|
)
|
|
|
|
def get_plans_for_group(self, group_id: str) -> list[dict]:
|
|
return self.client.get_all_pages(f"/groups/{group_id}/planner/plans")
|
|
|
|
def get_buckets_for_plan(self, plan_id: str) -> dict[str, str]:
|
|
buckets = self.client.get_all_pages(f"/planner/plans/{plan_id}/buckets")
|
|
return {b["id"]: b.get("name", "Sin bucket") for b in buckets}
|
|
|
|
def get_tasks_for_plan(self, plan_id: str) -> list[dict]:
|
|
return self.client.get_all_pages(f"/planner/plans/{plan_id}/tasks")
|
|
|
|
def get_all_projects(self, group_ids: list[str] | None = None) -> list[dict[str, Any]]:
|
|
"""
|
|
Extrae todos los planes y sus tareas.
|
|
|
|
Modo delegado: solo planes a los que el usuario tiene acceso.
|
|
Modo aplicacion: todos los grupos de la organizacion.
|
|
"""
|
|
projects: list[dict[str, Any]] = []
|
|
teams = self.get_my_teams() if AUTH_MODE == "delegated" else {}
|
|
|
|
if AUTH_MODE == "delegated" and not group_ids:
|
|
plans = self.get_my_plans()
|
|
plan_groups = [{
|
|
"plan": plan,
|
|
"group_id": plan.get("owner", ""),
|
|
"group_name": teams.get(plan.get("owner", ""), "Mi equipo"),
|
|
} for plan in plans]
|
|
else:
|
|
if group_ids:
|
|
groups = [{"id": gid, "displayName": gid} for gid in group_ids]
|
|
else:
|
|
groups = self.get_groups_with_planner()
|
|
|
|
plan_groups = []
|
|
for group in groups:
|
|
group_id = group["id"]
|
|
group_name = group.get("displayName", group_id)
|
|
try:
|
|
plans = self.get_plans_for_group(group_id)
|
|
except Exception:
|
|
continue
|
|
for plan in plans:
|
|
plan_groups.append({
|
|
"plan": plan,
|
|
"group_id": group_id,
|
|
"group_name": group_name,
|
|
})
|
|
|
|
for item in plan_groups:
|
|
plan = item["plan"]
|
|
group_name = item["group_name"]
|
|
plan_id = plan["id"]
|
|
plan_title = plan.get("title", "Sin título")
|
|
buckets = self.get_buckets_for_plan(plan_id)
|
|
|
|
try:
|
|
tasks = self.get_tasks_for_plan(plan_id)
|
|
except Exception:
|
|
tasks = []
|
|
|
|
normalized_tasks = []
|
|
for task in tasks:
|
|
percent = task.get("percentComplete", 0)
|
|
due = _parse_date(task.get("dueDateTime"))
|
|
now = datetime.now(timezone.utc)
|
|
|
|
assignees = []
|
|
for user_id in task.get("assignments", {}):
|
|
assignees.append(self._resolve_user(user_id))
|
|
|
|
normalized_tasks.append({
|
|
"grupo": group_name,
|
|
"proyecto": plan_title,
|
|
"plan_id": plan_id,
|
|
"tarea": task.get("title", "Sin título"),
|
|
"tarea_id": task["id"],
|
|
"bucket": buckets.get(task.get("bucketId", ""), "Sin bucket"),
|
|
"estado": _task_status(percent),
|
|
"porcentaje": percent,
|
|
"prioridad": _priority_label(task.get("priority", 9)),
|
|
"fecha_inicio": task.get("startDateTime", ""),
|
|
"fecha_vencimiento": task.get("dueDateTime", ""),
|
|
"asignados": ", ".join(assignees) if assignees else "Sin asignar",
|
|
"vencida": bool(due and due < now and percent < 100),
|
|
"creada": task.get("createdDateTime", ""),
|
|
"actualizada": task.get("lastModifiedDateTime", ""),
|
|
})
|
|
|
|
completed = sum(1 for t in normalized_tasks if t["porcentaje"] == 100)
|
|
overdue = sum(1 for t in normalized_tasks if t["vencida"])
|
|
total = len(normalized_tasks)
|
|
|
|
projects.append({
|
|
"grupo": group_name,
|
|
"proyecto": plan_title,
|
|
"plan_id": plan_id,
|
|
"total_tareas": total,
|
|
"completadas": completed,
|
|
"en_progreso": sum(1 for t in normalized_tasks if 0 < t["porcentaje"] < 100),
|
|
"por_hacer": sum(1 for t in normalized_tasks if t["porcentaje"] == 0),
|
|
"vencidas": overdue,
|
|
"porcentaje_avance": round((completed / total) * 100, 1) if total else 0,
|
|
"tareas": normalized_tasks,
|
|
})
|
|
|
|
return projects
|
|
|
|
def get_flat_tasks(self, projects: list[dict]) -> list[dict]:
|
|
"""Aplana todas las tareas de todos los proyectos en una sola lista."""
|
|
tasks = []
|
|
for project in projects:
|
|
tasks.extend(project["tareas"])
|
|
return tasks
|