Bisher begann der Liefertermin mit der Annahme. Das ist unfair in beide Richtungen: Wer zehn Tage bis zur Zahlung braucht, verbraucht zehn Tage der zugesagten Zeit, ohne dass ein Handschlag Arbeit passiert waere -- und ich stehe am Ende als der da, der seinen Termin reisst. Ein Projekt hat jetzt drei Abschnitte statt zwei: 1. angenommen, wartet auf Anzahlung -> Uhr steht 2. Anzahlung da -> Uhr laeuft, Termin ab HEUTE neu 3. uebergeben oder abgebrochen -> Uhr steht wieder Der Zahlungseingang loest alles Weitere von selbst aus: Uhr starten, Termin neu rechnen, Status von briefing auf design, 'wer ist am Zug' auf mich, Benachrichtigung in der Verwaltung. Der bei der Annahme genannte Termin bleibt als termin_geplant_am erhalten, und die Meldung nennt BEIDE -- so sieht man, dass sich etwas verschoben hat, ohne nachrechnen zu muessen. Eingehaengt an der Stelle, an der beide Wege zusammenlaufen (PayPals Meldung UND das Vermerken von Hand). Nur am Webhook haenge sich die Seite verschieden verhalten, je nachdem WIE das Geld ankam -- eine von Hand verbuchte Zahlung startete die Uhr nie. Zwei Grundsaetze fuer die Automatik: Sie setzt Dinge in Gang, nimmt aber nie eine Entscheidung zurueck, die ein Mensch getroffen hat (ein von Hand pausiertes Projekt wird nicht kommentarlos wieder gestartet). Und jeder Schritt hinterlaesst eine Spur im Verlauf UND als Meldung -- eine Automatik, die stillschweigend arbeitet, ist kein Helfer, sondern ein Raetsel. ABBRECHEN Ein angenommener Auftrag bleibt abbrechbar: Der Kunde zahlt nicht, meldet sich nicht, springt ab. Vorschau und Ausfuehrung sind getrennt -- die Seite rechnet aus dem Aufgabenfortschritt vor, wie viel Leistung erbracht wurde, und schlaegt daraus einen Erstattungsbetrag vor. Der Betrag ist ein VORSCHLAG: Ob im Einzelfall mehr oder weniger angemessen ist, haengt an Dingen, die keine Tabelle kennt. Offene Rechnungen werden storniert (eine Zahlungsaufforderung ohne Gegenleistung), bezahlte bleiben unangetastet, und die Rueckzahlung loest die Seite bewusst NICHT selbst aus -- PayPal-Rueckzahlungen sind nicht umkehrbar. BENACHRICHTIGUNGEN Eigene Tabelle statt im Verlauf: Der Verlauf haelt fest, WAS geschehen ist -- vollstaendig, zum Nachschlagen. Eine Benachrichtigung ist ein Anstupsen, das gelesen und weggelegt wird. Beides in einer Tabelle hiesse: entweder ein Verlauf voller Rauschen oder Meldungen, die man nicht wegklicken kann. Wegklicken markiert nur als gelesen, loescht nichts. Dazu die Liste der Projekte, die seit ueber einer Woche auf ihre Anzahlung warten. Sie stehen in keiner anderen Zahl, weil ihre Uhr nie zu laufen begann -- ohne diesen Hinweis vergisst man sie. Geprueft: 44 gegen eine echte Datenbank. Darunter der Kern -- die Annahme wird zehn Tage zurueckdatiert, und der Termin muss danach trotzdem volle 20 Werktage entfernt liegen. Beim Bauen des Tests selbst ein Fehler gefunden: Die erste Fassung datierte nur die Annahme zurueck, nicht den damals errechneten Termin, und bildete damit genau den Fall nicht ab, um den es geht. Co-Authored-By: Claude Opus 5 <[email protected]>
238 lines
9.6 KiB
JavaScript
238 lines
9.6 KiB
JavaScript
/* =====================================================================
|
|
webdesign-automatik.js — was von selbst passieren soll
|
|
|
|
Der Leitgedanke: Ich soll nichts von Hand nachtragen müssen, was die
|
|
Seite selbst wissen kann. Eine Zahlung geht ein — dann muss niemand
|
|
daran denken, die Uhr zu starten, den Status umzustellen und sich
|
|
eine Notiz zu machen. Das passiert, weil es passiert ist.
|
|
|
|
ZWEI REGELN, DIE DAS ERTRÄGLICH MACHEN
|
|
|
|
1. AUTOMATIK NUR IN EINE RICHTUNG.
|
|
Sie setzt Dinge in Gang, nimmt aber nie eine Entscheidung zurück,
|
|
die ein Mensch getroffen hat. Wer den Status von Hand auf
|
|
"pausiert" stellt, will das so — dann darf eine eingehende Zahlung
|
|
ihn nicht kommentarlos wieder auf "Design" schieben.
|
|
|
|
2. JEDER SCHRITT HINTERLÄSST EINE SPUR.
|
|
Alles, was hier von selbst geschieht, steht danach im Verlauf UND
|
|
als Benachrichtigung. Eine Automatik, die stillschweigend arbeitet,
|
|
ist keine Hilfe, sondern ein Rätsel: Man sieht ein verändertes
|
|
Projekt und weiss nicht, wer es angefasst hat.
|
|
===================================================================== */
|
|
import { db } from "../db.js";
|
|
import { jetzt, neueId, verlauf, centZuText } from "./webdesign-helfer.js";
|
|
import { terminVorschlag, laufzeitFuer, hatLiefertermin } from "./webdesign-termine.js";
|
|
|
|
/* ---------------------------------------------------------------------
|
|
BENACHRICHTIGUNGEN
|
|
--------------------------------------------------------------------- */
|
|
export function meldungAnlegen({ art, titel, text, ziel, zielId, dringend }) {
|
|
const id = neueId();
|
|
db.prepare(
|
|
`INSERT INTO wd_meldungen (id, art, titel, text, ziel, ziel_id, dringend, gelesen, erstellt_am)
|
|
VALUES (?,?,?,?,?,?,?,0,?)`
|
|
).run(id, art, titel, text || null, ziel || null, zielId || null, dringend ? 1 : 0, jetzt());
|
|
return id;
|
|
}
|
|
|
|
export function meldungenLesen(nurOffene = true, grenze = 20) {
|
|
return db.prepare(
|
|
`SELECT id, art, titel, text, ziel, ziel_id, dringend, gelesen, erstellt_am
|
|
FROM wd_meldungen ${nurOffene ? "WHERE gelesen = 0" : ""}
|
|
ORDER BY gelesen ASC, erstellt_am DESC LIMIT ?`
|
|
).all(grenze);
|
|
}
|
|
|
|
export function meldungenOffen() {
|
|
return db.prepare(`SELECT COUNT(*) AS n FROM wd_meldungen WHERE gelesen = 0`).get().n;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
DIE UHR STARTEN
|
|
|
|
Wird aufgerufen, wenn eine Zahlung als bezahlt vermerkt wird — egal
|
|
ob PayPal das gemeldet hat oder ich es von Hand eingetragen habe.
|
|
Beide Wege müssen dasselbe auslösen, sonst hinge das Verhalten davon
|
|
ab, WIE das Geld ankam.
|
|
|
|
Rückgabe: was tatsächlich geschehen ist, damit der Aufrufer es
|
|
weitermelden kann. Ein stilles `void` würde bedeuten, dass die
|
|
Oberfläche raten muss, ob etwas passiert ist.
|
|
--------------------------------------------------------------------- */
|
|
export function zahlungVerbucht(zahlungId, wer) {
|
|
const z = db.prepare(
|
|
`SELECT id, nummer, art, projekt_id, kunde_id, betrag_cent, waehrung
|
|
FROM wd_zahlungen WHERE id = ?`
|
|
).get(zahlungId);
|
|
if (!z) return { getan: [] };
|
|
|
|
const getan = [];
|
|
const kunde = z.kunde_id
|
|
? db.prepare(`SELECT name FROM wd_kunden WHERE id = ?`).get(z.kunde_id)
|
|
: null;
|
|
const wieViel = centZuText(z.betrag_cent, z.waehrung);
|
|
|
|
/* Ohne Projekt gibt es keine Uhr — etwa bei einer laufenden Betreuung.
|
|
Gemeldet wird trotzdem: Geld, das eingeht, will man sehen. */
|
|
if (!z.projekt_id) {
|
|
meldungAnlegen({
|
|
art: "zahlung_da",
|
|
titel: wieViel + " eingegangen",
|
|
text: (kunde ? kunde.name + " · " : "") + (z.nummer || ""),
|
|
ziel: "zahlungen", zielId: z.id,
|
|
});
|
|
return { getan: ["gemeldet"] };
|
|
}
|
|
|
|
const p = db.prepare(
|
|
`SELECT id, nummer, titel, paket, status, uhr_start_am, termin_am, termin_geplant_am
|
|
FROM wd_projekte WHERE id = ?`
|
|
).get(z.projekt_id);
|
|
if (!p) return { getan: [] };
|
|
|
|
/* ---- Ist das die Anzahlung, und steht die Uhr noch? ---- */
|
|
const istAnzahlung = z.art === "anzahlung";
|
|
const uhrStehtNoch = !p.uhr_start_am;
|
|
/* Ein abgebrochenes oder uebergebenes Projekt startet nicht mehr --
|
|
eine verspaetet verbuchte Zahlung darf kein totes Projekt
|
|
wiederbeleben. */
|
|
const laeuftNoch = p.status !== "abgebrochen" && p.status !== "uebergeben";
|
|
|
|
if (istAnzahlung && uhrStehtNoch && laeuftNoch) {
|
|
const start = jetzt();
|
|
/* Der Termin wird ab HEUTE neu gerechnet, nicht ab der Annahme.
|
|
|
|
Sonst verbraucht ein Kunde, der zehn Tage bis zur Zahlung
|
|
braucht, zehn Tage der zugesagten Zeit -- und ich stehe am Ende
|
|
als der da, der seinen Termin reisst. Der urspruenglich genannte
|
|
Termin bleibt als termin_geplant_am erhalten, damit man den
|
|
Unterschied sieht. */
|
|
const neuerTermin = hatLiefertermin(p.paket)
|
|
? terminVorschlag(p.paket, new Date()).termin
|
|
: null;
|
|
|
|
db.prepare(
|
|
`UPDATE wd_projekte
|
|
SET uhr_start_am = ?, start_am = ?,
|
|
termin_geplant_am = COALESCE(termin_geplant_am, termin_am),
|
|
termin_am = COALESCE(?, termin_am),
|
|
anzahlung_bezahlt = 1,
|
|
status = CASE WHEN status = 'briefing' THEN 'design' ELSE status END,
|
|
wartet_auf = 'dogfather',
|
|
naechster_schritt = CASE
|
|
WHEN naechster_schritt IS NULL OR naechster_schritt = ''
|
|
OR naechster_schritt LIKE 'Unterlagen%'
|
|
THEN 'Es geht los — erste Entwürfe'
|
|
ELSE naechster_schritt END,
|
|
aktualisiert_am = ?
|
|
WHERE id = ?`
|
|
).run(start, start, neuerTermin, jetzt(), p.id);
|
|
|
|
getan.push("uhr_gestartet");
|
|
verlauf("projekt", p.id, "uhr_gestartet", wer || "system",
|
|
`Anzahlung ${wieViel} verbucht` + (neuerTermin ? ` · Liefertermin ${neuerTermin}` : ""));
|
|
|
|
meldungAnlegen({
|
|
art: "anzahlung_da",
|
|
titel: "Anzahlung da — " + p.nummer + " läuft",
|
|
text: (kunde ? kunde.name + ": " : "") + wieViel + " eingegangen."
|
|
+ (neuerTermin ? " Liefertermin " + neuerTermin + "." : "")
|
|
+ (p.termin_am && neuerTermin && p.termin_am !== neuerTermin
|
|
? " (bei der Annahme war " + p.termin_am + " vorgesehen)" : ""),
|
|
ziel: "projekte", zielId: p.id,
|
|
dringend: 1,
|
|
});
|
|
return { getan, projektId: p.id, terminNeu: neuerTermin };
|
|
}
|
|
|
|
/* ---- Restzahlung oder eine weitere Zahlung ---- */
|
|
if (z.art === "rest") {
|
|
db.prepare(`UPDATE wd_projekte SET rest_bezahlt = 1, aktualisiert_am = ? WHERE id = ?`)
|
|
.run(jetzt(), p.id);
|
|
getan.push("rest_vermerkt");
|
|
}
|
|
meldungAnlegen({
|
|
art: "zahlung_da",
|
|
titel: wieViel + " eingegangen — " + p.nummer,
|
|
text: (kunde ? kunde.name + " · " : "") + (z.nummer || ""),
|
|
ziel: "projekte", zielId: p.id,
|
|
});
|
|
getan.push("gemeldet");
|
|
return { getan, projektId: p.id };
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
ERSTATTUNGSVORSCHLAG BEIM ABBRUCH
|
|
|
|
Rechnet vor, statt zu entscheiden. Der Vorschlag ergibt sich aus dem
|
|
Anteil erledigter Arbeitsschritte: Wer bei 40 % abbricht, hat 40 %
|
|
der Leistung bekommen.
|
|
|
|
Bewusst ein VORSCHLAG und keine Regel. Ob im Einzelfall mehr oder
|
|
weniger angemessen ist, hängt an Dingen, die keine Tabelle kennt --
|
|
wer abgebrochen hat, ob überhaupt mitgewirkt wurde, wie lange
|
|
gewartet wurde. Die Zahl nimmt einem das Rechnen ab, nicht das
|
|
Urteilen.
|
|
|
|
Der Fortschritt zählt nur Aufgaben, die nicht "entfaellt" sind:
|
|
Gestrichene Punkte sind keine erbrachte Leistung, würden die Quote
|
|
aber schönen.
|
|
--------------------------------------------------------------------- */
|
|
export function erstattungsVorschlag(projektId) {
|
|
const p = db.prepare(
|
|
`SELECT id, preis_cent, anzahlung_cent, waehrung FROM wd_projekte WHERE id = ?`
|
|
).get(projektId);
|
|
if (!p) return null;
|
|
|
|
const a = db.prepare(
|
|
`SELECT
|
|
COUNT(*) AS gesamt,
|
|
SUM(CASE WHEN status = 'erledigt' THEN 1 ELSE 0 END) AS fertig
|
|
FROM wd_aufgaben
|
|
WHERE projekt_id = ? AND status <> 'entfaellt' AND nicht_enthalten = 0`
|
|
).get(projektId);
|
|
|
|
const gesamt = a.gesamt || 0;
|
|
const fertig = a.fertig || 0;
|
|
const prozent = gesamt ? Math.round((fertig / gesamt) * 100) : 0;
|
|
|
|
/* Bezahlt ist, was tatsächlich verbucht wurde -- nicht, was
|
|
vorgesehen war. Ein Projekt kann angenommen sein, ohne dass je
|
|
Geld floss. */
|
|
const bezahlt = db.prepare(
|
|
`SELECT COALESCE(SUM(betrag_cent),0) AS n FROM wd_zahlungen
|
|
WHERE projekt_id = ? AND status = 'bezahlt'`
|
|
).get(projektId).n;
|
|
|
|
/* Wert der erbrachten Leistung, gedeckelt auf das, was gezahlt wurde:
|
|
Mehr als vorhanden kann man nicht behalten. */
|
|
const verdient = Math.min(bezahlt, Math.round((p.preis_cent || 0) * prozent / 100));
|
|
const erstattung = Math.max(0, bezahlt - verdient);
|
|
|
|
return {
|
|
prozent, fertig, gesamt,
|
|
bezahltCent: bezahlt, bezahltText: centZuText(bezahlt, p.waehrung),
|
|
behaltenCent: verdient, behaltenText: centZuText(verdient, p.waehrung),
|
|
erstattungCent: erstattung, erstattungText: centZuText(erstattung, p.waehrung),
|
|
};
|
|
}
|
|
|
|
/* Wie viel Zeit ist seit der Annahme vergangen, ohne dass gezahlt wurde?
|
|
|
|
Gebraucht für die Erinnerung: Ein Projekt, das seit zwei Wochen auf
|
|
eine Anzahlung wartet, ist meistens keins mehr -- man will das sehen,
|
|
bevor man es vergisst. */
|
|
export function wartetAufAnzahlung(tageGrenze = 7) {
|
|
return db.prepare(
|
|
`SELECT p.id, p.nummer, p.titel, p.angenommen_am, k.name AS kunde_name
|
|
FROM wd_projekte p JOIN wd_kunden k ON k.id = p.kunde_id
|
|
WHERE p.archiviert = 0
|
|
AND p.uhr_start_am IS NULL
|
|
AND p.status NOT IN ('abgebrochen','uebergeben')
|
|
AND p.angenommen_am IS NOT NULL
|
|
AND julianday('now') - julianday(p.angenommen_am) >= ?
|
|
ORDER BY p.angenommen_am ASC`
|
|
).all(tageGrenze);
|
|
}
|