Filipe am 22.09.: "die aufgaben die man unten sieht soll man auch oben sehen." GEMESSEN an den echten Daten: aufgaben_zuteilung hatte NULL Zeilen, waehrend im Brett zwei Aufgaben standen (OFFEN 1, IN ARBEIT 1). Jede Person las oben "nichts zugeteilt". Die Aufgaben hingen am aelteren Feld aufgaben.verantwortlich_id -- genau dem Feld, nach dem das Brett gruppiert. Die Uebersicht las eine andere Quelle als die Liste darunter. resuemeeFuer() zaehlt jetzt BEIDE Wege und keinen doppelt: Gibt es zu einer Aufgabe eine Zuteilungszeile fuer die Person, gewinnt die Zuteilung (genauerer Zustand). Nur wo keine Zeile existiert, zaehlt das alte Feld. Die Zuordnung status->zustand steht an EINER Stelle; "review" zaehlt wie "arbeit", "abgebrochen" zaehlt nirgends -- so wie im Brett auch. Die Personenliste wird nicht mehr aufgezaehlt, sondern abgeleitet: die Rollen des Hauses (immer, auch mit null Aufgaben -- wer frei ist, ist die haeufigste Frage) PLUS jede aktive Person, der eine Aufgabe gehoert. Ohne den zweiten Teil koennte im Brett eine Aufgabe stehen, deren Mensch oben fehlt. pruef-zuteilung 71/0 (war 65). Der neue Abschnitt legt eine Aufgabe GENAU SO an wie die echten alten -- verantwortlich_id, keine Zuteilungszeile -- und misst Filipes Satz direkt: "unten im Brett stehen genauso viele wie oben (2 = 2)". Zahl in der Bedingung, nicht nur im Meldetext. Co-Authored-By: Claude Opus 5 <[email protected]>
594 lines
26 KiB
JavaScript
594 lines
26 KiB
JavaScript
/* =====================================================================
|
|
AUFGABEN AN MENSCHEN — ZUTEILEN, ANNEHMEN, ABLEHNEN, BEWERTEN
|
|
=====================================================================
|
|
|
|
Auftrag vom 21.09.2026, Abschnitte 2 bis 5.
|
|
|
|
WAS DIESES MODUL BEANTWORTET — und was es bewusst NICHT tut:
|
|
|
|
Es beantwortet die Frage "wie steht diese Aufgabe bei DIESEM
|
|
Menschen". Die Frage "wie steht die Aufgabe" beantwortet weiterhin
|
|
`aufgaben.status` in workspace-aufgaben.js. Zwei Fragen, zwei Orte —
|
|
ein einziges Feld könnte bei drei Leuten nicht gleichzeitig
|
|
"angenommen" und "abgelehnt" sein.
|
|
|
|
DREI ARTEN ZU VERTEILEN, und der Unterschied zwischen den letzten
|
|
beiden ist der ganze Punkt:
|
|
|
|
einzeln eine Person, sie macht es
|
|
mehrere mehrere Personen, JEDE macht ihren Teil
|
|
pool mehrere sehen es, EINE nimmt es — danach ist es für die
|
|
anderen erledigt, damit niemand doppelt arbeitet
|
|
|
|
WER DARF WAS:
|
|
|
|
verteilen DogFather, rechte Hand, linke Hand
|
|
(darfAufgabenVerteilen — eine Quelle, nicht drei)
|
|
annehmen nur, wer sie bekommen hat
|
|
ablehnen dito, mit Begründung — Pflicht, siehe unten
|
|
übernehmen nur bei "pool", nur wer angeboten bekommen hat,
|
|
und nur solange niemand sonst sie genommen hat
|
|
bewerten nur wer verteilen darf, und erst wenn erledigt
|
|
|
|
WARUM DIE BEGRÜNDUNG BEIM ABLEHNEN PFLICHT IST: Filipe schreibt
|
|
"soll die Modi die Möglichkeit haben, eine Begründung bzw. Nachricht
|
|
zu hinterlassen". Ein leeres Nein ist für den, der die Aufgabe
|
|
verteilt hat, keine Information — er muss dann nachfragen, und genau
|
|
das sollte die Nachricht ersparen. Deshalb: mindestens ein paar
|
|
Zeichen. Wer wirklich nichts sagen will, schreibt "keine Zeit" —
|
|
auch das ist mehr als nichts.
|
|
|
|
WAS MIT `verantwortlich_id` PASSIERT (die alte Spalte):
|
|
|
|
einzeln = die eine Person
|
|
pool = leer, bis jemand übernimmt — danach der Übernehmer
|
|
mehrere = leer
|
|
|
|
Sie bleibt, weil das Aufgabenbrett, die Startseite und die Übersicht
|
|
danach gruppieren. Sie abzuschaffen hieße, drei gewachsene Ansichten
|
|
gleichzeitig umzubauen; sie stehen zu lassen und daneben die
|
|
Zuteilung zu führen ist der kleinere Eingriff — und bei "einzeln",
|
|
also dem häufigsten Fall, sagen beide dasselbe.
|
|
===================================================================== */
|
|
|
|
import express from "express";
|
|
import {
|
|
db, protokolliere, echteIp, darfAufgabenVerteilen, istDogFather, istHand,
|
|
sitzungLesen,
|
|
} from "./workspace.js";
|
|
import { TEAM_DOGI_ROLLEN } from "./crew-adresse.js";
|
|
|
|
export const zuteilungRouter = express.Router();
|
|
|
|
/* DIESELBE SCHRANKE WIE BEIM AUFGABENBRETT, und sie haengt UEBER allem
|
|
in diesem Modul: Wer nicht angemeldet ist, kommt an keine dieser
|
|
Routen. Eine Schranke je Route waere die Stelle, an der die naechste
|
|
Route sie vergisst.
|
|
|
|
`express.json` mit kleiner Grenze -- hier kommen Begruendungen und
|
|
Rueckmeldungen herein, keine Dateien. */
|
|
/* EIGENE SCHRANKE, KEIN IMPORT AUS workspace-aufgaben.js: Die Datei
|
|
dort holt sich gleich `zuteilen` und `mitZuteilung` von hier. Ein
|
|
Import in beide Richtungen waere ein Ringschluss -- der faellt in
|
|
Node nicht sofort auf, sondern erst, wenn eine der beiden Dateien
|
|
beim Laden auf etwas zugreift, das die andere noch nicht gesetzt
|
|
hat. Das sind vier Zeilen, die dort auch stehen; hier ist die
|
|
Dopplung billiger als der Ring. */
|
|
function angemeldet(req, res, next) {
|
|
const person = sitzungLesen(req);
|
|
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
|
req.person = person;
|
|
next();
|
|
}
|
|
|
|
zuteilungRouter.use("/workspace/api", angemeldet);
|
|
zuteilungRouter.use("/workspace/api", express.json({ limit: "8kb" }));
|
|
const jetzt = () => new Date().toISOString();
|
|
|
|
/** Die drei Arten. Als Menge, damit eine erfundene Art nicht
|
|
* stillschweigend durchrutscht und als NULL in der Tabelle landet. */
|
|
export const VERTEILARTEN = new Set(["einzeln", "mehrere", "pool"]);
|
|
|
|
/** Die Zustände eines Menschen zu einer Aufgabe, in der Reihenfolge,
|
|
* in der sie im Alltag durchlaufen werden. */
|
|
export const ZUSTAENDE = ["offen", "angenommen", "arbeit", "erledigt", "abgelehnt"];
|
|
|
|
/** Die drei Rückmeldungen aus Abschnitt 5. */
|
|
export const BEWERTUNGEN = new Set(["gut", "nicht_gut", "verbessern"]);
|
|
|
|
/** Klartext für die Oberfläche. Sie soll die Wörter nicht selbst
|
|
* erfinden — sonst heißt derselbe Zustand an zwei Stellen anders. */
|
|
export const ZUSTAND_TEXT = {
|
|
offen: "Noch nicht beantwortet",
|
|
angenommen: "Angenommen",
|
|
arbeit: "In Bearbeitung",
|
|
erledigt: "Erledigt",
|
|
abgelehnt: "Abgelehnt",
|
|
};
|
|
|
|
export const BEWERTUNG_TEXT = {
|
|
gut: "Gut gemacht",
|
|
nicht_gut: "Nicht gut gemacht",
|
|
verbessern: "Verbesserungsmöglichkeiten",
|
|
};
|
|
|
|
/* =====================================================================
|
|
LESEN
|
|
===================================================================== */
|
|
|
|
/** Alle Zuteilungen zu einer Menge von Aufgaben — in EINER Abfrage.
|
|
*
|
|
* EINE ABFRAGE UND NICHT EINE JE AUFGABE: Bei vierzig Aufgaben auf
|
|
* dem Brett wären das vierzig Abfragen, und die Seite würde spürbar
|
|
* langsamer, ohne dass jemand sähe, warum. */
|
|
export function zuteilungenZu(aufgabeIds) {
|
|
const ids = [...new Set((aufgabeIds || []).map(Number).filter(Number.isInteger))];
|
|
if (!ids.length) return new Map();
|
|
const platz = ids.map(() => "?").join(",");
|
|
const reihen = db().prepare(`
|
|
SELECT z.id, z.aufgabe_id, z.person_id, z.zustand, z.grund,
|
|
z.zugeteilt_am, z.geantwortet_am, z.erledigt_am,
|
|
z.bewertung, z.bewertung_text, z.bewertet_am,
|
|
p.name AS person_name, p.rolle AS person_rolle,
|
|
pb.name AS bewertet_von_name
|
|
FROM aufgaben_zuteilung z
|
|
LEFT JOIN personen p ON p.id = z.person_id
|
|
LEFT JOIN personen pb ON pb.id = z.bewertet_von
|
|
WHERE z.aufgabe_id IN (${platz})
|
|
ORDER BY p.name COLLATE NOCASE`).all(...ids);
|
|
|
|
const karte = new Map();
|
|
for (const z of reihen) {
|
|
if (!karte.has(z.aufgabe_id)) karte.set(z.aufgabe_id, []);
|
|
karte.get(z.aufgabe_id).push({
|
|
...z,
|
|
zustand_text: ZUSTAND_TEXT[z.zustand] || z.zustand,
|
|
bewertung_text_kurz: z.bewertung ? BEWERTUNG_TEXT[z.bewertung] : null,
|
|
});
|
|
}
|
|
return karte;
|
|
}
|
|
|
|
/** Hängt die Zuteilungen an eine Liste von Aufgabenzeilen.
|
|
*
|
|
* Dazu je Zeile drei abgeleitete Angaben, damit die Oberfläche nicht
|
|
* dieselbe Rechnung noch einmal macht — zwei Rechnungen für dieselbe
|
|
* Frage laufen auseinander. */
|
|
export function mitZuteilung(reihen, ich) {
|
|
const liste = Array.isArray(reihen) ? reihen : [];
|
|
const karte = zuteilungenZu(liste.map((r) => r.id));
|
|
for (const r of liste) {
|
|
const zu = karte.get(r.id) || [];
|
|
r.zuteilung = zu;
|
|
r.verteilart = r.verteilart || (zu.length > 1 ? "mehrere" : (zu.length ? "einzeln" : null));
|
|
/* MEIN eigener Stand — das ist die Angabe, aus der die persönliche
|
|
Ansicht eines Modi entsteht. */
|
|
r.meine_zuteilung = ich ? (zu.find((z) => z.person_id === ich.id) || null) : null;
|
|
/* BEI EINER POOL-AUFGABE: Hat sie schon jemand? Der Auftrag
|
|
verlangt ausdrücklich, dass die anderen das erkennen, "damit
|
|
keine doppelte Bearbeitung entsteht". */
|
|
r.uebernommen_von = r.verteilart === "pool"
|
|
? (zu.find((z) => z.zustand !== "offen" && z.zustand !== "abgelehnt")?.person_name || null)
|
|
: null;
|
|
}
|
|
return liste;
|
|
}
|
|
|
|
/* =====================================================================
|
|
SCHREIBEN — die eine Stelle, an der zugeteilt wird
|
|
===================================================================== */
|
|
|
|
/** Legt die Zuteilungen einer Aufgabe an (oder schreibt sie neu).
|
|
*
|
|
* Gibt zurück, was wirklich gesetzt wurde — der Aufrufer soll nicht
|
|
* raten müssen, ob seine Wünsche durchgekommen sind. */
|
|
export function zuteilen(aufgabeId, personIds, art, vonId) {
|
|
const id = Number(aufgabeId);
|
|
if (!Number.isInteger(id)) return { fehler: "ungueltige_aufgabe" };
|
|
|
|
const roh = [...new Set((personIds || []).map(Number).filter(Number.isInteger))];
|
|
if (!roh.length) return { gesetzt: [], art: null };
|
|
|
|
/* NUR AKTIVE MENSCHEN. Eine Aufgabe an einen gesperrten Zugang zu
|
|
hängen heißt: Sie steht im Resümee und niemand kann sie machen. */
|
|
const platz = roh.map(() => "?").join(",");
|
|
const echt = db().prepare(
|
|
`SELECT id FROM personen WHERE id IN (${platz}) AND aktiv = 1`).all(...roh).map((z) => z.id);
|
|
if (!echt.length) return { fehler: "keine_person" };
|
|
|
|
/* DIE ART WIRD ABGELEITET, WO SIE SICH ABLEITEN LÄSST. Eine Person
|
|
kann kein Pool sein — "der erste, der Zeit hat" braucht mindestens
|
|
zwei, die es sehen. Wer trotzdem "pool" schickt, bekommt
|
|
"einzeln"; das ist, was er gemeint haben muss. */
|
|
let gesetzteArt = VERTEILARTEN.has(art) ? art : (echt.length > 1 ? "mehrere" : "einzeln");
|
|
if (echt.length === 1) gesetzteArt = "einzeln";
|
|
|
|
const nun = jetzt();
|
|
const alt = db().prepare(
|
|
"SELECT person_id, zustand FROM aufgaben_zuteilung WHERE aufgabe_id = ?").all(id);
|
|
const behalten = new Map(alt.map((z) => [z.person_id, z.zustand]));
|
|
|
|
/* WER SCHON GEANTWORTET HAT, BEHÄLT SEINEN STAND. Sonst setzte ein
|
|
zweites Speichern im Formular ein "angenommen" zurück auf "offen"
|
|
— und der Mensch müsste noch einmal zusagen, ohne zu wissen,
|
|
warum. */
|
|
const setz = db().prepare(`
|
|
INSERT INTO aufgaben_zuteilung (aufgabe_id, person_id, zustand, zugeteilt_am, zugeteilt_von)
|
|
VALUES (?,?,?,?,?)
|
|
ON CONFLICT (aufgabe_id, person_id) DO NOTHING`);
|
|
for (const pid of echt) setz.run(id, pid, "offen", nun, vonId ?? null);
|
|
|
|
/* WER NICHT MEHR DABEI IST, FLIEGT RAUS — aber nur, wenn er noch
|
|
nicht geantwortet hat. Jemandem eine angenommene Aufgabe
|
|
wegzunehmen, ohne dass er es erfährt, wäre die unangenehmste Art,
|
|
Arbeit zu verlieren. */
|
|
for (const [pid, zustand] of behalten) {
|
|
if (echt.includes(pid)) continue;
|
|
if (zustand !== "offen") continue;
|
|
db().prepare("DELETE FROM aufgaben_zuteilung WHERE aufgabe_id = ? AND person_id = ?")
|
|
.run(id, pid);
|
|
}
|
|
|
|
db().prepare("UPDATE aufgaben SET verteilart = ? WHERE id = ?").run(gesetzteArt, id);
|
|
|
|
/* `verantwortlich_id` mitführen — siehe Kopf. Bei "einzeln" ist sie
|
|
die Person, sonst bleibt sie leer, bis jemand übernimmt. */
|
|
if (gesetzteArt === "einzeln") {
|
|
db().prepare("UPDATE aufgaben SET verantwortlich_id = ? WHERE id = ?").run(echt[0], id);
|
|
} else if (gesetzteArt === "mehrere") {
|
|
db().prepare("UPDATE aufgaben SET verantwortlich_id = NULL WHERE id = ?").run(id);
|
|
}
|
|
|
|
return { gesetzt: echt, art: gesetzteArt };
|
|
}
|
|
|
|
/* =====================================================================
|
|
DIE WEGE
|
|
===================================================================== */
|
|
|
|
function holen(req, res) {
|
|
const id = Number(req.params.id);
|
|
if (!Number.isInteger(id)) { res.status(400).json({ fehler: "ungueltig" }); return null; }
|
|
const a = db().prepare("SELECT id, titel, status, verteilart FROM aufgaben WHERE id = ?").get(id);
|
|
if (!a) { res.status(404).json({ fehler: "gibt_es_nicht" }); return null; }
|
|
return a;
|
|
}
|
|
|
|
/** Meine eigene Zuteilung zu dieser Aufgabe — oder nichts. */
|
|
function meine(aufgabeId, personId) {
|
|
return db().prepare(
|
|
"SELECT * FROM aufgaben_zuteilung WHERE aufgabe_id = ? AND person_id = ?")
|
|
.get(aufgabeId, personId) || null;
|
|
}
|
|
|
|
/* ---- Annehmen ------------------------------------------------------- */
|
|
zuteilungRouter.post("/workspace/api/aufgaben/:id/annehmen", (req, res) => {
|
|
try {
|
|
const a = holen(req, res); if (!a) return;
|
|
const z = meine(a.id, req.person.id);
|
|
if (!z) return res.status(403).json({ fehler: "nicht_zugeteilt" });
|
|
if (z.zustand === "erledigt") return res.status(409).json({ fehler: "schon_erledigt" });
|
|
|
|
db().prepare(`UPDATE aufgaben_zuteilung
|
|
SET zustand = 'angenommen', grund = NULL, geantwortet_am = ?
|
|
WHERE id = ?`).run(jetzt(), z.id);
|
|
|
|
/* BEIM POOL GEWINNT DER ERSTE. Sobald einer zusagt, ist sie für
|
|
die anderen weg — das ist der Zweck der Sache. */
|
|
if (a.verteilart === "pool") uebernahmeAbschliessen(a.id, req.person.id);
|
|
|
|
protokolliere("aufgabe_angenommen", {
|
|
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
|
detail: `#${a.id} ${a.titel}`.slice(0, 120),
|
|
});
|
|
res.json({ ok: true, zustand: "angenommen" });
|
|
} catch (f) {
|
|
console.error("[zuteilung] annehmen:", f?.message);
|
|
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
|
}
|
|
});
|
|
|
|
/* ---- Ablehnen, mit Begründung --------------------------------------- */
|
|
zuteilungRouter.post("/workspace/api/aufgaben/:id/ablehnen", (req, res) => {
|
|
try {
|
|
const a = holen(req, res); if (!a) return;
|
|
const z = meine(a.id, req.person.id);
|
|
if (!z) return res.status(403).json({ fehler: "nicht_zugeteilt" });
|
|
|
|
const grund = String(req.body?.grund ?? "").trim();
|
|
if (grund.length < 3) return res.status(400).json({ fehler: "grund_fehlt" });
|
|
if (grund.length > 500) return res.status(400).json({ fehler: "grund_zu_lang" });
|
|
|
|
db().prepare(`UPDATE aufgaben_zuteilung
|
|
SET zustand = 'abgelehnt', grund = ?, geantwortet_am = ?
|
|
WHERE id = ?`).run(grund, jetzt(), z.id);
|
|
|
|
/* HAT SIE NIEMAND MEHR? Dann muss das sichtbar sein. Die Aufgabe
|
|
bleibt stehen (sie ist ja nicht erledigt) — aber ohne
|
|
Verantwortliche, damit sie im Resümee der Leitung auffällt. */
|
|
if (a.verteilart === "einzeln") {
|
|
db().prepare("UPDATE aufgaben SET verantwortlich_id = NULL WHERE id = ?").run(a.id);
|
|
}
|
|
|
|
protokolliere("aufgabe_abgelehnt", {
|
|
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
|
detail: `#${a.id} ${grund}`.slice(0, 120),
|
|
});
|
|
res.json({ ok: true, zustand: "abgelehnt" });
|
|
} catch (f) {
|
|
console.error("[zuteilung] ablehnen:", f?.message);
|
|
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
|
}
|
|
});
|
|
|
|
/** Beim Pool: Einer hat sie, alle anderen sind raus.
|
|
*
|
|
* ALS EIGENE FUNKTION, weil zwei Wege hierher führen (annehmen und
|
|
* übernehmen) und zwei Abschriften derselben Regel auseinanderlaufen. */
|
|
function uebernahmeAbschliessen(aufgabeId, personId) {
|
|
db().prepare(`UPDATE aufgaben_zuteilung
|
|
SET zustand = 'abgelehnt', grund = 'Von jemand anderem übernommen',
|
|
geantwortet_am = ?
|
|
WHERE aufgabe_id = ? AND person_id <> ? AND zustand = 'offen'`)
|
|
.run(jetzt(), aufgabeId, personId);
|
|
db().prepare("UPDATE aufgaben SET verantwortlich_id = ? WHERE id = ?")
|
|
.run(personId, aufgabeId);
|
|
}
|
|
|
|
/* ---- Aus dem Pool übernehmen ---------------------------------------- */
|
|
zuteilungRouter.post("/workspace/api/aufgaben/:id/uebernehmen", (req, res) => {
|
|
try {
|
|
const a = holen(req, res); if (!a) return;
|
|
if (a.verteilart !== "pool") return res.status(409).json({ fehler: "kein_pool" });
|
|
const z = meine(a.id, req.person.id);
|
|
if (!z) return res.status(403).json({ fehler: "nicht_zugeteilt" });
|
|
|
|
/* HAT SCHON JEMAND? Diese Frage muss VOR dem Schreiben stehen,
|
|
sonst nehmen zwei Leute im selben Moment dieselbe Aufgabe und
|
|
beide sehen "hat geklappt". */
|
|
const schon = db().prepare(`SELECT p.name FROM aufgaben_zuteilung z
|
|
LEFT JOIN personen p ON p.id = z.person_id
|
|
WHERE z.aufgabe_id = ? AND z.person_id <> ?
|
|
AND z.zustand IN ('angenommen','arbeit','erledigt')`).get(a.id, req.person.id);
|
|
if (schon) return res.status(409).json({ fehler: "schon_vergeben", wer: schon.name });
|
|
|
|
db().prepare(`UPDATE aufgaben_zuteilung
|
|
SET zustand = 'angenommen', grund = NULL, geantwortet_am = ?
|
|
WHERE id = ?`).run(jetzt(), z.id);
|
|
uebernahmeAbschliessen(a.id, req.person.id);
|
|
|
|
protokolliere("aufgabe_uebernommen", {
|
|
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
|
detail: `#${a.id} ${a.titel}`.slice(0, 120),
|
|
});
|
|
res.json({ ok: true, zustand: "angenommen" });
|
|
} catch (f) {
|
|
console.error("[zuteilung] uebernehmen:", f?.message);
|
|
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
|
}
|
|
});
|
|
|
|
/* ---- Meinen eigenen Stand setzen ------------------------------------ */
|
|
zuteilungRouter.post("/workspace/api/aufgaben/:id/mein-stand", (req, res) => {
|
|
try {
|
|
const a = holen(req, res); if (!a) return;
|
|
const z = meine(a.id, req.person.id);
|
|
if (!z) return res.status(403).json({ fehler: "nicht_zugeteilt" });
|
|
|
|
const ziel = String(req.body?.zustand ?? "").trim();
|
|
/* NUR DIE DREI, DIE MAN SELBST SETZEN DARF. "offen" zurückzudrehen
|
|
hieße, die eigene Zusage zu löschen; "abgelehnt" hat einen
|
|
eigenen Weg, weil es eine Begründung braucht. */
|
|
if (!["angenommen", "arbeit", "erledigt"].includes(ziel)) {
|
|
return res.status(400).json({ fehler: "ungueltiger_zustand" });
|
|
}
|
|
if (z.zustand === "abgelehnt") return res.status(409).json({ fehler: "abgelehnt" });
|
|
|
|
db().prepare(`UPDATE aufgaben_zuteilung
|
|
SET zustand = ?, erledigt_am = ?
|
|
WHERE id = ?`).run(ziel, ziel === "erledigt" ? jetzt() : null, z.id);
|
|
|
|
protokolliere("aufgabe_stand", {
|
|
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
|
detail: `#${a.id} ${ziel}`.slice(0, 120),
|
|
});
|
|
res.json({ ok: true, zustand: ziel });
|
|
} catch (f) {
|
|
console.error("[zuteilung] mein-stand:", f?.message);
|
|
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
|
}
|
|
});
|
|
|
|
/* ---- Bewerten (Abschnitt 5) ----------------------------------------- */
|
|
zuteilungRouter.post("/workspace/api/aufgaben/:id/bewerten", (req, res) => {
|
|
try {
|
|
const a = holen(req, res); if (!a) return;
|
|
if (!darfAufgabenVerteilen(req.person)) {
|
|
return res.status(403).json({ fehler: "darf_nicht_bewerten" });
|
|
}
|
|
const personId = Number(req.body?.person_id);
|
|
const bewertung = String(req.body?.bewertung ?? "").trim();
|
|
const text = String(req.body?.text ?? "").trim();
|
|
|
|
if (!BEWERTUNGEN.has(bewertung)) return res.status(400).json({ fehler: "ungueltige_bewertung" });
|
|
/* BEI "VERBESSERUNGSMÖGLICHKEITEN" IST DER TEXT PFLICHT. Filipe:
|
|
"soll eine entsprechende Kachel bzw. ein Eingabebereich
|
|
erscheinen. Dogfather und die rechte Hand sollen dort einen
|
|
individuellen Text hinterlegen." Ein leeres "kann besser" sagt
|
|
dem, der es liest, nichts — außer dass jemand unzufrieden war. */
|
|
if (bewertung === "verbessern" && text.length < 3) {
|
|
return res.status(400).json({ fehler: "text_fehlt" });
|
|
}
|
|
if (text.length > 2000) return res.status(400).json({ fehler: "text_zu_lang" });
|
|
|
|
const z = db().prepare(
|
|
"SELECT * FROM aufgaben_zuteilung WHERE aufgabe_id = ? AND person_id = ?")
|
|
.get(a.id, personId);
|
|
if (!z) return res.status(404).json({ fehler: "nicht_zugeteilt" });
|
|
/* ERST FERTIG, DANN BEWERTEN. Eine Rückmeldung auf etwas, das noch
|
|
läuft, ist keine Bewertung, sondern eine Einmischung. */
|
|
if (z.zustand !== "erledigt") return res.status(409).json({ fehler: "noch_nicht_erledigt" });
|
|
|
|
db().prepare(`UPDATE aufgaben_zuteilung
|
|
SET bewertung = ?, bewertung_text = ?, bewertet_von = ?, bewertet_am = ?
|
|
WHERE id = ?`).run(bewertung, text || null, req.person.id, jetzt(), z.id);
|
|
|
|
protokolliere("aufgabe_bewertet", {
|
|
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
|
detail: `#${a.id} -> #${personId} ${bewertung}`.slice(0, 120),
|
|
});
|
|
res.json({ ok: true, bewertung, text: text || null });
|
|
} catch (f) {
|
|
console.error("[zuteilung] bewerten:", f?.message);
|
|
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
|
}
|
|
});
|
|
|
|
/* ---- Das persönliche Resümee (Abschnitt 1 und 4) -------------------- */
|
|
|
|
/** Zählt für eine Person, wie ihre Aufgaben stehen.
|
|
*
|
|
* ABGELEITET AUS DEN ZUSTÄNDEN, nicht aus einer zweiten Tabelle: Eine
|
|
* mitgeführte Zahl ist eine Zahl von gestern, sobald jemand etwas
|
|
* ändert und die Stelle vergisst. */
|
|
/** Der Zustand einer Aufgabe, die jemandem auf dem ALTEN Weg gehoert.
|
|
*
|
|
* Die Aufgabentabelle kennt offen/arbeit/review/erledigt/abgebrochen,
|
|
* die Zuteilung kennt offen/angenommen/arbeit/erledigt/abgelehnt.
|
|
* "review" heisst: liegt zur Freigabe, ist also noch nicht vom Tisch --
|
|
* deshalb zaehlt es wie "arbeit". "abgebrochen" ist weder dran noch
|
|
* erledigt und zaehlt gar nicht mit; es erscheint auch im Brett nicht
|
|
* in den Spalten OFFEN und IN ARBEIT.
|
|
*
|
|
* Die Zuordnung steht hier an EINER Stelle. Wer sie an zwei Stellen
|
|
* schreibt, hat beim naechsten Statuswert eine, die niemand nachzieht. */
|
|
const STATUS_ALS_ZUSTAND = {
|
|
offen: "offen", arbeit: "arbeit", review: "arbeit", erledigt: "erledigt",
|
|
};
|
|
|
|
export function resuemeeFuer(personId) {
|
|
/* ZWEI QUELLEN, EIN ERGEBNIS (22.09.2026).
|
|
==========================================================
|
|
Filipe: "die aufgaben die man unten sieht soll man auch oben
|
|
sehen."
|
|
|
|
Gemessen an den echten Daten: aufgaben_zuteilung hatte NULL
|
|
Zeilen, waehrend im Brett zwei Aufgaben standen (OFFEN 1,
|
|
IN ARBEIT 1). Jede Person las oben "nichts zugeteilt" -- weil
|
|
diese Zaehlung nur die neue Tabelle kannte. Die Aufgaben hingen
|
|
am aelteren Feld `aufgaben.verantwortlich_id`, und das ist genau
|
|
das Feld, nach dem das BRETT gruppiert.
|
|
|
|
Eine Uebersicht, die eine andere Quelle liest als die Liste
|
|
darunter, widerspricht ihr frueher oder spaeter. Deshalb zaehlen
|
|
hier BEIDE Wege -- und keiner doppelt: Gibt es zu einer Aufgabe
|
|
eine Zuteilungszeile fuer diese Person, gewinnt die Zuteilung
|
|
(sie hat den genaueren Zustand: angenommen, abgelehnt, bewertet).
|
|
Nur wo keine Zeile existiert, zaehlt das alte Feld. */
|
|
const zeilen = db().prepare(`
|
|
SELECT z.zustand, z.bewertung, a.frist, a.status AS aufgabe_status
|
|
FROM aufgaben_zuteilung z
|
|
JOIN aufgaben a ON a.id = z.aufgabe_id
|
|
WHERE z.person_id = ?`).all(Number(personId));
|
|
|
|
const alte = db().prepare(`
|
|
SELECT a.status, a.frist
|
|
FROM aufgaben a
|
|
WHERE a.verantwortlich_id = ?
|
|
AND NOT EXISTS (SELECT 1 FROM aufgaben_zuteilung z
|
|
WHERE z.aufgabe_id = a.id AND z.person_id = a.verantwortlich_id)
|
|
`).all(Number(personId));
|
|
for (const a of alte) {
|
|
const zustand = STATUS_ALS_ZUSTAND[a.status];
|
|
if (!zustand) continue; /* abgebrochen: zaehlt nirgends */
|
|
zeilen.push({ zustand, bewertung: null, frist: a.frist, aufgabe_status: a.status });
|
|
}
|
|
|
|
const heute = new Date().toISOString().slice(0, 10);
|
|
const zaehl = { offen: 0, angenommen: 0, arbeit: 0, erledigt: 0, abgelehnt: 0 };
|
|
let ueberfaellig = 0, gelobt = 0, zuVerbessern = 0;
|
|
for (const z of zeilen) {
|
|
if (zaehl[z.zustand] !== undefined) zaehl[z.zustand]++;
|
|
const laeuft = z.zustand !== "erledigt" && z.zustand !== "abgelehnt";
|
|
if (laeuft && z.frist && z.frist < heute) ueberfaellig++;
|
|
if (z.bewertung === "gut") gelobt++;
|
|
if (z.bewertung === "verbessern" || z.bewertung === "nicht_gut") zuVerbessern++;
|
|
}
|
|
return {
|
|
gesamt: zeilen.length, ...zaehl, ueberfaellig, gelobt, zu_verbessern: zuVerbessern,
|
|
/* Was gerade zu tun ist — die eine Zahl, die ein Mensch morgens
|
|
wissen will. */
|
|
dran: zaehl.offen + zaehl.angenommen + zaehl.arbeit,
|
|
};
|
|
}
|
|
|
|
zuteilungRouter.get("/workspace/api/aufgaben/resuemee", (req, res) => {
|
|
try {
|
|
/* WER DARF WESSEN RESÜMEE SEHEN? Sein eigenes jeder. Fremde nur,
|
|
wer Aufgaben verteilt — das ist genau die Gruppe, die im Auftrag
|
|
"eine vollständige Gesamtübersicht" bekommen soll. */
|
|
const wunsch = Number(req.query?.person);
|
|
const fremd = Number.isInteger(wunsch) && wunsch !== req.person.id;
|
|
if (fremd && !darfAufgabenVerteilen(req.person)) {
|
|
return res.status(403).json({ fehler: "nicht_erlaubt" });
|
|
}
|
|
if (!fremd) return res.json({ person_id: req.person.id, ...resuemeeFuer(req.person.id) });
|
|
|
|
const p = db().prepare("SELECT id, name, rolle FROM personen WHERE id = ?").get(wunsch);
|
|
if (!p) return res.status(404).json({ fehler: "gibt_es_nicht" });
|
|
res.json({ person_id: p.id, name: p.name, rolle: p.rolle, ...resuemeeFuer(p.id) });
|
|
} catch (f) {
|
|
console.error("[zuteilung] resuemee:", f?.message);
|
|
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
|
}
|
|
});
|
|
|
|
/** Das Resümee ALLER — für DogFather, rechte und linke Hand.
|
|
*
|
|
* Abschnitt 1: "Auf einen Blick muss erkennbar sein, wer welche
|
|
* Aufgabe hat, was erledigt wurde, was noch offen ist und wo
|
|
* Handlungsbedarf besteht." */
|
|
zuteilungRouter.get("/workspace/api/aufgaben/team-resuemee", (req, res) => {
|
|
try {
|
|
if (!darfAufgabenVerteilen(req.person)) {
|
|
return res.status(403).json({ fehler: "nicht_erlaubt" });
|
|
}
|
|
/* WER GEHÖRT IN DIE ÜBERSICHT: alle, die Aufgaben bekommen können.
|
|
Abgeleitet aus der Rollenliste dieses Hauses statt aus einer
|
|
eigenen Aufzählung — wer morgen eine Rolle hinzufügt, muss hier
|
|
nichts nachziehen. */
|
|
/* WER IN DER LISTE STEHT -- ABGELEITET, NICHT AUFGEZAEHLT.
|
|
==========================================================
|
|
Die Rollen des Hauses stehen immer da, auch mit null Aufgaben:
|
|
Wer frei ist, ist die haeufigste Frage beim Verteilen.
|
|
|
|
Dazu kommt JEDE aktive Person, der im Brett eine Aufgabe
|
|
gehoert. Ohne diesen zweiten Teil koennte im Brett eine Aufgabe
|
|
stehen, deren Mensch oben fehlt -- genau der Widerspruch, den
|
|
Filipe am 22.09. gemeldet hat, nur an anderer Stelle. So kann
|
|
das nicht wieder entstehen: Die Liste folgt den Aufgaben. */
|
|
const rollen = [...TEAM_DOGI_ROLLEN];
|
|
const platz = rollen.map(() => "?").join(",");
|
|
const leute = db().prepare(`
|
|
SELECT id, name, rolle FROM personen
|
|
WHERE aktiv = 1
|
|
AND (rolle IN (${platz})
|
|
OR id IN (SELECT verantwortlich_id FROM aufgaben
|
|
WHERE verantwortlich_id IS NOT NULL)
|
|
OR id IN (SELECT person_id FROM aufgaben_zuteilung))
|
|
AND rolle NOT IN ('creator','scout','manager','spicy','gast')
|
|
ORDER BY CASE rolle WHEN 'admin' THEN 0 WHEN 'hand' THEN 1
|
|
WHEN 'linke' THEN 2 ELSE 3 END,
|
|
name COLLATE NOCASE`).all(...rollen);
|
|
res.json({
|
|
leute: leute.map((p) => ({ ...p, ...resuemeeFuer(p.id) })),
|
|
darf_bewerten: true,
|
|
ist_dogfather: istDogFather(req.person),
|
|
ist_hand: istHand(req.person),
|
|
});
|
|
} catch (f) {
|
|
console.error("[zuteilung] team-resuemee:", f?.message);
|
|
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
|
}
|
|
});
|