diff --git a/CLAUDE.md b/CLAUDE.md
index 5bcf8a4..caf92c7 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -106,7 +106,9 @@ SQL wird durchgehend per String-Verkettung gebaut. Escaping ist Handarbeit über
## Termine
-Ein Termin über mehrere Tage ist eine **Reihe einzelner Zeilen** in `termin`, die alle dieselbe `main_id` tragen — die id der ersten Zeile. Der Kalender zeigt die Zeilen einzeln, die Terminliste fasst sie über `main_id` zusammen. Wird ein Termin auf mehrere Tage verlängert, wird die alte Reihe verworfen und neu angelegt; er bekommt dabei eine neue id.
+Ein Termin über mehrere Tage ist eine **Reihe einzelner Zeilen** in `termin`, die alle dieselbe `main_id` tragen — die id der ersten Zeile (des „Hauptsatzes“). Der Kalender zeigt die Zeilen einzeln, die Terminliste fasst sie über `main_id` zusammen.
+
+Beim Speichern bleibt der Hauptsatz immer stehen und wird aktualisiert; nur die Folgetage werden gelöscht und neu angelegt. Der Termin behält dadurch seine id, egal ob aus einem Tag mehrere werden oder umgekehrt — Verweise auf die id (Drucken, Links, Protokoll) bleiben gültig. `terminHauptsatz()` löst dafür jede Zeilen-id auf ihre Reihe auf, damit auch der Einstieg über eine Nebenzeile funktioniert.
Urlaub und Krankheit sind Termine ohne Kundenadresse. Ihre Typ-ids sind fest verdrahtet — `TERMTYP_URLAUB` = 0 und `TERMTYP_KRANKHEIT` = 5 in `incs/termin.php`; `incs/users.php` rechnet den Urlaubsanspruch über `typ = 0` aus und `templates/addtermin.tpl` blendet danach die Felder um. `termtyp` muss also eine Zeile mit der id 0 enthalten; beim Kopieren einer Datenbank braucht es dafür `NO_AUTO_VALUE_ON_ZERO`, sonst macht MariaDB eine 1 daraus.
@@ -119,7 +121,6 @@ Ganztägige Termine haben die Dauer `24:00:00` — die Terminliste unterscheidet
- **PDF-Schriften nur über `PDF_SCHRIFT_SERIF` / `PDF_SCHRIFT_SANS`** (oben in `incs/makepdf.php`, FreeSerif/FreeSans). Die eingebauten PDF-Schriften Times/Helvetica können nur westeuropäische Zeichen; aus ř, ł, č würde ein „?“.
- **`incs/font/` gehört zu pChart, nicht zu TCPDF.** Die TTFs dort werden von `stats.php` für die Diagramme geladen; TCPDF bringt seine Schriften in `incs/tcpdf/fonts/` selbst mit.
- **`index.php` setzt `error_reporting(E_ERROR | E_PARSE)`.** Warnungen und Notices sind unterdrückt, und der Code verlässt sich darauf — nicht ohne Not hochdrehen, sonst überflutet es die Ausgabe.
-- **`templates/calendar_month.tpl` ist tot.** Kein Code bindet es ein, und unter PHP 8 läuft es in einen Fehler (`{$prevtermin = ""}` und danach `{$prevtermin.termtime}`).
- **`templates_c/`, `ajax/templates_c/` und `temp/` müssen für den Webserver schreibbar sein.** Sie sind git-ignoriert und nur über `.gitkeep` im Repo vorhanden.
- **`uploads_*/{notes,signs}` enthalten personenbezogene Kundendaten** (Rechnungen, Scans, Unterschriften) und sind bewusst git-ignoriert. Nie committen.
- Zeilenenden werden über `.gitattributes` auf LF normalisiert.
@@ -135,4 +136,6 @@ Klassischer Login plus WebAuthn/FIDO2. Server-Seite: `authServer.php` mit `incs/
Am 2026-08-28 wurden tote Pfade entfernt: `smarty3/` (ungenutztes Smarty 3.1.6), `incs/tcpdf_old/`, `incs/fpdf.php` samt FPDF-Schriftmetriken und `incs/font/makefont/`, `sites/makepdf_OLD.php`, `incs/users_old.php` und `oldindex.php`. Auf keines davon verwies noch Code.
+Am 2026-09-22 kam `templates/calendar_month.tpl` dazu (Monatsansicht des Kalenders): kein Code band es ein, und unter PHP 8 lief es in einen Fehler.
+
Das Repository wurde am 2026-08-28 neu initialisiert. Die mitgelieferte Historie von 2014 war irreparabel beschädigt und wurde verworfen — der erste Commit ist der Ausgangsstand, nicht der Projektbeginn.
diff --git a/incs/termin.php b/incs/termin.php
index fbf576c..21ecdf6 100644
--- a/incs/termin.php
+++ b/incs/termin.php
@@ -146,6 +146,19 @@ function pruefeUeberschneidungen($dbconn, $werte, $start, $dauer, $tage, $ausser
* Speichern
* ------------------------------------------------------------------ */
+/*
+ * Der Hauptsatz der Reihe, zu der die Zeile $id gehoert. Altdaten ohne
+ * main_id bestehen nur aus sich selbst. null, wenn es die Zeile nicht gibt,
+ * false bei Fehlern.
+ */
+function terminHauptsatz($dbconn, $id)
+{
+ $mainId = db_wert($dbconn, "SELECT main_id FROM termin WHERE id = ?", array($id));
+ if($mainId === false || $mainId === null)
+ return $mainId;
+ return (int)$mainId > 0 ? (int)$mainId : (int)$id;
+}
+
/*
* Legt einen Folgetermin an: gleicher Kopf und dieselben Mitarbeiter, aber
* ohne Datum und mit einem Wiedervorlagedatum in $tage Tagen. Gibt dessen
@@ -190,21 +203,26 @@ function speichereTermin($dbconn, $terminid, $adressid)
beginneSpeichern($dbconn);
- $neuAnlegen = !$terminid;
- if($terminid && $tage > 1)
+ $mainId = 0;
+ $alt = null;
+ if($terminid)
{
- // aus einem Einzeltermin wird eine Reihe: alte Zeilen verwerfen
- $err = loescheTerminreihe($dbconn, $terminid);
- if($err)
- return beendeSpeichern($dbconn, $err);
- $neuAnlegen = true;
- $terminid = 0;
- }
- elseif($terminid)
- {
- // Zeilen der bisherigen Reihe bis auf die erste entfernen
- $sql = "DELETE FROM termin WHERE main_id = ? AND id != ?";
- db_abfrage($dbconn, $sql, array($terminid, $terminid))
+ $mainId = terminHauptsatz($dbconn, $terminid);
+ if($mainId === false)
+ return beendeSpeichern($dbconn, error("Fehler beim ermitteln des Termins:
".mysqli_error($dbconn)));
+ if($mainId === null)
+ return beendeSpeichern($dbconn, error("Dieser Termin existiert nicht!"));
+
+ $alt = db_zeile($dbconn, "SELECT * FROM termin WHERE id = ?", array($mainId));
+ if($alt === false || !$alt)
+ return beendeSpeichern($dbconn, error("Dieser Termin existiert nicht!"));
+
+ /*
+ * Die Folgetage werden neu angelegt, der Hauptsatz bleibt stehen -
+ * so behaelt der Termin seine id, auch wenn aus einem Tag mehrere
+ * werden oder umgekehrt.
+ */
+ db_abfrage($dbconn, "DELETE FROM termin WHERE main_id = ? AND id != ?", array($mainId, $mainId))
or $err = error("Konnte Nebentermine nicht löschen:
".mysqli_error($dbconn));
if(!empty($err))
return beendeSpeichern($dbconn, $err);
@@ -212,21 +230,11 @@ function speichereTermin($dbconn, $terminid, $adressid)
if(!$werte["done"])
{
- $err = pruefeUeberschneidungen($dbconn, $werte, $start, $dauer, $tage, $terminid);
+ $err = pruefeUeberschneidungen($dbconn, $werte, $start, $dauer, $tage, $mainId);
if($err)
return beendeSpeichern($dbconn, $err);
}
- $alt = null;
- if(!$neuAnlegen)
- {
- $alt = db_zeile($dbconn, "SELECT * FROM termin WHERE id = ?", array($terminid));
- if($alt === false)
- return beendeSpeichern($dbconn, error("Fehler beim ermitteln des Termins:
".mysqli_error($dbconn)));
- if(!$alt)
- return beendeSpeichern($dbconn, error("Dieser Termin existiert nicht!"));
- }
-
// Folgetermin nur einmal und nur, wenn der Termin gerade erledigt wird
$folgeId = $alt ? (int)$alt["followed_by"] : 0;
if($werte["done"] && $werte["folge"] && !$folgeId)
@@ -256,44 +264,49 @@ function speichereTermin($dbconn, $terminid, $adressid)
$set = implode(", ", $zuweisung);
$err = 0;
- if(!$neuAnlegen)
+ // Tag 0 ist der Hauptsatz: beim Aendern bleibt er stehen, sonst entsteht er hier
+ if($mainId)
{
$sql = "UPDATE termin SET $set, termtime = ?, changetime = NOW() WHERE id = ?";
- $params = array_merge(array_values($felder), array(terminTag($start, 0), $terminid));
+ $params = array_merge(array_values($felder), array(terminTag($start, 0), $mainId));
if(!db_abfrage($dbconn, $sql, $params))
return beendeSpeichern($dbconn, error("Fehler beim ändern des Termins:
".mysqli_error($dbconn)));
$altWerte = array();
foreach(array_keys($felder) as $spalte)
$altWerte[] = $alt[$spalte];
$err = saveLog($dbconn, db_sql_text($dbconn, $sql, $params)." [termin change]",
- db_sql_text($dbconn, $sql, array_merge($altWerte, array($alt["termtime"], $terminid))), "ändern");
+ db_sql_text($dbconn, $sql, array_merge($altWerte, array($alt["termtime"], $mainId))), "ändern");
}
else
{
- $mainId = 0;
- foreach(range(0, $tage - 1) as $i)
+ $sql = "INSERT INTO termin SET $set, termtime = ?, changetime = NOW(), datetime = NOW()";
+ $params = array_merge(array_values($felder), array(terminTag($start, 0)));
+ if(!db_abfrage($dbconn, $sql, $params))
+ return beendeSpeichern($dbconn, error("Fehler beim anlegen des Termins:
".mysqli_error($dbconn)));
+ $mainId = mysqli_insert_id($dbconn);
+ db_abfrage($dbconn, "UPDATE termin SET main_id = ? WHERE id = ?", array($mainId, $mainId))
+ or $err = error("Fehler beim verknüpfen der Terminreihe:
".mysqli_error($dbconn));
+ if(!$err)
+ $err = saveLog($dbconn, db_sql_text($dbconn, $sql, $params)." [termin add, hat ID $mainId]",
+ "DELETE FROM termin WHERE id = $mainId", "anlegen");
+ }
+
+ // die weiteren Tage haengen als eigene Zeilen am Hauptsatz
+ // (range(1, 0) liefe rueckwaerts, deshalb eine normale Schleife)
+ for($i = 1; $i < $tage; $i++)
+ {
+ if($err)
+ break;
+ $sql = "INSERT INTO termin SET $set, termtime = ?, main_id = ?, changetime = NOW(), datetime = NOW()";
+ $params = array_merge(array_values($felder), array(terminTag($start, $i), $mainId));
+ if(!db_abfrage($dbconn, $sql, $params))
{
- if($err)
- break;
- $sql = "INSERT INTO termin SET $set, termtime = ?, main_id = ?, changetime = NOW(), datetime = NOW()";
- $params = array_merge(array_values($felder), array(terminTag($start, $i), $mainId));
- if(!db_abfrage($dbconn, $sql, $params))
- {
- $err = error("Fehler beim anlegen des Termins:
".mysqli_error($dbconn));
- break;
- }
- $id = mysqli_insert_id($dbconn);
- if(!$mainId)
- {
- // der erste Tag ist der Hauptsatz der Reihe
- $mainId = $id;
- db_abfrage($dbconn, "UPDATE termin SET main_id = ? WHERE id = ?", array($id, $id))
- or $err = error("Fehler beim verknüpfen der Terminreihe:
".mysqli_error($dbconn));
- }
- if(!$err)
- $err = saveLog($dbconn, db_sql_text($dbconn, $sql, $params)." [termin add, hat ID $id]",
- "DELETE FROM termin WHERE id = $id", "anlegen");
+ $err = error("Fehler beim anlegen des Termins:
".mysqli_error($dbconn));
+ break;
}
+ $id = mysqli_insert_id($dbconn);
+ $err = saveLog($dbconn, db_sql_text($dbconn, $sql, $params)." [termin add, hat ID $id]",
+ "DELETE FROM termin WHERE id = $id", "anlegen");
}
return beendeSpeichern($dbconn, $err);
}
@@ -303,11 +316,12 @@ function loescheTerminreihe($dbconn, $id)
{
beginneSpeichern($dbconn);
$err = 0;
- $beschreibung = db_wert($dbconn, "SELECT head FROM termin WHERE id = ?", array($id));
- if($beschreibung === false)
+ $id = terminHauptsatz($dbconn, $id);
+ if($id === false)
return beendeSpeichern($dbconn, error("Fehler beim ermitteln des Termins:
".mysqli_error($dbconn)));
- if($beschreibung === null)
+ if($id === null)
return beendeSpeichern($dbconn, error("Dieser Termin existiert nicht!"));
+ $beschreibung = db_wert($dbconn, "SELECT head FROM termin WHERE id = ?", array($id));
// erst die Nebenzeilen, dann der Hauptsatz - sonst verliert main_id den Bezug
$sql = "DELETE FROM termin WHERE main_id = ? AND id != ?";
@@ -328,10 +342,10 @@ function loescheTerminreihe($dbconn, $id)
function setzeTerminErledigt($dbconn, $id, $erledigt)
{
$erledigt = $erledigt ? 1 : 0;
- $vorhanden = db_wert($dbconn, "SELECT id FROM termin WHERE id = ?", array($id));
- if($vorhanden === false)
+ $id = terminHauptsatz($dbconn, $id);
+ if($id === false)
return error("Fehler beim ermitteln des Termins:
".mysqli_error($dbconn));
- if($vorhanden === null)
+ if($id === null)
return error("Dieser Termin existiert nicht!");
beginneSpeichern($dbconn);
@@ -359,7 +373,8 @@ function terminZumBearbeiten($dbconn, $id)
SEC_TO_TIME(SUM(TIME_TO_SEC(termdauer))) AS termdauer,
head, followed_by, MIN(id) AS id, main_id, typ, adresse_id,
done, user, user2, user3, user4, changeuser, act_date, text
- FROM termin WHERE main_id = ? GROUP BY main_id", array($id));
+ FROM termin WHERE main_id = ? OR (id = ? AND main_id = 0)
+ GROUP BY main_id", array($id, $id));
if($res === false)
{
error("Fehler beim ermitteln des Termins:
".mysqli_error($dbconn));
diff --git a/templates/calendar_month.tpl b/templates/calendar_month.tpl
deleted file mode 100644
index 24833d9..0000000
--- a/templates/calendar_month.tpl
+++ /dev/null
@@ -1,276 +0,0 @@
-
-
-- -
-- -
-- -
-- -
-