From b4a339392d10965f5939597d2679ab42929938fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Reinhard=20X=2E=20F=C3=BCrst?= Date: Sat, 8 Aug 2026 12:57:36 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Sonderf=C3=BChrungen=20aus=20dem=20Logb?= =?UTF-8?q?uch=20in=20SoFue2=20nachtragen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RepoSoFue::ausLogbuch() sucht zu einem Logbuch-Eintrag der Art 'SF' den passenden Datensatz über DATE(wtermin) — bei mehreren Führungen an einem Tag gewinnt die zeitlich nächstgelegene — und setzt über das vorhandene updateAfter() stattgefunden, anzahl_echt, bezahlt und remarks. Aufgerufen wird das im Dispatcher nach dem Commit der Logbuch- Transaktion; die Methode wirft nie, sondern liefert einen Status, der als Feld 'sofue' in der Antwort zurückgeht. bezahlt wird wie im beoanswer-Formular geschrieben (Kasse 50€, Überweisung, Spendenkässle, keine). Da SoFue2 latin1 ist und remarks nur 100 Zeichen fasst, kürzt cp1252Safe() den Text und entfernt nicht darstellbare Zeichen. Co-Authored-By: Claude Opus 5 (1M context) --- sternwarte/DB4js_all.php | 92 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 4 deletions(-) diff --git a/sternwarte/DB4js_all.php b/sternwarte/DB4js_all.php index aa545f8..b302fdf 100644 --- a/sternwarte/DB4js_all.php +++ b/sternwarte/DB4js_all.php @@ -588,6 +588,19 @@ class RepoSoFue { return DB::one("SELECT * FROM " . TBL_SOFUE . " WHERE wtermin=? AND deleted=0", [$termin]); } + /** + * Passende Führung zum Beginn eines Logbuch-Eintrags: gleiches Datum, zeitlich am + * nächsten. Der Logbuch-Beginn liegt typisch 0-30 Minuten vor dem wtermin. + */ + public static function findByLogbuchBeginn(string $beginn): ?array + { + return DB::one( + "SELECT id, wtermin FROM " . TBL_SOFUE + . " WHERE deleted=0 AND DATE(wtermin) = DATE(?)" + . " ORDER BY ABS(TIMESTAMPDIFF(MINUTE, ?, wtermin)) ASC, id ASC LIMIT 1", + [$beginn, $beginn] + ); + } public static function getAfterDate(string $date) { // Accept YYYYMMDD or YYYY-MM-DD; compare by DATE(wtermin) @@ -709,6 +722,75 @@ class RepoSoFue { return DB::exec("UPDATE " . TBL_SOFUE . " SET deleted=1 WHERE id=?", [$id]); } + + /** + * Spende aus dem Logbuch als bezahlt-Text — gleiche Schreibweise wie das + * beoanswer-Formular (beoanswer/src/components/LastButtons.jsx). + */ + private static function bezahltText(array $p): string + { + switch ($p['Spende'] ?? '') { + case 'bar': + // Ganze Beträge ohne Nachkommastellen ("Kasse 50€"), sonst mit Komma + // ("Kasse 25,50€") — passt zu den Bestandswerten und in VARCHAR(15). + $betrag = (float)($p['SpendeBetrag'] ?? 0); + $text = fmod($betrag, 1.0) === 0.0 + ? number_format($betrag, 0, ',', '') + : number_format($betrag, 2, ',', ''); + return 'Kasse ' . $text . '€'; + case 'ueberw': return 'Überweisung'; + case 'kasse': return 'Spendenkässle'; + case 'keine': return 'keine'; + default: return ''; + } + } + + /** + * Kürzt auf $max Zeichen und entfernt alles, was die latin1-Spalten von SoFue2 + * nicht darstellen können (z. B. Emoji) — sonst scheitert der UPDATE im Strict Mode. + */ + private static function cp1252Safe(string $s, int $max): string + { + // 'none' lässt nicht abbildbare Zeichen (z. B. Emoji) ersatzlos entfallen, + // statt sie wie iconv//TRANSLIT durch '?' zu ersetzen. + $vorher = mb_substitute_character(); + mb_substitute_character('none'); + $cp = mb_convert_encoding($s, 'CP1252', 'UTF-8'); + mb_substitute_character($vorher); + return mb_substr(mb_convert_encoding($cp, 'UTF-8', 'CP1252'), 0, $max); + } + + /** + * Trägt eine im Logbuch erfasste Sonderführung im Register nach: stattgefunden, + * anzahl_echt, bezahlt und remarks. Der Datensatz wird über das Datum gefunden. + * Wirft nie — der Logbuch-Eintrag ist zu diesem Zeitpunkt bereits gespeichert. + */ + public static function ausLogbuch(array $p): array + { + if (($p['ArtFuehrung'] ?? '') !== 'SF') return ['status' => 'uebersprungen']; + $datum = substr((string)($p['Beginn'] ?? ''), 0, 10); + try { + $treffer = self::findByLogbuchBeginn((string)($p['Beginn'] ?? '')); + if (!$treffer) return ['status' => 'kein_termin', 'datum' => $datum]; + + self::updateAfter((int)$treffer['id'], [ + 'stattgefunden' => 1, + 'besucher' => (int)($p['Besucher'] ?? 0), + 'remark' => self::cp1252Safe((string)($p['Bemerkungen'] ?? ''), 100), + 'bezahlt' => self::bezahltText($p), + ]); + + return [ + 'status' => 'ok', + 'id' => (int)$treffer['id'], + 'wtermin' => $treffer['wtermin'], + 'datum' => $datum, + ]; + } catch (Throwable $e) { + error_log('SoFue-Rueckschreibung: ' . $e->getMessage()); + return ['status' => 'fehler', 'datum' => $datum]; + } + } } class RepoStatistik @@ -1679,8 +1761,8 @@ class Commands 'LB_GET_BEO_ADRESSEN' => 'Logbuch: BEOs mit Adressen (km-Berechnung)', 'LB_UPDATE_BEO_KM' => 'Logbuch: km-Entfernung eines BEOs setzen', 'LB_LIST_LOGBUCH' => 'Logbuch: Einträge abrufen', - 'LB_CREATE_LOGBUCH' => 'Logbuch: Eintrag anlegen', - 'LB_UPDATE_LOGBUCH' => 'Logbuch: Eintrag ändern', + 'LB_CREATE_LOGBUCH' => 'Logbuch: Eintrag anlegen (trägt Sonderführungen in SoFue2 nach)', + 'LB_UPDATE_LOGBUCH' => 'Logbuch: Eintrag ändern (trägt Sonderführungen in SoFue2 nach)', 'LB_DELETE_LOGBUCH' => 'Logbuch: Eintrag löschen', 'LB_GET_BEOS' => 'Logbuch: Guide-BEOs abrufen', 'LB_GET_OBJEKTE' => 'Logbuch: Objekte (zuletzt genutzt)', @@ -2192,13 +2274,15 @@ Spendenbescheinigung: \t" . $info['spende'] . " respond(RepoLogbuch::listLogbuch($input)); case 'LB_CREATE_LOGBUCH': $newId = RepoLogbuch::createLogbuch($input); - respond(['id' => $newId], 201); + // Erst nach dem Commit: eine Sonderführung wird im Register nachgetragen, + // ein Problem dabei darf den Logbuch-Eintrag nicht zurückrollen. + respond(['id' => $newId, 'sofue' => RepoSoFue::ausLogbuch($input)], 201); case 'LB_UPDATE_LOGBUCH': $id = (int)($input['id'] ?? respondError('id missing')); $userId = (int)($input['user_id'] ?? respondError('user_id missing')); $userRole = (string)($input['user_role'] ?? ''); RepoLogbuch::updateLogbuch($id, $userId, $userRole, $input); - respond(['ok' => true]); + respond(['ok' => true, 'sofue' => RepoSoFue::ausLogbuch($input)]); case 'LB_DELETE_LOGBUCH': $id = (int)($input['id'] ?? respondError('id missing')); $userId = (int)($input['user_id'] ?? respondError('user_id missing'));