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 @@ -

-

-

-  -

-

-  -

-

-  -

-

-  -

-
-
-
- - - - {if $page == 0} - {**}
{*
*} - {else} -
-
- {/if} - {foreach $weekdays as $weekday} - {$lastarbtermend = 0} - {if $i == 1 && $page == 0} -
{$weekday.weekday} - -
- {else if {$weekday.weekday|substr:0:7} != "Samstag" && {$weekday.weekday|substr:0:7} != "Sonntag"} -
{$weekday.weekday}
- -
- {**}
{*
*} - {else} -
{$weekday.weekday}
- -
- {**}
{*
*} - {/if} -
- - {$shift = 0} - {$prevtermin= ""} - {$absmaxshift.{$weekday.weekday} = 1} - {$absmaxshiftone.{$weekday.weekday} = 0} - {$shiftone = 0} - {foreach $weekday.termine as $key=>$termin} - - {if {$termin.termdauer} == "24:00:00"} - {$termin.termdauer = "23:59:59"} - {/if} - {if {$prevtermin.termtime|date_format:"%H%M"} <= {$termin.termtime|date_format:"%H%M"} && {{$prevtermin.termtime|date_format:"%H%M"}+{$prevtermin.termdauer|date_format:"%H%M"}} > {$termin.termtime|date_format:"%H%M"}} {*starttime of next term is within the prev term*} - {if {$prevtermin.termdauer} == "23:59:59"} - {$shiftone = $shiftone+1} - {else} - {$shift = $shift+1} - {/if} - {/if} - {if $termin.termdauer == "23:59:59"} - {if $termin.user2 > 0} - {$shiftone = $shiftone+1} - {/if} - {if $termin.user3 > 0} - {$shiftone = $shiftone+1} - {/if} - {if $termin.user4 > 0} - {$shiftone = $shiftone+1} - {/if} - {else} - {if $termin.user2 > 0} - {$shift = $shift+1} - {/if} - {if $termin.user3 > 0} - {$shift = $shift+1} - {/if} - {if $termin.user4 > 0} - {$shift = $shift+1} - {/if} - {/if} - {$weekday.termine.{$key}.shift = $shift} - {$weekday.termine.{$key}.shiftone = $shiftone} - - {if $shift+1 > $absmaxshift.{$weekday.weekday}} - {$absmaxshift.{$weekday.weekday} = $shift+1} - {/if} - {if $shiftone > $absmaxshiftone.{$weekday.weekday}} - {$absmaxshiftone.{$weekday.weekday} = $shiftone} - {/if} - {$prevtermin = $termin} - {/foreach} - - {$width = ((18 / $absmaxshift.{$weekday.weekday})-$absmaxshiftone.{$weekday.weekday})} - {foreach $weekday.termine as $key=>$termin} - - {$shift = 10*$i + 3.9 + ($width/2)*$termin.shift + $termin.shiftone} - {if {$termin.termdauer} == '24:00:00'} - {$terminlength = 960} - {$terminstart = 390} - {$height = ($terminlength/15)} - {$start = 4 + ($terminstart - 420)/15} - -
- {$termin.typ|substr:0:3|escape}. -
- {else} - {$terminlength = ({$termin.termdauer|date_format:"%H"}*60 + {$termin.termdauer|date_format:"%M"})} - {$terminstart = ({$termin.termtime|date_format:"%H"}*60 + {$termin.termtime|date_format:"%M"})} - {if {$terminstart} == 0} -
- {$terminlength = {$termin.termdauer}*60} - {$terminstart = 450+$lastarbtermend} - {$lastarbtermend = $terminstart-450 + $termin.termdauer * 60} - {$termin.typ = 'Arbeitsleistung'} - {else} - - {/if} - {$height = ($terminlength/15)} - {$start = 4 + ($terminstart - 420)/15} - {if {$termin.typ} != 'Arbeitsleistung'} -
Dauer {if {$termin.termdauer|date_format:'%k'} > 0} {$termin.termdauer|date_format:'%k'}h{/if} - {if {$termin.termdauer|date_format:'%M'} > 0}{$termin.termdauer|date_format:'%M'}min{/if}
- In {$termin.ort|escape}{if $termin.ortsteil}-{$termin.ortsteil|escape}{/if}
- Bei {$termin.vname|escape} {$termin.nname|escape} {$termin.firma|escape}'> - {else} -
{$termin.typ|substr:0:3|escape}. -
- {/if} -
- {if $termin.user2} - {$shift = 10*$i + 3.9 + ($width)*($termin.shift-1) + $termin.shiftone} - - {if {$termin.termdauer} == '24:00:00'} - {$terminlength = 960} - {$terminstart = 390} - {$height = ($terminlength/15)} - {$start = 4 + ($terminstart - 420)/15} -
- {$termin.typ|substr:0:3|escape}. -
- {else} - {$terminlength = ({$termin.termdauer|date_format:"%H"}*60 + {$termin.termdauer|date_format:"%M"})} - {$terminstart = ({$termin.termtime|date_format:"%H"}*60 + {$termin.termtime|date_format:"%M"})} - {$height = ($terminlength/15)} - {$start = 4 + ($terminstart - 420)/15} -
Dauer {if {$termin.termdauer|date_format:'%k'} > 0} {$termin.termdauer|date_format:'%k'}h{/if} {if {$termin.termdauer|date_format:'%M'} > 0}{$termin.termdauer|date_format:'%M'}min{/if}
In {$termin.ort|escape}{if $termin.ortsteil}-{$termin.ortsteil|escape}{/if}
Bei {$termin.vname|escape} {$termin.nname|escape} {$termin.firma|escape}'> - {$termin.typ|substr:0:3|escape}. -
- {/if} -
- {/if} - {if $termin.user3} - {$shift = 10*$i + 3.9 + ($width)*($termin.shift-2) + $termin.shiftone} - - {if {$termin.termdauer} == '24:00:00'} - {$terminlength = 960} - {$terminstart = 390} - {$height = ($terminlength/15)} - {$start = 4 + ($terminstart - 420)/15} -
- {$termin.typ|substr:0:3|escape}. -
- {else} - {$terminlength = ({$termin.termdauer|date_format:"%H"}*60 + {$termin.termdauer|date_format:"%M"})} - {$terminstart = ({$termin.termtime|date_format:"%H"}*60 + {$termin.termtime|date_format:"%M"})} - {$height = ($terminlength/15)} - {$start = 4 + ($terminstart - 420)/15} -
Dauer {if {$termin.termdauer|date_format:'%k'} > 0} {$termin.termdauer|date_format:'%k'}h{/if} {if {$termin.termdauer|date_format:'%M'} > 0}{$termin.termdauer|date_format:'%M'}min{/if}
In {$termin.ort|escape}{if $termin.ortsteil}-{$termin.ortsteil|escape}{/if}
Bei {$termin.vname|escape} {$termin.nname|escape} {$termin.firma|escape}'> - {$termin.typ|substr:0:3|escape}. -
- {/if} -
- {/if} - {if $termin.user4} - {$shift = 10*$i + 3.9 + ($width)*($termin.shift-3) + $termin.shiftone} - - {if {$termin.termdauer} == '24:00:00'} - {$terminlength = 960} - {$terminstart = 390} - {$height = ($terminlength/15)} - {$start = 4 + ($terminstart - 420)/15} -
- {$termin.typ|substr:0:3|escape}. -
- {else} - {$terminlength = ({$termin.termdauer|date_format:"%H"}*60 + {$termin.termdauer|date_format:"%M"})} - {$terminstart = ({$termin.termtime|date_format:"%H"}*60 + {$termin.termtime|date_format:"%M"})} - {$height = ($terminlength/15)} - {$start = 4 + ($terminstart - 420)/15} -
Dauer {if {$termin.termdauer|date_format:'%k'} > 0} {$termin.termdauer|date_format:'%k'}h{/if} {if {$termin.termdauer|date_format:'%M'} > 0}{$termin.termdauer|date_format:'%M'}min{/if}
In {$termin.ort|escape}{if $termin.ortsteil}-{$termin.ortsteil|escape}{/if}
Bei {$termin.vname|escape} {$termin.nname|escape} {$termin.firma|escape}'> - {$termin.typ|substr:0:3|escape}. -
- {/if} -
- {/if} - {/foreach} - - {$i=$i+1} - {/foreach} - - {$i = 0} - {while $i < 16} - {$i = $i+1} -
{{$i+6}|str_pad:2:'0':0}:00
-
- {/while} - {$time = {$smarty.now|date_format:"%H"}*60 + {$smarty.now|date_format:"%M"}} -
- -{if $page == 0} - {for $i = 1 to 7} -
- {$weather.{$i}.low}°C - {$weather.{$i}.high}°C -
- {/for} -{/if} - - - -
-
- -{foreach $users as $user} - {$user.vname|escape} {$user.nname|escape} -{/foreach} -
- -