"""Colonnes de board personnalisables par espace (0.30.0). Avant : six colonnes codées en dur (backlog, todo, in_progress, qa, review, done) et un mapping fixe colonne → étiquette ``kanban:``. Maintenant, chaque espace de travail définit ses colonnes : nom d'affichage, couleur, ordre, étiquette de forge mappée et alias reconnus. Les clés sont immuables (les données board_state les référencent) ; backlog et done restent des colonnes système (renommables, non supprimables) parce qu'elles portent une sémantique : défaut des cartes neuves, et état fermé des issues. Sans définition propre, un espace retombe sur les défauts historiques — comportement 0.24 strictement conservé. """ from __future__ import annotations from app import models DEFAULT_COLUMNS: list[dict] = [ {"key": "backlog", "name": "📥 Backlog", "color": "#6b7280", "label": "", "aliases": "", "kind": "backlog", "in_focus": 0}, {"key": "todo", "name": "📋 À faire", "color": "#3b82f6", "label": "kanban:todo", "aliases": "", "kind": "flow", "in_focus": 0}, {"key": "in_progress", "name": "🚧 En cours", "color": "#f59e0b", "label": "kanban:in_progress", "aliases": "", "kind": "flow", "in_focus": 1}, {"key": "qa", "name": "🧪 QA", "color": "#06b6d4", "label": "kanban:qa", "aliases": "", "kind": "flow", "in_focus": 1}, {"key": "review", "name": "🔍 Révision", "color": "#8b5cf6", "label": "kanban:review", "aliases": "", "kind": "flow", "in_focus": 1}, {"key": "done", "name": "✅ Terminé", "color": "#10b981", "label": "", "aliases": "", "kind": "done", "in_focus": 0}, ] _cache: dict[int, list[dict]] = {} def invalidate(workspace_id: int | None = None) -> None: if workspace_id is None: _cache.clear() else: _cache.pop(workspace_id, None) def ensure_columns(workspace_id: int) -> list[dict]: """Seed the historical defaults if the workspace has none.""" existing = models.list_board_columns(workspace_id) if existing: return existing for position, column in enumerate(DEFAULT_COLUMNS): models.upsert_board_column( workspace_id, {**column, "position": position} ) invalidate(workspace_id) return models.list_board_columns(workspace_id) def get_columns(workspace_id: int = 1) -> list[dict]: """Effective columns of a workspace (cached, seeded lazily).""" if workspace_id not in _cache: _cache[workspace_id] = ensure_columns(workspace_id) return _cache[workspace_id] def keys(workspace_id: int = 1) -> list[str]: return [c["key"] for c in get_columns(workspace_id)] def names(workspace_id: int = 1) -> dict[str, str]: return {c["key"]: c["name"] for c in get_columns(workspace_id)} def colors(workspace_id: int = 1) -> dict[str, str]: return {c["key"]: c["color"] for c in get_columns(workspace_id)} def flow_keys(workspace_id: int = 1) -> list[str]: return [ c["key"] for c in get_columns(workspace_id) if c["kind"] == "flow" ] def focus_keys(workspace_id: int = 1) -> list[str]: return [ c["key"] for c in get_columns(workspace_id) if c.get("in_focus") ] def wall_keys(workspace_id: int = 1) -> list[str]: """Wallboard cards: focus columns + the first flow column.""" columns = get_columns(workspace_id) wanted = {c["key"] for c in columns if c.get("in_focus")} flow = [c["key"] for c in columns if c["kind"] == "flow"] if flow: wanted.add(flow[0]) return [c["key"] for c in columns if c["key"] in wanted] def label_for(workspace_id: int, key: str) -> str | None: """The forge label mirrored when a card enters this column.""" for column in get_columns(workspace_id): if column["key"] == key and column["kind"] == "flow": return column.get("label") or None return None def label_lookup(workspace_id: int = 1) -> dict[str, str]: """label name (lowercased) → column key, aliases included. This is the configurable label/column mapping: the canonical label of each flow column plus every alias declared for it (e.g. Flowdeck's ``kanban_a_faire`` on the todo column). """ lookup: dict[str, str] = {} for column in get_columns(workspace_id): if column["kind"] != "flow": continue names = [column.get("label") or ""] names += [ a.strip() for a in (column.get("aliases") or "").split(",") ] for name in names: name = name.strip().lower() if name: lookup[name] = column["key"] return lookup def recognized_labels(workspace_id: int = 1) -> set[str]: """Every label name (lowercased) owned by the mapping.""" return set(label_lookup(workspace_id))