diff --git a/server/mess-manager-ziele.mjs b/server/mess-manager-ziele.mjs index 9883cc5a..8a99f418 100644 --- a/server/mess-manager-ziele.mjs +++ b/server/mess-manager-ziele.mjs @@ -229,6 +229,51 @@ await bild("formular-handy", keksScout, 412, 915, async (seite) => { if (knopf) { await knopf.click(); await seite.waitForTimeout(400); } }); +/* ---------- EIN KLICK, UND ES STEHT DRIN ---------------------------- + Der Kern von Filipes Wunsch („so wenig wie moeglich zu tippen"). + Gemessen wird nicht, ob der Knopf da ist, sondern ob danach eine + andere Zahl dasteht -- und ob „Rueckgaengig" sie zurueckholt. */ +{ + const kontext = await browser.newContext({ viewport: { width: 412, height: 915 }, hasTouch: true }); + await kontext.addCookies(keksScout.split("; ").map((k) => { + const [n, ...r] = k.split("="); + return { name: n, value: r.join("="), domain: "127.0.0.1", path: "/" }; + })); + const seite = await kontext.newPage(); + const konsole = []; + seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message)); + seite.on("console", (e) => { + if (e.type() === "error" && !/favicon|net::ERR/i.test(e.text())) konsole.push(e.text().slice(0, 160)); + }); + await seite.goto(`http://127.0.0.1:${PORT}/workspace/manager-ziele.html`, { waitUntil: "networkidle" }); + await seite.waitForTimeout(1200); + + const zahlVon = (schluessel) => seite.evaluate((k) => document + .querySelector(`.mz-zeile[data-aufgabe="${k}"] .mz-zeile__zahl`)?.textContent, schluessel); + + const vorher = await zahlVon("schulung"); + await seite.click('.mz-zeile[data-aufgabe="schulung"] .mz-schnell .knopf'); + await seite.waitForTimeout(900); + const nachher = await zahlVon("schulung"); + const meldung = await seite.evaluate(() => + document.querySelector(".mz-gelungen")?.textContent || "-"); + await seite.screenshot({ path: "bild-manager-ziele-einklick.png" }); + + /* Und zurueck. */ + const zurueckDa = await seite.$(".mz-gelungen__zurueck"); + if (zurueckDa) { await zurueckDa.click(); await seite.waitForTimeout(900); } + const zurueckZahl = await zahlVon("schulung"); + + console.log(""); + console.log("--- Ein Klick, ein Eintrag (412px) ---"); + console.log(` vorher: ${vorher}`); + console.log(` nach EINEM Klick: ${nachher} ${nachher !== vorher ? "" : "— KEINE Aenderung, Befund!"}`); + console.log(` Meldung: ${meldung}`); + console.log(` nach Rueckgaengig: ${zurueckZahl} ${zurueckZahl === vorher ? "(wieder wie vorher)" : "— NICHT zurueck, Befund!"}`); + if (konsole.length) console.log(` KONSOLE: ${konsole.join(" | ")}`); + await kontext.close(); +} + /* ---------- Und die Kachelwand davor -------------------------------- Gemessen wird nicht, wie es aussieht, sondern WAS wirklich gesetzt ist: das Merkmal am Element und der Schatten, den der Browser diff --git a/server/pruef-manager-ziele.mjs b/server/pruef-manager-ziele.mjs index 43986567..f8309d8c 100644 --- a/server/pruef-manager-ziele.mjs +++ b/server/pruef-manager-ziele.mjs @@ -731,6 +731,155 @@ if (heuteNr === 1) { "…alle seine Zeilen stehen auf neutral, keine einzige warnt"); } +/* ===================================================================== + 12b · DER SCHNELL-EINTRAG + ===================================================================== + + Filipe am 02.10.2026: „so wenig wie moeglich zu tippen. fertige + sachen, bereit um ab zu gehen." + + DER WICHTIGSTE FALL IST DIE TEILWEISE ABLEHNUNG. Drei Namen + einwerfen, von denen einer schon dasteht -- wenn dann die ganze + Liste zurueckgewiesen wird, tippt man alles noch einmal. Genau + deshalb wird hier geprueft, dass die anderen zwei durchgehen UND + dass gesagt wird, welcher nicht. + ===================================================================== */ +melde("\n=== Der Schnell-Eintrag ==="); + +/* Ein eigener Mensch dafuer, damit die Zaehlungen der vorherigen + Abschnitte nicht durcheinandergeraten. */ +const SCHNELL_CODE = "CODE-Z90-000001"; +const dbS = new DatabaseSync(process.env.WORKSPACE_DB); +{ + const salz = randomBytes(16).toString("hex"); + const h = scryptSync(SCHNELL_CODE, salz, 64, + { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + dbS.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n," + + " code_kennung, aktiv, erstellt) VALUES (?,?,?,?,?,?,1,?)") + .run("Schnell Schnell", "scout", h, salz, 32768, + createHmac("sha256", kennSchluessel).update(SCHNELL_CODE).digest("hex"), ALT); +} +dbS.close(); +CODES.schnell = SCHNELL_CODE; +kekse.schnell = await anmelden("schnell", "scout"); +braucht(!!kekse.schnell, "der Testzugang fuer den Schnell-Eintrag kam nicht herein"); + +/* ---- Ohne jede Angabe: ein einziger Aufruf, ein Eintrag ---------- */ +const ohneAngabe = await schick("schnell", `${API}/eintraege`, "POST", + { aufgabe: "meeting", datum: HEUTE }); +const ohneAngabeD = await ohneAngabe.json(); +ok(ohneAngabe.status === 201, + `ein Meeting ohne eine einzige Angabe (${ohneAngabe.status})`); +ok(ohneAngabeD.angelegt.length === 1, "genau ein Eintrag entstanden"); +ok(ohneAngabeD.stand.aufgaben.find((a) => a.schluessel === "meeting").zahl === 1, + "der Zaehler steht sofort auf 1 — ohne Dialog, ohne Neuladen"); + +/* ---- Zweimal am selben Tag: Hinweis, keine Sperre ---------------- */ +const zweimal = await schick("schnell", `${API}/eintraege`, "POST", + { aufgabe: "meeting", datum: HEUTE }); +const zweimalD = await zweimal.json(); +ok(zweimal.status === 201, + `ein zweites Meeting am selben Tag wird NICHT verboten (${zweimal.status})` + + " — zwei an einem Tag sind moeglich"); +ok(zweimalD.warnungen?.[0]?.art === "zweimal_am_tag", + "…aber es wird gesagt, dass jetzt zwei dastehen (Doppeltipper am Ein-Klick-Knopf)"); +/* GEGENPROBE: Beim ERSTEN kommt diese Warnung nicht -- sonst waere sie + keine Warnung, sondern ein Begleittext. */ +const erstmal = await schick("schnell", `${API}/eintraege`, "POST", + { aufgabe: "schulung", datum: HEUTE, art: "talk" }); +const erstmalD = await erstmal.json(); +ok(!erstmalD.warnungen?.length, + "beim ersten Eintrag des Tages kommt keine Warnung"); +/* Und aufgeraeumt, damit die Zaehlungen danach stimmen. */ +for (const e of [...zweimalD.angelegt, ...erstmalD.angelegt]) { + await hol("schnell", `${API}/eintrag/${e.id}`, { method: "DELETE" }); +} + +/* ---- Drei Namen in einem Wurf ----------------------------------- */ +const drei = await schick("schnell", `${API}/eintraege`, "POST", + { aufgabe: "creator", datum: HEUTE, namen: ["@eins", "@zwei", "@drei"] }); +const dreiD = await drei.json(); +ok(drei.status === 201, `drei Namen auf einmal (${drei.status})`); +ok(dreiD.angelegt.length === 3, `drei Eintraege entstanden (${dreiD.angelegt.length})`); +ok(dreiD.stand.aufgaben.find((a) => a.schluessel === "creator").zahl === 3, + "…und der Zaehler steht auf 3"); +ok(dreiD.stand.aufgaben.find((a) => a.schluessel === "creator").stufe === "gruen", + "…womit das Ziel in EINEM Vorgang erreicht ist"); + +/* ---- Teilweise Ablehnung: der wichtigste Fall -------------------- */ +const teils = await schick("schnell", `${API}/eintraege`, "POST", + { aufgabe: "creator", datum: HEUTE, namen: ["@eins", "@vier", "@fuenf"] }); +const teilsD = await teils.json(); +ok(teils.status === 201, + `die Liste geht durch, obwohl einer schon dasteht (${teils.status})`); +ok(teilsD.angelegt.length === 2, + `die beiden neuen sind drin (${teilsD.angelegt.length})`); +ok(teilsD.abgelehnt.length === 1, + `der doppelte ist einzeln abgelehnt (${teilsD.abgelehnt.length})`); +ok(teilsD.abgelehnt[0]?.was === "@eins", + `…und es steht da, WELCHER (${teilsD.abgelehnt[0]?.was})`); +ok(String(teilsD.abgelehnt[0]?.text || "").includes("@eins"), + "…im Klartext, nicht als Fehlerschluessel"); + +/* GEGENPROBE: Geht gar nichts durch, ist es kein Erfolg. Ein 201 + ueber eine leere Liste waere eine Zusage, die nicht stimmt. */ +const nichts = await schick("schnell", `${API}/eintraege`, "POST", + { aufgabe: "creator", datum: HEUTE, namen: ["@eins"] }); +ok(nichts.status === 409, + `eine Liste, von der nichts durchgeht, meldet KEINEN Erfolg (${nichts.status})`); + +/* ---- Die Grenze von zwanzig ------------------------------------- */ +const viele = Array.from({ length: 26 }, (_, i) => `@menge${i}`); +const zuviel = await schick("schnell", `${API}/eintraege`, "POST", + { aufgabe: "creator", datum: HEUTE, namen: viele }); +const zuvielD = await zuviel.json(); +ok(zuvielD.angelegt.length === 20, + `aus 26 Namen werden 20 (${zuvielD.angelegt.length}) — ein versehentlich` + + " eingefuegter Textblock legt keine 300 Zeilen an"); +ok(!!zuvielD.zuviel, "…und es wird gesagt, dass gekuerzt wurde"); + +/* ---- Ein ungueltiger Link in einer Liste ------------------------ */ +const linkListe = await schick("schnell", `${API}/eintraege`, "POST", + { aufgabe: "werbung", datum: HEUTE, art: "agentur", + links: ["https://www.tiktok.com/@x/video/1", "kaputt", "https://example.org/v/2"] }); +const linkListeD = await linkListe.json(); +ok(linkListeD.angelegt.length === 2, + `zwei gueltige Links durch (${linkListeD.angelegt.length})`); +ok(linkListeD.abgelehnt.length === 1 && linkListeD.abgelehnt[0].fehler === "link_ungueltig", + "der kaputte einzeln abgelehnt, mit Grund"); + +/* ---- Die Schranke gilt auch hier -------------------------------- */ +const creatorSchnell = await schick("creator", `${API}/eintraege`, "POST", + { aufgabe: "meeting", datum: HEUTE }); +ok(creatorSchnell.status === 404, + `ein Creator kommt auch ueber den Schnellweg nicht hinein (${creatorSchnell.status})`); + +/* ---- Und die Datumsgrenzen ebenfalls ---------------------------- */ +const schnellZukunft = await schick("schnell", `${API}/eintraege`, "POST", + { aufgabe: "meeting", datum: morgen }); +const schnellZukunftD = await schnellZukunft.json(); +ok(schnellZukunft.status === 409 + && schnellZukunftD.abgelehnt?.[0]?.fehler === "datum_zukunft", + `ein Datum in der Zukunft wird auch hier abgelehnt (${schnellZukunft.status})`); + +/* ---- Die zuletzt gewaehlte Art kommt zurueck -------------------- */ +const standSchnell = await (await hol("schnell", `${API}/stand`)).json(); +const werbungZeile = standSchnell.aufgaben.find((a) => a.schluessel === "werbung"); +ok(werbungZeile?.letzteArt === "agentur", + `die zuletzt benutzte Art wird mitgeschickt (${werbungZeile?.letzteArt})` + + " — damit ist sie beim naechsten Mal schon gewaehlt"); +const schulungZeile = standSchnell.aufgaben.find((a) => a.schluessel === "schulung"); +ok(schulungZeile?.letzteArt === "schulung", + `ohne eigenen Eintrag gilt die erste Art als Vorgabe (${schulungZeile?.letzteArt})`); + +/* ---- Rueckgaengig: was der Schnellweg anlegt, nimmt er zurueck --- */ +const zumZuruecknehmen = ohneAngabeD.angelegt[0].id; +const zurueck = await hol("schnell", `${API}/eintrag/${zumZuruecknehmen}`, { method: "DELETE" }); +const zurueckD = await zurueck.json().catch(() => ({})); +ok(zurueck.status === 200, `Rueckgaengig geht (${zurueck.status})`); +ok(zurueckD.stand?.aufgaben.find((a) => a.schluessel === "meeting")?.zahl === 0, + "…und der Zaehler steht wieder bei 0"); + /* ===================================================================== 13 · WER GEHT, NIMMT SEINE ZAHLEN NICHT MIT ===================================================================== diff --git a/server/workspace-manager-ziele.js b/server/workspace-manager-ziele.js index f9f2e83a..037011b2 100644 --- a/server/workspace-manager-ziele.js +++ b/server/workspace-manager-ziele.js @@ -317,6 +317,19 @@ export function standFuer(person, monat = monatJetzt(), heute = tagBerlin()) { sehen; er soll nur nicht dafür gemahnt werden. */ const stufe = pflichtig ? stufeFuer(n, ziel, tag, letzter) : (ziel > 0 && n >= ziel ? "gruen" : "neutral"); + /* WAS DIESE PERSON ZULETZT GEWAEHLT HAT -- abgeleitet aus ihrem + letzten Eintrag, nicht in einer Einstellung gespeichert. + + Zweck: Der Schnell-Eintrag kann die Art vorbelegen, und damit + faellt bei „Werbung" und „Schulung" je ein Handgriff weg. Eine + eigene Spalte „Lieblingsart" waere ein zweiter Bestand, der + pflegebeduerftig ist und veralten kann; der letzte Eintrag + veraltet nie, er ist der letzte Eintrag. */ + const zuletzt = a.arten + ? db().prepare(`SELECT art FROM mz_eintrag + WHERE person_id = ? AND aufgabe = ? AND art IS NOT NULL + ORDER BY id DESC LIMIT 1`).get(person.id, a.schluessel)?.art + : null; return { schluessel: a.schluessel, name: a.name, @@ -324,6 +337,7 @@ export function standFuer(person, monat = monatJetzt(), heute = tagBerlin()) { hinweis: a.hinweis, feld: a.feld, arten: a.arten || null, + letzteArt: zuletzt || a.arten?.[0]?.wert || null, einheit: a.einheit, zahl: n, ziel, @@ -613,6 +627,159 @@ managerZieleRouter.get(`${BASIS}/eintraege`, (req, res) => { } }); +/** Legt EINEN Eintrag an und gibt zurueck, was daraus wurde. + * + * HERAUSGEZOGEN (02.10.2026), weil es den Weg jetzt zweimal gibt: + * einzeln (Dialog) und als Liste (Schnell-Eintrag). Zwei Fassungen + * derselben Pruefungen waeren zwei Fassungen, von denen eine beim + * naechsten Nachschaerfen zurueckbleibt -- und gerade hier geht es + * um Dublettenschutz und Datumsgrenzen, also genau um das, was + * niemand doppelt pflegen sollte. */ +function eintragAnlegen(person, rumpf, monat, heute) { + const gepruefte = eintragPruefen(rumpf, monat, heute); + if (gepruefte.fehler) return { fehler: gepruefte.fehler }; + const w = gepruefte.werte; + + const doppel = dublette(person.id, monat, w); + if (doppel.eigen) { + return { + fehler: "schon_eingetragen", + text: w.name + ? `„${w.name}" steht diesen Monat schon in deiner Liste.` + : "Dieses Video steht diesen Monat schon in deiner Liste.", + }; + } + + /* Der Lead wird nur uebernommen, wenn er wirklich existiert und zu + dieser Person gehoert. Eine Nummer in der Adresse ist kein + Besitznachweis -- dieselbe Luecke, die am 03.09.2026 bei + `darfCreator` steckte. */ + if (w.lead_id) { + const l = db().prepare("SELECT id, scout_id, erstellt_von FROM leads WHERE id = ?") + .get(w.lead_id); + const meiner = l && (l.scout_id === person.id || l.erstellt_von === person.id); + if (!l || (!meiner && !siehtAlles(person))) w.lead_id = null; + } + + const info = db().prepare( + `INSERT INTO mz_eintrag + (person_id, von_name, von_rolle, aufgabe, monat, datum, name, name_klein, + link, link_klein, art, notiz, lead_id, doppelt_bei, erstellt) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run( + person.id, person.name || null, person.rolle || null, + w.aufgabe, monat, w.datum, w.name, w.name_klein, + w.link, w.link_klein, w.art, w.notiz, w.lead_id, + doppel.fremd?.person_id ?? null, jetzt()); + + /* ZWEIMAL AM SELBEN TAG -- ein Hinweis, keine Sperre (02.10.2026). + + Seit es den Ein-Klick-Weg gibt, ist ein Doppeltipper die + wahrscheinlichste Fehleingabe: Der Knopf „Heute eingetragen" + braucht keine Angabe, also merkt man nichts davon. Zwei Meetings + an einem Tag sind aber moeglich -- es zu VERBIETEN waere falsch. + + Also gesagt, was dasteht, und das Zuruecknehmen liegt ohnehin + daneben. Nur fuer die Aufgaben OHNE eigenes Erkennungsmerkmal; + Creator und Link haben ihren Dublettenschutz schon. */ + let tageswarnung = null; + if (!w.name_klein && !w.link_klein) { + const schonHeute = db().prepare( + `SELECT COUNT(*) n FROM mz_eintrag + WHERE person_id = ? AND monat = ? AND aufgabe = ? AND datum = ?`) + .get(person.id, monat, w.aufgabe, w.datum)?.n ?? 0; + if (schonHeute > 1) { + tageswarnung = { + art: "zweimal_am_tag", + text: `Achtung: Für diesen Tag stehen jetzt ${schonHeute}. War das Absicht?`, + }; + } + } + + return { + id: Number(info.lastInsertRowid), + was: w.name || w.link || null, + warnung: doppel.fremd + ? { art: "auch_bei", name: doppel.fremd.name, + text: `Hinweis: ${doppel.fremd.name || "jemand anderes"} hat das diesen Monat auch eingetragen.` } + : tageswarnung, + }; +} + +/* ===================================================================== + DER SCHNELL-EINTRAG (02.10.2026) + ===================================================================== + + Filipe: „will dass es viel einfacher ist. am besten so wenig wie + moeglich zu tippen. fertige sachen, bereit um ab zu gehen." + + GEZAEHLT, WAS EIN EINTRAG VORHER KOSTETE: Knopf „+ Eintragen", + Dialog, Knopf „Speichern" -- zwei Klicks und ein Fenster fuer die + Aussage „ich war heute im Meeting". Bei vier Aufgaben im Monat + sind das acht Klicks und acht Fenster fuer etwas, das keine + einzige Angabe braucht. + + DIESER WEG NIMMT EINE LISTE. Damit ist „drei Creator auf einmal" + ein Vorgang statt drei, und das ist der eigentliche Gewinn: Wer + drei Namen aus Discord kopiert, hat den Monat in einem Zug + erledigt. + + JEDER EINTRAG WIRD EINZELN GEPRUEFT und einzeln beantwortet. Ein + Abbruch der ganzen Liste, weil ein Name schon dasteht, waere die + bequeme und die falsche Loesung: Dann weiss man nicht, welcher der + drei das Problem war, und tippt alles noch einmal. + ===================================================================== */ +managerZieleRouter.post(`${BASIS}/eintraege`, (req, res) => { + try { + const heute = tagBerlin(); + const monat = monatJetzt(); + monatSichern(monat); + + const vorlage = req.body || {}; + const liste = []; + const namen = Array.isArray(vorlage.namen) ? vorlage.namen : null; + const links = Array.isArray(vorlage.links) ? vorlage.links : null; + + /* HOECHSTENS ZWANZIG AUF EINMAL. Nicht aus Angst vor Last -- + zwanzig Zeilen sind nichts --, sondern weil ein versehentlich + eingefuegter Textblock sonst dreihundert Eintraege erzeugt, + die jemand einzeln wieder loeschen muss. */ + const GRENZE = 20; + if (namen) { + for (const n of namen.slice(0, GRENZE)) liste.push({ ...vorlage, name: n }); + } else if (links) { + for (const l of links.slice(0, GRENZE)) liste.push({ ...vorlage, link: l }); + } else { + liste.push({ ...vorlage }); + } + if (!liste.length) return res.status(400).json({ fehler: "nichts_angegeben" }); + + const angelegt = [], abgelehnt = [], warnungen = []; + for (const einzeln of liste) { + const e = eintragAnlegen(req.person, einzeln, monat, heute); + if (e.fehler) abgelehnt.push({ was: einzeln.name || einzeln.link || null, ...e }); + else { + angelegt.push({ id: e.id, was: e.was }); + if (e.warnung) warnungen.push(e.warnung); + } + } + + /* 201 nur, wenn wirklich etwas entstanden ist. Ein 201 ueber eine + leere Liste waere eine Zusage, die nicht stimmt. */ + res.status(angelegt.length ? 201 : 409).json({ + angelegt, abgelehnt, warnungen, + zuviel: (namen || links || []).length > GRENZE + ? `Nur die ersten ${GRENZE} wurden übernommen.` : null, + stand: standFuer(req.person, monat), + }); + } catch (fehler) { + if (String(fehler?.message || "").includes("monat_gesperrt")) { + return res.status(409).json({ fehler: "monat_gesperrt" }); + } + console.error("[manager-ziele] Schnell-Eintrag:", fehler?.message); + res.status(503).json({ fehler: "nicht_gespeichert" }); + } +}); + /** Einen Eintrag anlegen. */ managerZieleRouter.post(`${BASIS}/eintrag`, (req, res) => { try { @@ -620,56 +787,15 @@ managerZieleRouter.post(`${BASIS}/eintrag`, (req, res) => { const monat = monatJetzt(); monatSichern(monat); - const gepruefte = eintragPruefen(req.body, monat, heute); - if (gepruefte.fehler) return res.status(400).json({ fehler: gepruefte.fehler }); - const w = gepruefte.werte; - - const doppel = dublette(req.person.id, monat, w); - if (doppel.eigen) { - return res.status(409).json({ - fehler: "schon_eingetragen", - text: w.name - ? `„${w.name}" steht diesen Monat schon in deiner Liste.` - : "Dieses Video steht diesen Monat schon in deiner Liste.", - }); + const e = eintragAnlegen(req.person, req.body, monat, heute); + if (e.fehler) { + const code = e.fehler === "schon_eingetragen" ? 409 : 400; + return res.status(code).json({ fehler: e.fehler, text: e.text }); } - - /* Der Lead wird nur übernommen, wenn er wirklich existiert und zu - dieser Person gehört. Eine Nummer in der Adresse ist kein - Besitznachweis — dieselbe Lücke, die am 03.09.2026 bei - `darfCreator` steckte. */ - if (w.lead_id) { - const l = db().prepare("SELECT id, scout_id, erstellt_von FROM leads WHERE id = ?") - .get(w.lead_id); - const meiner = l && (l.scout_id === req.person.id || l.erstellt_von === req.person.id); - if (!meiner && !siehtAlles(req.person)) w.lead_id = null; - else if (!l) w.lead_id = null; - } - - /* NAME UND ROLLE WERDEN MITGESCHRIEBEN, nicht nur verknuepft. - Zwei Gruende, und beide stehen in der Vorlage: „Wer die Rolle - verliert, sieht die Kachel nicht mehr; die Daten bleiben fuer - den DogFather erhalten." Ohne diese zwei Spalten staende nach - einem Rollenwechsel ein abgeschlossener Monat ploetzlich an den - Zielzahlen der NEUEN Rolle -- und nach einer Loeschung ohne - Namen da. */ - const info = db().prepare( - `INSERT INTO mz_eintrag - (person_id, von_name, von_rolle, aufgabe, monat, datum, name, name_klein, - link, link_klein, art, notiz, lead_id, doppelt_bei, erstellt) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run( - req.person.id, req.person.name || null, req.person.rolle || null, - w.aufgabe, monat, w.datum, w.name, w.name_klein, - w.link, w.link_klein, w.art, w.notiz, w.lead_id, - doppel.fremd?.person_id ?? null, jetzt()); - res.status(201).json({ - id: Number(info.lastInsertRowid), + id: e.id, stand: standFuer(req.person, monat), - warnung: doppel.fremd - ? { art: "auch_bei", name: doppel.fremd.name, - text: `Hinweis: ${doppel.fremd.name || "jemand anderes"} hat das diesen Monat auch eingetragen.` } - : null, + warnung: e.warnung, }); } catch (fehler) { if (String(fehler?.message || "").includes("monat_gesperrt")) { diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index 5666d22e..4a0d9f8f 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,8 +16,8 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +