Annehmen war bisher nur ein Etikett: Man stellte den Status um, und es passierte nichts. Kunde, Projekt, Aufgabenliste, Termin und Zugang musste man danach von Hand in fünf Schritten nachbauen. Der alte Code gab das im Kommentar selbst zu -- schlug der zweite von zwei Aufrufen fehl, stand der Kunde schon in der Datenbank und man durfte es nicht noch einmal versuchen. Jetzt macht das EIN Aufruf, ganz oder gar nicht: Kunde finden oder anlegen, Projekt anlegen, die komplette Aufgabenliste des Pakets einsetzen, Liefertermin berechnen, Einladungslink erzeugen. Der Kunde verfolgt ab diesem Moment alles in seinem Portal. Die Zeit läuft wirklich: - Eigenes Datumsfeld (termin_am) neben dem freien Text. Aus 'Mitte Oktober' kann man keine verbleibenden Tage rechnen. - Werktage statt Kalendertage, inklusive luxemburgischer Feiertage. Die beweglichen werden über die Osterformel berechnet statt gepflegt -- eine Liste ist im übernächsten Jahr lautlos falsch. - Vorschlag je Paket (Onepager 10, Website 20, Shop 30 Werktage), überschreibbar vor dem Bestätigen. - Verbleibende Zeit wird SERVERSEITIG gerechnet. Der Browser kennt die Feiertage nicht; zwei verschiedene Zahlen für denselben Termin wären schlimmer als gar keine. Ablehnen mit Grund und vorbereiteter Absage in fünf Sprachen, Text serverseitig erzeugt und vor dem Abschicken lesbar. Ein laufendes Projekt lässt sich nicht nachträglich als Anfrage ablehnen. Sechs Fehler dabei gefunden: - Ein unbekanntes Paket hätte GAR KEINEN Termin bekommen statt des Ersatzwerts. Zwei Funktionen lasen dieselbe Tabelle, nur eine hatte einen Rückfallwert. Eine fehlende Zahl fällt nirgends auf. - wd_anfragen hat keine Spalte 'firma' -- better-sqlite3 weist undefined ab, die ganze Annahme wäre gescheitert. - Migrationen stehen in einer ausdrücklichen Liste; 0015 fehlte darin. - datumKurz() wurde aufgerufen, gab es aber nicht. Der Fehler wäre erst NACH dem Anlegen aufgetreten. - Der Installations-Hinweis lag fest über dem Annehmen-Knopf und machte ihn auf dem Handy untreffbar. Die Seite reserviert jetzt Platz dafür. - 'richttermin' ist freier Text, lief aber durch einen Datumsformatierer: Der Kunde las 'Invalid Date' an der wichtigsten Stelle seines Projekts. Geprüft: 49 Prüfungen der Terminrechnung gegen nachschlagbare Osterdaten und Wochentage, 70 gegen eine echte Datenbank (darunter: zweiter Klick legt nichts doppelt an, Abbruch mittendrin lässt NICHTS zurück, gesperrter Kunde wird nicht still entsperrt), 42 im Browser auf Computer und Handy, 40 im Portal. Alle bestehenden Prüfungen weiter grün. Co-Authored-By: Claude Opus 5 <[email protected]>
427 lines
17 KiB
JavaScript
427 lines
17 KiB
JavaScript
/* =====================================================================
|
|
routes/webdesign-aufgaben.js — "Was ist schon gemacht von dem, was im
|
|
Plan war?"
|
|
|
|
Wunsch vom 22.08.2026. Bis hierher sah der Kunde nur die Phase
|
|
("Gestaltung · 3/7") — also eine Zahl ohne Inhalt. Die häufigste
|
|
Rückfrage in jedem Projekt ("wie weit seid ihr denn?") beantwortet
|
|
eine Phasenanzeige nicht.
|
|
|
|
Zwei Regeln durchziehen diese Datei:
|
|
|
|
1. Der Kunde sieht NUR seine eigenen Punkte. Jede Abfrage im
|
|
Kundenteil filtert auf kunde_id aus der SITZUNG — nie auf eine ID
|
|
aus der Anfrage. Eine Projekt-ID im Pfad ist eine Behauptung des
|
|
Aufrufers, die Sitzung ist ein Beweis.
|
|
|
|
2. Der Kunde darf abhaken, was IHM zugewiesen ist ("Texte geliefert"),
|
|
und sonst nichts. Das ist kein Misstrauen, sondern Klarheit: Wenn
|
|
beide Seiten alles abhaken könnten, wäre am Ende unklar, wer was
|
|
bestätigt hat — und genau diese Klarheit ist der Zweck der Liste.
|
|
===================================================================== */
|
|
|
|
import { db } from "../db.js";
|
|
import { json } from "../lib/http.js";
|
|
import { requireSession } from "../lib/auth.js";
|
|
import { hasPermission } from "../lib/permissions.js";
|
|
import { ausweisPruefen } from "../lib/webdesign-ausweis.js";
|
|
import { brauchtKunde } from "./webdesign-portal.js";
|
|
import { jetzt, neueId, text, verlauf } from "../lib/webdesign-helfer.js";
|
|
|
|
export const KATEGORIEN = ["start", "gestaltung", "umsetzung", "abschluss"];
|
|
export const AUFGABE_STATUS = ["offen", "in_arbeit", "erledigt", "entfaellt"];
|
|
const WER_DRAN = ["dogfather", "kunde"];
|
|
|
|
/* Gleiche Zugangsprüfung wie in webdesign-kunden.js — Ausweis der
|
|
Zugangswand ODER Team-Anmeldung mit Einzelrecht. Begründung dort. */
|
|
async function darf(req, recht) {
|
|
const ausweis = ausweisPruefen(req);
|
|
if (ausweis) return { erlaubt: true, sitzung: { actor: ausweis.rolle, isOwner: true } };
|
|
const sitzung = await requireSession(req);
|
|
if (!sitzung) return { erlaubt: false, grund: 401, sitzung: null };
|
|
if (sitzung.isOwner) return { erlaubt: true, sitzung };
|
|
const ok = hasPermission(sitzung.permissions, sitzung.overrides, recht);
|
|
return { erlaubt: ok, grund: ok ? null : 403, sitzung };
|
|
}
|
|
|
|
function werIst(p) {
|
|
return p.sitzung?.actor ? "team:" + p.sitzung.actor : "team";
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
ZUSAMMENFASSUNG — die Zahlen, die oben über der Liste stehen
|
|
|
|
Absichtlich hier und nicht im Browser gerechnet: Verwaltung und Portal
|
|
brauchen dieselben Zahlen. Zweimal gerechnet heißt irgendwann
|
|
zweierlei Ergebnisse, und dann diskutiert man über Prozentwerte statt
|
|
über das Projekt.
|
|
|
|
"entfaellt" zählt NICHT in den Nenner. Ein Punkt, der wegfällt, darf
|
|
den Fortschritt weder bremsen noch künstlich beschleunigen.
|
|
--------------------------------------------------------------------- */
|
|
export function fortschritt(projektId) {
|
|
const zeilen = db
|
|
.prepare(`SELECT kategorie, status, wer_dran FROM wd_aufgaben WHERE projekt_id = ?`)
|
|
.all(projektId);
|
|
|
|
const gesamt = zeilen.filter((z) => z.status !== "entfaellt");
|
|
const erledigt = gesamt.filter((z) => z.status === "erledigt");
|
|
const jeKategorie = {};
|
|
for (const k of KATEGORIEN) {
|
|
const inK = gesamt.filter((z) => z.kategorie === k);
|
|
jeKategorie[k] = {
|
|
gesamt: inK.length,
|
|
erledigt: inK.filter((z) => z.status === "erledigt").length,
|
|
inArbeit: inK.filter((z) => z.status === "in_arbeit").length,
|
|
};
|
|
}
|
|
|
|
return {
|
|
gesamt: gesamt.length,
|
|
erledigt: erledigt.length,
|
|
inArbeit: gesamt.filter((z) => z.status === "in_arbeit").length,
|
|
/* Die wichtigste Zahl im Portal: offene Punkte, die auf den KUNDEN
|
|
warten. Steht sie auf 0, kann er beruhigt sein — steht sie höher,
|
|
weiß er sofort, dass es an ihm liegt. */
|
|
beimKunden: gesamt.filter((z) => z.wer_dran === "kunde" && z.status !== "erledigt").length,
|
|
prozent: gesamt.length ? Math.round((erledigt.length / gesamt.length) * 100) : 0,
|
|
jeKategorie,
|
|
};
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
KUNDENSEITE
|
|
--------------------------------------------------------------------- */
|
|
|
|
/* GET /webdesign/portal/projekt/:id/aufgaben */
|
|
export function portalAufgaben(req, res) {
|
|
const kunde = brauchtKunde(req, res);
|
|
if (!kunde) return;
|
|
|
|
/* Das UND ist die ganze Absicherung. Ohne kunde_id in dieser Abfrage
|
|
könnte jeder angemeldete Kunde die Aufgabenliste jedes anderen
|
|
Projekts lesen, indem er die ID im Pfad austauscht. */
|
|
const projekt = db
|
|
.prepare(`SELECT id, nummer, titel, paket, status FROM wd_projekte WHERE id = ? AND kunde_id = ?`)
|
|
.get(String(req.params.id || ""), kunde.id);
|
|
if (!projekt) return json(res, { ok: false, error: "Projekt nicht gefunden." }, 404);
|
|
|
|
const aufgaben = db
|
|
.prepare(
|
|
`SELECT id, titel, beschreibung, kategorie, status, wer_dran,
|
|
nicht_enthalten, hinweis, reihenfolge, erledigt_am
|
|
FROM wd_aufgaben WHERE projekt_id = ? ORDER BY reihenfolge, erstellt_am`
|
|
)
|
|
.all(projekt.id);
|
|
|
|
return json(res, { ok: true, projekt, aufgaben, fortschritt: fortschritt(projekt.id) });
|
|
}
|
|
|
|
/* POST /webdesign/portal/aufgabe/:id/erledigt
|
|
|
|
Der Kunde meldet einen eigenen Punkt als erledigt ("Texte geliefert").
|
|
Drei Bedingungen, alle notwendig: sein Projekt, wer_dran = kunde,
|
|
nicht als "nicht enthalten" markiert. */
|
|
export function portalAufgabeErledigt(req, res) {
|
|
const kunde = brauchtKunde(req, res);
|
|
if (!kunde) return;
|
|
|
|
const aufgabe = db
|
|
.prepare(`SELECT * FROM wd_aufgaben WHERE id = ? AND kunde_id = ?`)
|
|
.get(String(req.params.id || ""), kunde.id);
|
|
if (!aufgabe) return json(res, { ok: false, error: "Punkt nicht gefunden." }, 404);
|
|
|
|
if (aufgabe.wer_dran !== "kunde") {
|
|
return json(
|
|
res,
|
|
{ ok: false, error: "Diesen Punkt erledigt Dogfather — du musst hier nichts tun." },
|
|
403
|
|
);
|
|
}
|
|
if (aufgabe.nicht_enthalten) {
|
|
return json(res, { ok: false, error: "Dieser Punkt gehört nicht zum Umfang." }, 403);
|
|
}
|
|
|
|
/* Der Kunde darf umschalten, nicht nur setzen. Ein Fehlklick soll ihn
|
|
nicht zwingen, deswegen zu schreiben. */
|
|
const neu = aufgabe.status === "erledigt" ? "offen" : "erledigt";
|
|
db.prepare(
|
|
`UPDATE wd_aufgaben SET status = ?, erledigt_am = ?, aktualisiert_am = ? WHERE id = ?`
|
|
).run(neu, neu === "erledigt" ? jetzt() : null, jetzt(), aufgabe.id);
|
|
|
|
verlauf("aufgabe", aufgabe.id, "kunde_" + neu, "kunde:" + kunde.id, aufgabe.titel);
|
|
return json(res, { ok: true, status: neu, fortschritt: fortschritt(aufgabe.projekt_id) });
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
VERWALTUNGSSEITE
|
|
--------------------------------------------------------------------- */
|
|
|
|
/* GET /webdesign/admin/projekte/:id/aufgaben */
|
|
export async function adminAufgaben(req, res) {
|
|
const p = await darf(req, "WD_PROJEKTE_MANAGE");
|
|
if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund);
|
|
|
|
const projektId = String(req.params.id || "");
|
|
const projekt = db
|
|
.prepare(
|
|
`SELECT p.id, p.nummer, p.titel, p.paket, p.status, p.kunde_id, k.name AS kunde_name
|
|
FROM wd_projekte p JOIN wd_kunden k ON k.id = p.kunde_id WHERE p.id = ?`
|
|
)
|
|
.get(projektId);
|
|
if (!projekt) return json(res, { ok: false, error: "Projekt nicht gefunden." }, 404);
|
|
|
|
const aufgaben = db
|
|
.prepare(`SELECT * FROM wd_aufgaben WHERE projekt_id = ? ORDER BY reihenfolge, erstellt_am`)
|
|
.all(projektId);
|
|
|
|
return json(res, { ok: true, projekt, aufgaben, fortschritt: fortschritt(projektId) });
|
|
}
|
|
|
|
/* GET /webdesign/admin/projekte/:id/alles — das ganze Projekt
|
|
|
|
Warum ein zusätzlicher Endpunkt neben adminAufgaben: Die Verwaltung
|
|
konnte ein Projekt bisher nur ANLEGEN. Ändern, Änderungswünsche
|
|
beziffern und im Projekt antworten ging serverseitig längst — es gab
|
|
nur keine Oberfläche dafür.
|
|
|
|
Das war die schmerzhafteste Lücke im ganzen Bereich: Der Kunde sieht
|
|
in seinem Portal ganz oben die Phase und den "nächsten Schritt". Beide
|
|
liessen sich nirgends ändern. Ein Projekt blieb also für immer im
|
|
Briefing stehen, egal wie weit es wirklich war.
|
|
|
|
Alles in EINER Antwort statt in fünf: Beim Öffnen eines Projekts
|
|
braucht man ohnehin alles, und fünf Anfragen hintereinander bauen die
|
|
Ansicht sichtbar ruckelnd auf. */
|
|
export async function adminProjektAlles(req, res) {
|
|
const p = await darf(req, "WD_PROJEKTE_MANAGE");
|
|
if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund);
|
|
|
|
const id = String(req.params.id || "");
|
|
const projekt = db
|
|
.prepare(
|
|
`SELECT p.*, k.name AS kunde_name, k.firma AS kunde_firma, k.email AS kunde_email
|
|
FROM wd_projekte p JOIN wd_kunden k ON k.id = p.kunde_id WHERE p.id = ?`
|
|
)
|
|
.get(id);
|
|
if (!projekt) return json(res, { ok: false, error: "Projekt nicht gefunden." }, 404);
|
|
|
|
const aufgaben = db
|
|
.prepare(`SELECT * FROM wd_aufgaben WHERE projekt_id = ? ORDER BY reihenfolge, erstellt_am`)
|
|
.all(id);
|
|
|
|
const aenderungen = db
|
|
.prepare(`SELECT * FROM wd_aenderungen WHERE projekt_id = ? ORDER BY erstellt_am DESC`)
|
|
.all(id);
|
|
|
|
const nachrichten = db
|
|
.prepare(`SELECT * FROM wd_nachrichten WHERE projekt_id = ? ORDER BY erstellt_am ASC`)
|
|
.all(id);
|
|
|
|
return json(res, {
|
|
ok: true,
|
|
projekt,
|
|
aufgaben,
|
|
aenderungen,
|
|
nachrichten,
|
|
fortschritt: fortschritt(id),
|
|
});
|
|
}
|
|
|
|
/* POST /webdesign/admin/projekte/:id/aufgaben — neuen Punkt anlegen */
|
|
export async function adminAufgabeAnlegen(req, res) {
|
|
const p = await darf(req, "WD_PROJEKTE_MANAGE");
|
|
if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund);
|
|
|
|
const projekt = db
|
|
.prepare(`SELECT id, kunde_id FROM wd_projekte WHERE id = ?`)
|
|
.get(String(req.params.id || ""));
|
|
if (!projekt) return json(res, { ok: false, error: "Projekt nicht gefunden." }, 404);
|
|
|
|
const titel = text(req.body?.titel, 200);
|
|
if (!titel) return json(res, { ok: false, error: "Bitte einen Titel angeben." }, 400);
|
|
|
|
const kategorie = KATEGORIEN.includes(req.body?.kategorie) ? req.body.kategorie : "umsetzung";
|
|
const werDran = WER_DRAN.includes(req.body?.werDran) ? req.body.werDran : "dogfather";
|
|
|
|
/* Neue Punkte landen am Ende ihrer Kategorie, nicht am Ende der Liste.
|
|
Sonst rutscht ein nachgetragener Gestaltungspunkt hinter den
|
|
Abschluss und die Liste erzählt eine falsche Reihenfolge. */
|
|
const basis = { start: 0, gestaltung: 100, umsetzung: 200, abschluss: 800 }[kategorie];
|
|
const maxInK = db
|
|
.prepare(
|
|
`SELECT MAX(reihenfolge) AS m FROM wd_aufgaben WHERE projekt_id = ? AND kategorie = ?`
|
|
)
|
|
.get(projekt.id, kategorie).m;
|
|
const reihenfolge = (maxInK != null ? maxInK : basis) + 10;
|
|
|
|
const id = neueId();
|
|
db.prepare(
|
|
`INSERT INTO wd_aufgaben
|
|
(id, projekt_id, kunde_id, titel, beschreibung, kategorie, status, wer_dran,
|
|
nicht_enthalten, hinweis, reihenfolge, erstellt_am)
|
|
VALUES (?,?,?,?,?,?,'offen',?,?,?,?,?)`
|
|
).run(
|
|
id, projekt.id, projekt.kunde_id, titel, text(req.body?.beschreibung, 600) || null,
|
|
kategorie, werDran, req.body?.nichtEnthalten ? 1 : 0,
|
|
text(req.body?.hinweis, 300) || null, reihenfolge, jetzt()
|
|
);
|
|
|
|
verlauf("aufgabe", id, "angelegt", werIst(p), titel);
|
|
return json(res, { ok: true, id, reihenfolge, fortschritt: fortschritt(projekt.id) });
|
|
}
|
|
|
|
/* POST /webdesign/admin/aufgaben/:id — Punkt ändern */
|
|
export async function adminAufgabeAendern(req, res) {
|
|
const p = await darf(req, "WD_PROJEKTE_MANAGE");
|
|
if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund);
|
|
|
|
const aufgabe = db.prepare(`SELECT * FROM wd_aufgaben WHERE id = ?`).get(String(req.params.id || ""));
|
|
if (!aufgabe) return json(res, { ok: false, error: "Punkt nicht gefunden." }, 404);
|
|
|
|
const felder = [];
|
|
const werte = [];
|
|
const b = req.body || {};
|
|
|
|
if (typeof b.titel === "string") {
|
|
const t = text(b.titel, 200);
|
|
if (!t) return json(res, { ok: false, error: "Der Titel darf nicht leer sein." }, 400);
|
|
felder.push("titel = ?"); werte.push(t);
|
|
}
|
|
if (typeof b.beschreibung === "string") {
|
|
felder.push("beschreibung = ?"); werte.push(text(b.beschreibung, 600) || null);
|
|
}
|
|
if (typeof b.hinweis === "string") {
|
|
felder.push("hinweis = ?"); werte.push(text(b.hinweis, 300) || null);
|
|
}
|
|
if (KATEGORIEN.includes(b.kategorie)) {
|
|
felder.push("kategorie = ?"); werte.push(b.kategorie);
|
|
}
|
|
if (WER_DRAN.includes(b.werDran)) {
|
|
felder.push("wer_dran = ?"); werte.push(b.werDran);
|
|
}
|
|
if (b.nichtEnthalten !== undefined) {
|
|
felder.push("nicht_enthalten = ?"); werte.push(b.nichtEnthalten ? 1 : 0);
|
|
}
|
|
if (AUFGABE_STATUS.includes(b.status)) {
|
|
felder.push("status = ?"); werte.push(b.status);
|
|
/* erledigt_am wird beim Zurücksetzen geleert. Bliebe das Datum
|
|
stehen, zeigte die Liste später "erledigt am 3. März" bei einem
|
|
offenen Punkt — ein Widerspruch, den niemand mehr aufklärt. */
|
|
felder.push("erledigt_am = ?"); werte.push(b.status === "erledigt" ? jetzt() : null);
|
|
}
|
|
if (Number.isInteger(b.reihenfolge)) {
|
|
felder.push("reihenfolge = ?"); werte.push(b.reihenfolge);
|
|
}
|
|
|
|
if (!felder.length) return json(res, { ok: false, error: "Nichts zu ändern." }, 400);
|
|
|
|
felder.push("aktualisiert_am = ?"); werte.push(jetzt());
|
|
werte.push(aufgabe.id);
|
|
db.prepare(`UPDATE wd_aufgaben SET ${felder.join(", ")} WHERE id = ?`).run(...werte);
|
|
|
|
verlauf("aufgabe", aufgabe.id, "geaendert", werIst(p), aufgabe.titel);
|
|
return json(res, { ok: true, fortschritt: fortschritt(aufgabe.projekt_id) });
|
|
}
|
|
|
|
/* POST /webdesign/admin/aufgaben/:id/loeschen
|
|
|
|
Echtes Löschen, kein Archivieren — anders als bei Kunden und Anfragen.
|
|
Begründung: Ein versehentlich angelegter Punkt ist Arbeitsmaterial,
|
|
keine Geschäftsunterlage. Ein archivierter, aber unsichtbarer Punkt
|
|
wäre hier nur Ballast. Der Verlauf hält fest, dass es ihn gab. */
|
|
export async function adminAufgabeLoeschen(req, res) {
|
|
const p = await darf(req, "WD_PROJEKTE_MANAGE");
|
|
if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund);
|
|
|
|
const aufgabe = db.prepare(`SELECT * FROM wd_aufgaben WHERE id = ?`).get(String(req.params.id || ""));
|
|
if (!aufgabe) return json(res, { ok: false, error: "Punkt nicht gefunden." }, 404);
|
|
|
|
db.prepare(`DELETE FROM wd_aufgaben WHERE id = ?`).run(aufgabe.id);
|
|
verlauf("aufgabe", aufgabe.id, "geloescht", werIst(p), aufgabe.titel);
|
|
return json(res, { ok: true, fortschritt: fortschritt(aufgabe.projekt_id) });
|
|
}
|
|
|
|
/* POST /webdesign/admin/projekte/:id/aufgaben/vorlage
|
|
|
|
Vorlage nachträglich einspielen — für Projekte, die vor dieser
|
|
Funktion angelegt wurden, oder wenn eine Liste versehentlich geleert
|
|
wurde. Fügt HINZU statt zu ersetzen: Vorhandene Punkte könnten schon
|
|
abgehakt sein, und ein Ersetzen würde diesen Fortschritt löschen. */
|
|
/**
|
|
* Wendet die Aufgabenvorlage eines Pakets auf ein Projekt an.
|
|
*
|
|
* Herausgeloest aus dem Endpunkt, weil die Annahme einer Anfrage
|
|
* dasselbe braucht. Kopiert man den Ablauf statt ihn zu teilen, laufen
|
|
* beide Fassungen mit der Zeit auseinander -- und dann bekommen von
|
|
* Hand angelegte Projekte eine andere Liste als angenommene, ohne dass
|
|
* es jemand merkt.
|
|
*
|
|
* @returns {{neu:number, uebersprungen:number, gesamt:number}}
|
|
*/
|
|
export function vorlageAnwenden(projektId, kundeId, paket) {
|
|
const vorlage = db
|
|
.prepare(
|
|
`SELECT titel, beschreibung, kategorie, wer_dran, reihenfolge
|
|
FROM wd_aufgaben_vorlagen WHERE paket = ? AND aktiv = 1 ORDER BY reihenfolge`
|
|
)
|
|
.all(paket);
|
|
if (!vorlage.length) return { neu: 0, uebersprungen: 0, gesamt: 0 };
|
|
|
|
/* Titel, die schon in der Liste stehen, werden uebersprungen. Sonst
|
|
erzeugt ein zweiter Klick eine doppelte Liste -- und das merkt man
|
|
erst, wenn der Kunde sie sieht. */
|
|
const vorhanden = new Set(
|
|
db.prepare(`SELECT titel FROM wd_aufgaben WHERE projekt_id = ?`).all(projektId)
|
|
.map((z) => z.titel.toLowerCase())
|
|
);
|
|
|
|
const einfuegen = db.prepare(
|
|
`INSERT INTO wd_aufgaben
|
|
(id, projekt_id, kunde_id, titel, beschreibung, kategorie, status, wer_dran, reihenfolge, erstellt_am)
|
|
VALUES (?,?,?,?,?,?,'offen',?,?,?)`
|
|
);
|
|
|
|
let neu = 0;
|
|
db.transaction(() => {
|
|
for (const v of vorlage) {
|
|
if (vorhanden.has(v.titel.toLowerCase())) continue;
|
|
einfuegen.run(neueId(), projektId, kundeId, v.titel, v.beschreibung,
|
|
v.kategorie, v.wer_dran, v.reihenfolge, jetzt());
|
|
neu++;
|
|
}
|
|
})();
|
|
return { neu, uebersprungen: vorlage.length - neu, gesamt: vorlage.length };
|
|
}
|
|
|
|
export async function adminVorlageAnwenden(req, res) {
|
|
const p = await darf(req, "WD_PROJEKTE_MANAGE");
|
|
if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund);
|
|
|
|
const projekt = db
|
|
.prepare(`SELECT id, kunde_id, paket FROM wd_projekte WHERE id = ?`)
|
|
.get(String(req.params.id || ""));
|
|
if (!projekt) return json(res, { ok: false, error: "Projekt nicht gefunden." }, 404);
|
|
|
|
const paket = text(req.body?.paket, 40) || projekt.paket;
|
|
const vorhandeneVorlage = db
|
|
.prepare(
|
|
`SELECT COUNT(*) AS n
|
|
FROM wd_aufgaben_vorlagen WHERE paket = ? AND aktiv = 1`
|
|
)
|
|
.get(paket);
|
|
if (!vorhandeneVorlage || !vorhandeneVorlage.n) {
|
|
return json(res, { ok: false, error: "Für dieses Paket gibt es keine Vorlage." }, 404);
|
|
}
|
|
|
|
const ergebnis = vorlageAnwenden(projekt.id, projekt.kunde_id, paket);
|
|
|
|
verlauf("projekt", projekt.id, "vorlage_angewendet", werIst(p), `${paket}: ${ergebnis.neu} Punkte`);
|
|
return json(res, {
|
|
ok: true,
|
|
neu: ergebnis.neu,
|
|
uebersprungen: ergebnis.uebersprungen,
|
|
fortschritt: fortschritt(projekt.id),
|
|
});
|
|
}
|