Brett je Einheit, Stufe 3: Aufgaben sehen, wer sie sehen soll (#684)

- TaskBoard: das Brett einer Aufgabe wird nicht gespeichert, es folgt aus
  der Organisation - Einheit des Zustaendigen, sonst des Anlegers. Offene
  Aufgaben ziehen damit beim Umzug mit. Sichtbar: das eigene Brett, fuer eine
  Leitung alle darunter, und was man selbst angelegt hat (nach oben
  Gereichtes bleibt im Blick).
- TaskItem.CreatedBy, in der Datei als created_by und in der DB. Ein Import
  aus einer Datei ohne das Feld behaelt, was die DB weiss.
- Taskboard-Tool: task_list, task_find und die Dublettenpruefung sehen nur
  die eigenen Bretter - eine aehnliche Buchhaltungsaufgabe haelt keine
  Marketingaufgabe auf und bekommt keinen Kommentar von dort. task_update und
  task_comment antworten auf eine fremde Aufgabe wie auf eine unbekannte.
- TaskboardService.ListAsync mit Sichtfilter: erst filtern, dann kappen.
- WebUI: jede Aufgabe nennt ihr Brett, ein Filter waehlt eines aus.
- Konzept (Abschnitt 3) und Einrichtungshandbuch nachgetragen.

Ohne Einheiten aendert sich nichts - ein Brett fuer alle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-24 17:03:39 +02:00
co-authored by Claude Opus 5.5
parent 609298dd7a
commit 29d953a5eb
17 changed files with 458 additions and 30 deletions
@@ -58,6 +58,9 @@ const ABGESCHLOSSEN = ["Done", "Canceled", "Archived"];
let zeigeAbgeschlossene = false;
/** #684: The board shown — "" for all, "-" for the top level, otherwise a unit id. */
let brettFilter = "";
export async function rendereAufgaben() {
const container = document.getElementById("ansicht-inhalt");
if (!container) return;
@@ -114,9 +117,14 @@ async function ladeTaskboard() {
try {
const alle = (await api("/tasks?limit=200")) || [];
const tasks = alle.filter((t) => zeigeAbgeschlossene || !ABGESCHLOSSEN.includes(t.status));
const offen = alle.filter((t) => zeigeAbgeschlossene || !ABGESCHLOSSEN.includes(t.status));
const tasks = offen.filter((t) => !brettFilter || (brettFilter === "-" ? !t.board : t.board === brettFilter));
container.replaceChildren();
// #684: With units, every task is on a unit's board — pick one to see only that.
const bretter = brettAuswahl(alle);
if (bretter) container.append(bretter);
if (tasks.length === 0) {
container.append(
erstelleKarte(t("tasks.empty"), [
@@ -141,6 +149,30 @@ async function ladeTaskboard() {
}
}
/**
* #684: The boards the tasks are on, as a filter. Built from the tasks themselves, so it
* shows only boards that hold something; absent when no task is on a unit's board.
*/
function brettAuswahl(tasks) {
const bretter = new Map();
for (const task of tasks) if (task.board) bretter.set(task.board, task.boardPath || task.board);
if (bretter.size === 0) return null;
const auswahl = erstelleElement("select", {
"aria-label": t("tasks.board"),
onchange: (e) => { brettFilter = e.target.value; ladeTaskboard(); }
},
erstelleElement("option", { value: "", text: t("tasks.boardAll") }),
erstelleElement("option", { value: "-", text: t("tasks.boardTopLevel") }),
...[...bretter.entries()]
.sort((a, b) => a[1].localeCompare(b[1]))
.map(([id, pfad]) => erstelleElement("option", { value: id, text: pfad })));
auswahl.value = brettFilter;
return erstelleElement("label", { klasse: "gruppe-horizontal text-klein taskboard-bretter" },
erstelleElement("span", { text: t("tasks.board") }), auswahl);
}
function terminText(task) {
if (!task.when) return t("tasks.scheduleNone");
const art = task.when.kind;
@@ -155,7 +187,10 @@ function erstelleAufgabenKarte(task) {
"div",
{ klasse: "task-status-zeile gruppe-horizontal" },
erstellePlakette(holeStatusText(task.status), STATUS_ART[task.status] || "neutral"),
erstelleElement("span", { klasse: "gedaempft text-klein", text: `${task.id} · ${task.assignee || "—"}` })
erstelleElement("span", {
klasse: "gedaempft text-klein",
text: `${task.id} · ${task.assignee || "—"}${task.boardPath ? ` · ▤ ${task.boardPath}` : ""}`
})
)
);