Beim Weiterarbeiten am Vorlagenbrett nachgemessen und gefunden:
`benachrichtige` kam in workspace-zuteilung.js KEIN EINZIGES MAL vor,
in workspace-vorlagen.js auch nicht.
Beide Bewerbungswege waren gebaut, beide funktionierten -- und beide
waren stumm:
* Bewirbt sich Frida, erfaehrt DogFather es nur, wenn er von sich
aus das Brett aufmacht.
* Antwortet er, erfaehrt Frida es nur, wenn SIE von sich aus
nachsieht.
Das ist keine Kleinigkeit, das ist die Funktion. Wer sich bewirbt,
wartet -- und Warten ohne Rueckmeldung fuehlt sich nach zwei Tagen an
wie "interessiert keinen". Genau das soll eine Bewerbung verhindern.
WER ES ERFAEHRT -- ABGELEITET, NICHT AUFGEZAEHLT
------------------------------------------------
Die naheliegende Zeile waere `rolle IN ('admin','hand')` gewesen; so
steht sie in workspace-hilfe.js. Das ist eine Abschrift, und
Abschriften altern: Kaeme morgen eine Rolle dazu, die entscheiden
darf, bekaeme sie keine einzige Meldung -- und niemand merkte es, weil
ja alles funktioniert.
Gefragt wird deshalb die Regel selbst (entscheidetUeberAufgaben),
Person fuer Person. Und zusaetzlich darfSchreibenMit: Wer den Bewerber
gar nicht sehen darf, bekommt auch keine Meldung ueber ihn. Das ist
keine Vorsicht um ihrer selbst willen -- ohne diese Zeile erfuehre die
Agentur ueber eine Push-Nachricht, dass es Team Dogi ueberhaupt gibt.
Gemessen: Die Bewerbung eines Modis erreicht genau zwei Leute
(admin, hand) von sechs Aktiven. Nicht die linke Hand (sie entscheidet
hier nicht mit), niemand aus dem anderen Haus, und nicht der Bewerber
selbst.
ZWEI SCHALTER, ZWEI ENTSCHEIDUNGEN
----------------------------------
"bewerbung_neu" trifft den, der antwortet -- an einem lebhaften Tag
mehrfach, das kann man stumm stellen wollen. "bewerbung_antwort"
trifft den, der wartet; sie kommt einmal, und niemand will sie stumm
stellen. Eine gemeinsame Art hiesse: beides zusammen abschalten oder
beides zusammen ertragen. Dieselbe Ueberlegung wie beim Chat
(Nachricht / Erwaehnung).
Beide von sich aus an. Keine Ausnahme von der Ruhezeit: Eine Bewerbung
wartet, ein Anruf nicht.
DIE NOTIZ STEHT IN DER MELDUNG
------------------------------
Filipe hat sie ausdruecklich verlangt ("mit einem text als notiz").
Sie erst zu verlangen und dann an genau der Stelle zu verschweigen, an
der man sie liest, waere die halbe Funktion. Und das Ergebnis steht im
TITEL -- "angenommen" oder "diesmal nicht" -- damit man es lesen kann,
ohne zu oeffnen. Auch die gute Nachricht.
Der Wortlaut steht in zwei reinen Funktionen (bewerbungText,
antwortText), exportiert, damit eine Pruefung sie lesen kann, ohne
einen Push-Dienst nachzubauen. Genau an so einer Stelle steckte am
18.09. der Fehler "Nachricht von [object Object]", der von aussen
nicht messbar war.
EINE STELLE FUER BEIDE WEGE
---------------------------
workspace-bewerbung-melden.js. Zwei Fassungen waeren zwei
Gelegenheiten, dass eine davon die Ruhezeit, die Abschaltbarkeit oder
die Haeusertrennung vergisst -- und dieselbe Person laese zweimal
etwas Verschiedenes ueber denselben Vorgang.
Die Meldung wird NICHT abgewartet (`void`): Ob sie durchgeht, haengt
am Push-Dienst, an der Ruhezeit und an den Einstellungen des
Empfaengers. Nichts davon darf entscheiden, ob die Bewerbung
gespeichert ist -- die ist es laengst.
NOCH EINE ROTE PRUEFUNG, DIE NIEMAND GESEHEN HAT
-------------------------------------------------
pruef-push-ziel meldete: "aber nicht auf eine Seite, die es fuer ihn
nicht gibt (/workspace/calls.html)". Das sah aus wie ein Befund und
war eine erfuellte Bestellung -- Filipe hatte am 22.09. genau das
Gegenteil bestellt ("jeder der einen kalender hat soll auch sowas
haben"). Nachgemessen: Modi, rechte und linke Hand haben je eine
Calls-Kachel.
Die Pruefung steht jetzt andersherum: Die Calls-Seite MUSS stehen
bleiben. Dieselbe Zeile schuetzt damit das, was sie vorher verboten
hat -- und wird rot, wenn die Kachel je wieder verschwindet. Das
Umlenken selbst bleibt geprueft (Scouting, zweimal).
Das ist die DRITTE stille rote Pruefung an einem Tag (nach
pruef-modi-wortleck und pruef-zuteilung). Die Frage an Filipe, ob ein
naechtlicher Lauf sie selbst anstossen soll, steht in der Vault-Notiz
und wird nicht von mir allein entschieden.
GEPRUEFT
--------
pruef-modi-katalog: 116 Pruefungen, 0 Fehler (vorher 95).
Neu: die beiden Schalter, wer es erfaehrt (samt Gegenprobe, dass es
nicht einfach alle sind: 2 von 6), und der Wortlaut an acht Proben.
Dabei war meine eigene erste Messung falsch -- sie erwartete eine
Kuerzung bei 50 Zeichen, die nur gilt, wenn eine Notiz danebensteht.
Steht als Begruendung in der Pruefung.
pruef-push-ziel: 11 von 11 (vorher 1 Fehler).
pruef-zuteilung, pruef-push, pruef-push-weg: gruen.
Co-Authored-By: Claude Opus 5 <[email protected]>
940 lines
42 KiB
JavaScript
940 lines
42 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, entscheidetUeberAufgaben,
|
||
istDogFather, istHand,
|
||
sitzungLesen,
|
||
} from "./workspace.js";
|
||
import { TEAM_DOGI_ROLLEN } from "./crew-adresse.js";
|
||
/* WER ES ERFAEHRT -- an einer Stelle fuer beide Bewerbungswege.
|
||
Begruendung samt Messung steht in der Datei selbst. */
|
||
import {
|
||
meldeBewerbung, meldeBewerbungsantwort,
|
||
} from "./workspace-bewerbung-melden.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",
|
||
/* „Beworben" ist kein halbes „Angenommen", sondern ein eigener
|
||
Zustand: Es liegt an jemand anderem, nicht mehr an der Person
|
||
selbst. Der Satz sagt das, statt es offen zu lassen. */
|
||
beworben: "Beworben – wartet auf Antwort",
|
||
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,
|
||
z.entscheid_text, z.entschieden_am,
|
||
p.name AS person_name, p.rolle AS person_rolle,
|
||
pb.name AS bewertet_von_name,
|
||
pe.name AS entschieden_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
|
||
LEFT JOIN personen pe ON pe.id = z.entschieden_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"
|
||
&& z.zustand !== "beworben")?.person_name || null)
|
||
: null;
|
||
|
||
/* DIE OFFENEN BEWERBUNGEN, abgeleitet statt danebengefuehrt.
|
||
Die Oberflaeche soll nicht dieselbe Rechnung noch einmal machen
|
||
-- zwei Rechnungen fuer dieselbe Frage laufen auseinander. */
|
||
/* NACH EINGANG SORTIERT, NICHT NACH NAMEN (22.09.2026).
|
||
|
||
Filipe zu meinem eigenen Satz „wer zuerst Zeit hat — genau das
|
||
geht ja nicht mehr": „falls das nicht mehr geht mach das es
|
||
geht wenn es wieder geht ist alles gut."
|
||
|
||
Er hat recht, und ich hatte es zu schnell aufgegeben. „Wer
|
||
zuerst Zeit hat" muss nicht heissen, dass man sich selbst
|
||
bedient -- es kann genauso heissen, dass die ERSTE BEWERBUNG
|
||
zuerst drankommt. Damit gilt beides: Die Leitung entscheidet
|
||
(sein Wunsch von vorhin), und wer schnell ist, hat den Vorteil
|
||
(sein Wunsch von eben).
|
||
|
||
Die Abfrage weiter oben sortiert nach NAMEN -- gut fuer die
|
||
Liste der Zugeteilten, falsch hier: Nach Alphabet gaebe es
|
||
keinen Vorteil fuer den Schnellen, sondern einen fuer Anna.
|
||
|
||
`geantwortet_am` ist der Zeitpunkt der Bewerbung (beim
|
||
Bewerben gesetzt). Gleichstand gibt es praktisch nicht; falls
|
||
doch, entscheidet die laufende Nummer -- irgendeine Ordnung
|
||
ist besser als eine zufaellige. */
|
||
r.bewerbungen = zu.filter((z) => z.zustand === "beworben")
|
||
.sort((x, y) => String(x.geantwortet_am || "").localeCompare(String(y.geantwortet_am || ""))
|
||
|| (x.id - y.id));
|
||
/* UND OB ICH MICH UEBERHAUPT NOCH BEWERBEN KANN. Ein Knopf, der
|
||
beim Druecken eine Absage bringt, ist schlimmer als keiner. */
|
||
r.ich_beworben = !!(r.meine_zuteilung
|
||
&& r.meine_zuteilung.zustand === "beworben");
|
||
}
|
||
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" };
|
||
|
||
/* NIEMAND TRAEGT SICH SELBST EIN, DER NICHT ENTSCHEIDEN DARF
|
||
(22.09.2026).
|
||
|
||
Ohne diese Zeile waere das Verbot von oben ein Umweg mit einem
|
||
Klick mehr: Die linke Hand darf verteilen -- sie haette sich die
|
||
Aufgabe also einfach selbst zugeteilt und damit genau das getan,
|
||
was Filipe ausgeschlossen hat („keine aufgaben selber uebernehmen
|
||
die ihnen nicht zugetragen sind").
|
||
|
||
DIE ROLLE WIRD NACHGESCHLAGEN statt durchgereicht: `zuteilen`
|
||
bekommt nur eine Nummer, und eine zusaetzliche Angabe im Aufruf
|
||
waere die, die beim dritten Aufrufer fehlt. Eine Abfrage mehr
|
||
kostet nichts und kann nicht vergessen werden.
|
||
|
||
ES WIRD ABGELEHNT UND NICHT STILL HERAUSGEFILTERT: Wer sich
|
||
einträgt und sich danach nicht in der Liste findet, sucht den
|
||
Fehler bei sich. */
|
||
const verteiler = vonId
|
||
? db().prepare("SELECT id, rolle FROM personen WHERE id = ?").get(vonId)
|
||
: null;
|
||
if (verteiler && echt.includes(verteiler.id)
|
||
&& !entscheidetUeberAufgaben(verteiler)) {
|
||
return { fehler: "nicht_an_dich_selbst" };
|
||
}
|
||
|
||
/* 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" });
|
||
|
||
/* BEIM POOL IST „ANNEHMEN" DASSELBE WIE „UEBERNEHMEN" (22.09.2026).
|
||
|
||
Das ist beim Bauen aufgefallen, nicht beim Planen: Der Weg
|
||
`uebernehmen` wurde fuer Modis und die linke Hand geschlossen --
|
||
und `annehmen` stand daneben weit offen. Bei einer Pool-Aufgabe
|
||
heisst annehmen aber genau, sie den anderen wegzunehmen
|
||
(`uebernahmeAbschliessen` weiter unten in dieser Funktion). Ein
|
||
Verbot mit einer zweiten, offenen Tuer ist keins.
|
||
|
||
BEI „einzeln" UND „mehrere" BLEIBT ES, WIE ES WAR. Dort wurde
|
||
die Aufgabe jemandem ZUGETRAGEN -- und genau die darf er
|
||
annehmen. Filipes Satz nennt den Unterschied selbst: „keine
|
||
aufgaben selber uebernehmen DIE IHNEN NICHT ZUGETRAGEN SIND." */
|
||
if (a.verteilart === "pool" && !entscheidetUeberAufgaben(req.person)) {
|
||
return res.status(403).json({ fehler: "erst_bewerben" });
|
||
}
|
||
|
||
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);
|
||
|
||
/* DIE OFFENEN BEWERBUNGEN FALLEN AUCH WEG (22.09.2026).
|
||
|
||
Ohne diese Zeile blieb eine Bewerbung auf „beworben" stehen,
|
||
nachdem jemand anders die Aufgabe bekommen hat -- die Person
|
||
haette weiter gewartet, und in der Liste der Leitung staende eine
|
||
Entscheidung an, die es nicht mehr gibt. Gefunden hat es die
|
||
Pruefung.
|
||
|
||
EIGENE ANWEISUNG UND NICHT DIESELBE, und der Grund ist das Feld
|
||
`grund`: Dort stehen bei einer Bewerbung die WORTE DER PERSON
|
||
(„Ich habe Freitag Zeit."). Sie mit „Von jemand anderem
|
||
uebernommen" zu ueberschreiben hiesse, die Frage mit der Antwort
|
||
zu loeschen -- genau das, was die Trennung der zwei Felder
|
||
verhindern soll. Der Hinweis kommt deshalb in `entscheid_text`. */
|
||
db().prepare(`UPDATE aufgaben_zuteilung
|
||
SET zustand = 'abgelehnt',
|
||
entscheid_text = 'Von jemand anderem übernommen',
|
||
entschieden_am = ?, geantwortet_am = ?
|
||
WHERE aufgabe_id = ? AND person_id <> ? AND zustand = 'beworben'`)
|
||
.run(jetzt(), 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" });
|
||
/* WER NICHT ENTSCHEIDET, NIMMT SICH AUCH NICHTS (22.09.2026).
|
||
Filipe: „ueberhaupt ueberall sollen die keine aufgaben selber
|
||
uebernehmen die ihnen nicht zugetragen sind."
|
||
Fuer Modis und die linke Hand fuehrt der Weg jetzt ueber eine
|
||
Bewerbung -- der Fehlercode sagt das, statt nur nein. */
|
||
if (!entscheidetUeberAufgaben(req.person)) {
|
||
return res.status(403).json({ fehler: "erst_bewerben" });
|
||
}
|
||
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" });
|
||
}
|
||
});
|
||
|
||
/* =====================================================================
|
||
SICH BEWERBEN — und wer darueber entscheidet (22.09.2026)
|
||
=====================================================================
|
||
Filipe: „ich will dass die sich fuer aufgaben bewerben koennen aber
|
||
die rechte hand oder dogfather muessen annehmen oder ablehnen koennen
|
||
und das mit einem kommentar als moeglichkeit sogar noch zum
|
||
hinzufuegen."
|
||
|
||
DREI ENTSCHEIDUNGEN, die nicht auf der Hand lagen:
|
||
|
||
(1) DER KOMMENTAR IST FREIWILLIG, die Begruendung beim ABLEHNEN einer
|
||
zugeteilten Aufgabe bleibt Pflicht. Das ist kein Widerspruch:
|
||
Dort sagt jemand ab, der gefragt wurde -- der Fragende muss
|
||
wissen, warum, sonst fragt er nach. Hier entscheidet die
|
||
Leitung ueber eine Bitte; ein „ja" braucht keine Begruendung.
|
||
Filipes Wort ist „als moeglichkeit", nicht „als pflicht".
|
||
|
||
(2) DER TEXT DER BEWERBUNG UND DER KOMMENTAR DER LEITUNG STEHEN IN
|
||
ZWEI FELDERN. `grund` traegt die Worte der Person selbst, der
|
||
Kommentar kommt in `entscheid_text`. In dasselbe Feld waere
|
||
bequemer und wuerde die Frage mit der Antwort ueberschreiben --
|
||
danach weiss niemand mehr, worum jemand gebeten hat.
|
||
|
||
(3) WER SICH BEWIRBT, IST NICHT ZUGETEILT. Es entsteht eine Zeile
|
||
mit `zustand = 'beworben'` und ohne `zugeteilt_von` -- sonst
|
||
staende in der Liste, jemand haette ihm die Aufgabe gegeben.
|
||
===================================================================== */
|
||
|
||
const BEWERBUNG_TEXT_MAX = 500;
|
||
|
||
/* ---- Sich bewerben --------------------------------------------------- */
|
||
zuteilungRouter.post("/workspace/api/aufgaben/:id/bewerben", (req, res) => {
|
||
try {
|
||
const a = holen(req, res); if (!a) return;
|
||
|
||
/* WER SELBST ENTSCHEIDET, BEWIRBT SICH NICHT. Er teilt sich die
|
||
Aufgabe zu -- eine Bewerbung an sich selbst waere ein Umweg mit
|
||
zwei Klicks und demselben Ergebnis. */
|
||
if (entscheidetUeberAufgaben(req.person)) {
|
||
return res.status(409).json({ fehler: "du_entscheidest_selbst" });
|
||
}
|
||
if (a.status === "erledigt" || a.status === "abgebrochen") {
|
||
return res.status(409).json({ fehler: "schon_erledigt" });
|
||
}
|
||
|
||
const z = meine(a.id, req.person.id);
|
||
if (z && z.zustand === "beworben") {
|
||
return res.status(409).json({ fehler: "schon_beworben" });
|
||
}
|
||
/* WER SIE SCHON HAT, BEWIRBT SICH NICHT -- mit einer Ausnahme,
|
||
und die ist der Kern der Sache:
|
||
|
||
einzeln/mehrere, Zustand „offen" -> sie wurde IHM zugetragen.
|
||
Er nimmt sie an oder lehnt
|
||
sie ab. Keine Bewerbung.
|
||
pool, Zustand „offen" -> sie wurde ANGEBOTEN, nicht
|
||
zugetragen. Frueher hiess
|
||
die Antwort darauf „ich
|
||
nehme sie mir"; seit heute
|
||
heisst sie „ich bewerbe
|
||
mich".
|
||
|
||
Ohne diese Unterscheidung haette der erlaubte Weg fuer genau die
|
||
Aufgaben nicht funktioniert, fuer die er gedacht ist. Gefunden
|
||
hat es die Pruefung, nicht das Nachdenken. */
|
||
const angeboten = z && z.zustand === "offen" && a.verteilart === "pool";
|
||
if (z && !angeboten && z.zustand !== "abgelehnt") {
|
||
return res.status(409).json({ fehler: "schon_zugeteilt" });
|
||
}
|
||
|
||
/* HAT SIE SCHON JEMAND? Dann ist eine Bewerbung eine Enttaeuschung
|
||
mit Anlauf. Dieselbe Frage wie beim Uebernehmen, damit beide
|
||
Wege dasselbe sagen. */
|
||
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 && a.verteilart === "pool") {
|
||
return res.status(409).json({ fehler: "schon_vergeben", wer: schon.name });
|
||
}
|
||
|
||
const text = String(req.body?.text ?? "").trim().slice(0, BEWERBUNG_TEXT_MAX);
|
||
const nun = jetzt();
|
||
|
||
if (z) {
|
||
db().prepare(`UPDATE aufgaben_zuteilung
|
||
SET zustand = 'beworben', grund = ?, geantwortet_am = ?,
|
||
entscheid_text = NULL, entschieden_von = NULL, entschieden_am = NULL
|
||
WHERE id = ?`).run(text || null, nun, z.id);
|
||
} else {
|
||
/* `zugeteilt_von` bleibt leer: Niemand hat sie ihm gegeben. */
|
||
db().prepare(`INSERT INTO aufgaben_zuteilung
|
||
(aufgabe_id, person_id, zustand, grund, zugeteilt_am, geantwortet_am)
|
||
VALUES (?,?,'beworben',?,?,?)`)
|
||
.run(a.id, req.person.id, text || null, nun, nun);
|
||
}
|
||
|
||
/* ==== UND JETZT ERFAEHRT ES AUCH JEMAND (23.09.2026) =========
|
||
|
||
Bis heute endete eine Bewerbung hier: gespeichert, protokolliert
|
||
-- und still. DogFather erfuhr davon nur, wenn er von sich aus
|
||
das Brett aufmachte. Wer sich bewirbt, wartet aber; Warten ohne
|
||
Rueckmeldung fuehlt sich nach zwei Tagen an wie "interessiert
|
||
keinen", und genau das soll eine Bewerbung verhindern.
|
||
|
||
NICHT ABWARTEN (`void`): Ob die Meldung durchgeht, haengt am
|
||
Push-Dienst, an der Ruhezeit und an den Einstellungen des
|
||
Empfaengers. Nichts davon darf entscheiden, ob die Bewerbung
|
||
selbst gespeichert ist -- die ist es laengst. */
|
||
void meldeBewerbung({
|
||
bewerber: { id: req.person.id, name: req.person.name },
|
||
titel: a.titel,
|
||
wort: text,
|
||
ziel: `/workspace/aufgaben.html?aufgabe=${a.id}`,
|
||
});
|
||
|
||
protokolliere("aufgabe_beworben", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
detail: `#${a.id} ${a.titel}`.slice(0, 120),
|
||
});
|
||
res.json({ ok: true, zustand: "beworben" });
|
||
} catch (f) {
|
||
console.error("[zuteilung] bewerben:", f?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/* ---- Eine Bewerbung zuruecknehmen ------------------------------------ */
|
||
zuteilungRouter.post("/workspace/api/aufgaben/:id/bewerbung-zurueck", (req, res) => {
|
||
try {
|
||
const a = holen(req, res); if (!a) return;
|
||
const z = meine(a.id, req.person.id);
|
||
if (!z || z.zustand !== "beworben") {
|
||
return res.status(409).json({ fehler: "keine_bewerbung" });
|
||
}
|
||
/* GANZ WEG, nicht „zurueckgezogen". Wer es sich anders ueberlegt,
|
||
hat nichts getan, was jemand nachlesen muesste -- und eine Zeile,
|
||
die nur noch sagt „hier stand mal eine Bewerbung", waere in der
|
||
Liste der Leitung Laerm. */
|
||
db().prepare("DELETE FROM aufgaben_zuteilung WHERE id = ?").run(z.id);
|
||
protokolliere("bewerbung_zurueckgezogen", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
detail: `#${a.id} ${a.titel}`.slice(0, 120),
|
||
});
|
||
res.json({ ok: true });
|
||
} catch (f) {
|
||
console.error("[zuteilung] bewerbung-zurueck:", f?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/* ---- Ueber eine Bewerbung entscheiden -------------------------------- */
|
||
for (const [weg, entscheidung] of [["annehmen", "ja"], ["ablehnen", "nein"]]) {
|
||
zuteilungRouter.post(`/workspace/api/aufgaben/:id/bewerbung/${weg}`, (req, res) => {
|
||
try {
|
||
const a = holen(req, res); if (!a) return;
|
||
/* NUR DIE RECHTE HAND UND DOGFATHER. Die linke Hand darf
|
||
verteilen, aber nicht ueber Bewerbungen entscheiden -- sie
|
||
bewirbt sich ja selbst. */
|
||
if (!entscheidetUeberAufgaben(req.person)) {
|
||
return res.status(403).json({ fehler: "nicht_erlaubt" });
|
||
}
|
||
|
||
const wer = Number(req.body?.person_id);
|
||
if (!Number.isInteger(wer)) return res.status(400).json({ fehler: "ungueltig" });
|
||
|
||
const z = db().prepare(`SELECT * FROM aufgaben_zuteilung
|
||
WHERE aufgabe_id = ? AND person_id = ?`).get(a.id, wer);
|
||
if (!z || z.zustand !== "beworben") {
|
||
return res.status(409).json({ fehler: "keine_bewerbung" });
|
||
}
|
||
|
||
const kommentar = String(req.body?.kommentar ?? "").trim()
|
||
.slice(0, BEWERBUNG_TEXT_MAX) || null;
|
||
const nun = jetzt();
|
||
|
||
if (entscheidung === "ja") {
|
||
/* HAT INZWISCHEN JEMAND ANDERES? Die Frage muss VOR dem
|
||
Schreiben stehen -- sonst sagen zwei Zusagen im selben
|
||
Moment beide „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, wer);
|
||
if (schon && a.verteilart === "pool") {
|
||
return res.status(409).json({ fehler: "schon_vergeben", wer: schon.name });
|
||
}
|
||
db().prepare(`UPDATE aufgaben_zuteilung
|
||
SET zustand = 'angenommen', geantwortet_am = ?,
|
||
entscheid_text = ?, entschieden_von = ?, entschieden_am = ?
|
||
WHERE id = ?`).run(nun, kommentar, req.person.id, nun, z.id);
|
||
if (a.verteilart === "pool") uebernahmeAbschliessen(a.id, wer);
|
||
else {
|
||
db().prepare("UPDATE aufgaben SET verantwortlich_id = ? WHERE id = ?")
|
||
.run(wer, a.id);
|
||
}
|
||
} else {
|
||
db().prepare(`UPDATE aufgaben_zuteilung
|
||
SET zustand = 'abgelehnt', geantwortet_am = ?,
|
||
entscheid_text = ?, entschieden_von = ?, entschieden_am = ?
|
||
WHERE id = ?`).run(nun, kommentar, req.person.id, nun, z.id);
|
||
}
|
||
|
||
/* DIE ANTWORT IST DER WICHTIGERE TEIL. Eine Absage, die man
|
||
drei Tage spaeter zufaellig entdeckt, ist schlimmer als eine
|
||
sofortige -- und die NOTIZ geht mit, denn genau dafuer hat
|
||
Filipe sie verlangt ("mit einem text als notiz"). Sie erst zu
|
||
verlangen und dann an der Stelle zu verschweigen, an der man
|
||
sie liest, waere die halbe Funktion. */
|
||
void meldeBewerbungsantwort({
|
||
anWen: wer,
|
||
titel: a.titel,
|
||
ja: entscheidung === "ja",
|
||
notiz: kommentar,
|
||
ziel: `/workspace/aufgaben.html?aufgabe=${a.id}`,
|
||
});
|
||
|
||
protokolliere(entscheidung === "ja" ? "bewerbung_angenommen" : "bewerbung_abgelehnt", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
detail: `#${a.id} fuer ${wer}`.slice(0, 120),
|
||
});
|
||
res.json({ ok: true, zustand: entscheidung === "ja" ? "angenommen" : "abgelehnt" });
|
||
} catch (f) {
|
||
console.error(`[zuteilung] bewerbung/${weg}:`, 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" });
|
||
}
|
||
});
|