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]>
82 lines
3.6 KiB
JavaScript
82 lines
3.6 KiB
JavaScript
/* =====================================================================
|
|
db.js — SQLite-Datenbank (better-sqlite3), Ersatz für Cloudflare D1.
|
|
Führt beim ersten Start alle Migrationen aus ../cloudflare-worker/migrations/
|
|
in der ursprünglichen Reihenfolge aus (0001 Basis-Schema, 0002 app_settings,
|
|
0003 Supporter-Abo, 0004 TikTok/Discord-Spalten, 0005 Supporter-Abstimmungen)
|
|
— anders als bei VanVans Shop ist hier NIE alles in einer konsolidierten
|
|
schema.sql zusammengefasst worden, deshalb müssen wir hier tatsächlich alle
|
|
der Reihe nach anwenden.
|
|
===================================================================== */
|
|
|
|
import Database from "better-sqlite3";
|
|
import { readFileSync, existsSync, mkdirSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const DB_PATH = process.env.DB_PATH || "/var/lib/dogfather-internal/dogfather-internal.db";
|
|
const MIGRATIONS_DIR = join(__dirname, "..", "cloudflare-worker", "migrations");
|
|
|
|
mkdirSync(dirname(DB_PATH), { recursive: true });
|
|
|
|
export const db = new Database(DB_PATH);
|
|
db.pragma("journal_mode = WAL");
|
|
db.pragma("foreign_keys = ON");
|
|
|
|
const MIGRATION_FILES = [
|
|
"0001_init.sql",
|
|
"0002_app_settings.sql",
|
|
"0003_supporter_abo.sql",
|
|
"0004_supporter_oauth_providers.sql",
|
|
"0005_supporter_polls.sql",
|
|
"0006_testimonials.sql",
|
|
"0007_team_members.sql",
|
|
"0008_team_members_rich.sql",
|
|
"0009_supporter_access_code.sql",
|
|
"0010_testimonials_avatar.sql",
|
|
// Webdesign-Bereich (22.08.2026): Anfragen, Kunden, Projekte, Dateien,
|
|
// Nachrichten, Änderungswünsche, pflegbare Inhalte, Änderungsverlauf.
|
|
// Alle Tabellen tragen das Präfix "wd_" und sind fachlich vollständig
|
|
// vom Universe getrennt — Begründung im Kopf der Migration.
|
|
"0011_webdesign.sql",
|
|
// Zahlungen im Webdesign-Bereich (22.08.2026): PayPal-Einmalzahlungen
|
|
// fuer Anzahlung/Restbetrag/Zusatzangebote, PayPal-Abo fuer die
|
|
// monatliche Betreuung, Webhook-Merkliste gegen Doppelverbuchung.
|
|
"0012_webdesign_zahlungen.sql",
|
|
// Aufgabenliste je Projekt ("was ist schon gemacht von dem, was im Plan
|
|
// war") und ein allgemeines Postfach fuer Nachrichten OHNE Projektbezug
|
|
// (22.08.2026). Begruendung fuer die getrennte Postfach-Tabelle steht
|
|
// im Kopf der Migration.
|
|
"0013_webdesign_aufgaben.sql",
|
|
// Elektronische Widerrufsfunktion (§ 356a BGB, seit 19.06.2026) und der
|
|
// Nachweis der Zustimmung zum vorzeitigen Leistungsbeginn (§ 356 Abs. 4
|
|
// BGB). Ausfuehrliche Begruendung im Kopf der Migration -- kurz: Im
|
|
// Portal werden Zusatzangebote per Klick verbindlich angenommen, das ist
|
|
// ein Vertragsschluss ueber eine Online-Benutzeroberflaeche.
|
|
"0014_webdesign_widerruf.sql",
|
|
// Annehmen und Ablehnen einer Anfrage: echter Liefertermin (termin_am)
|
|
// fuer den Countdown, Zeitpunkt der Annahme, Ablehnungsgrund.
|
|
"0015_webdesign_annahme.sql",
|
|
];
|
|
|
|
function alreadyApplied(name) {
|
|
const row = db.prepare(`SELECT 1 FROM _migrations WHERE name = ?`).get(name);
|
|
return !!row;
|
|
}
|
|
|
|
export function initDb() {
|
|
db.exec(`CREATE TABLE IF NOT EXISTS _migrations (name TEXT PRIMARY KEY, applied_at TEXT NOT NULL)`);
|
|
for (const file of MIGRATION_FILES) {
|
|
if (alreadyApplied(file)) continue;
|
|
const path = join(MIGRATIONS_DIR, file);
|
|
if (!existsSync(path)) {
|
|
console.warn(`⚠️ Migration ${file} nicht gefunden unter ${path}, übersprungen.`);
|
|
continue;
|
|
}
|
|
const sql = readFileSync(path, "utf-8");
|
|
db.exec(sql);
|
|
db.prepare(`INSERT INTO _migrations (name, applied_at) VALUES (?, ?)`).run(file, new Date().toISOString());
|
|
console.log(`Migration angewendet: ${file}`);
|
|
}
|
|
}
|