Files
2026-07-01 17:56:31 +08:00

99 lines
3.4 KiB
Python

"""Read-only organisation viewer for the CLI board."""
from __future__ import annotations
from rich.console import Group, RenderableType
from rich.panel import Panel
from rich.text import Text
from textual.widgets import Static
from ..state.models import OrgEmployeeView, OrgRoleView, OrgSnapshotView
from ..state.store import BoardStateStore
from .render_utils import truncate_text
class OrgViewerWidget(Static):
"""Render a read-only org structure: role tree, employees, and work-item projection."""
def __init__(self, state: BoardStateStore) -> None:
super().__init__(id="org-viewer")
self.state = state
self.org: OrgSnapshotView | None = None
def set_org(self, org: OrgSnapshotView | None) -> None:
self.org = org
self.refresh()
def render(self) -> RenderableType:
focused = self.state.pane_focus == "main" and self.state.view_mode == "org"
if self.org is None:
return Panel(
Text("No org data. Press 5 to load organisation view.", style="dim"),
title="Organisation [Focused]" if focused else "Organisation",
border_style="cyan" if focused else "white",
)
parts: list[RenderableType] = []
parts.append(self._render_roles())
parts.append(self._render_employees())
title = "Organisation [Focused]" if focused else "Organisation"
return Panel(Group(*parts), title=title, border_style="cyan" if focused else "white")
# ── Roles ──
def _render_roles(self) -> Text:
org = self.org
text = Text()
text.append(f"Roles ({org.role_count})\n", style="bold #cbd5e1")
if not org.role_tree:
text.append(" No roles configured.\n", style="dim")
return text
for node in org.role_tree:
self._render_role_node(text, node, depth=0)
return text
def _render_role_node(self, text: Text, role: OrgRoleView, depth: int) -> None:
indent = " " * (depth + 1)
connector = ""
if depth > 0:
connector = "\u251c\u2500 " if True else "\u2514\u2500 " # ├─
text.append(f"{indent}{connector}", style="dim")
text.append(role.role_id, style="bold #38bdf8")
resp = truncate_text(role.responsibility, 30)
if resp:
text.append(f" \"{resp}\"", style="dim")
text.append(f" {role.employee_count} emp", style="dim italic")
text.append("\n")
for child in role.children:
self._render_role_node(text, child, depth=depth + 1)
# ── Employees ──
def _render_employees(self) -> Text:
org = self.org
text = Text("\n")
text.append(f"Employees ({org.employee_count})\n", style="bold #cbd5e1")
if not org.employees:
text.append(" No employees registered.\n", style="dim")
return text
for emp in org.employees[:20]:
text.append(f" {truncate_text(emp.name, 14):<14s}", style="bold white")
text.append(f" {truncate_text(emp.role_id, 14):<14s}", style="#38bdf8")
text.append(f" {emp.seniority:<8s}", style="dim")
if emp.domains:
text.append(f" [{', '.join(emp.domains[:4])}]", style="#a78bfa")
text.append("\n")
if len(org.employees) > 20:
text.append(f" \u2026 +{len(org.employees) - 20} more\n", style="dim")
return text