feat: Sonderführungen aus dem Logbuch in SoFue2 nachtragen

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) <noreply@anthropic.com>
This commit is contained in:
2026-08-08 12:57:36 +02:00
parent 8b2b421f63
commit b4a339392d
+88 -4
View File
@@ -588,6 +588,19 @@ class RepoSoFue
{ {
return DB::one("SELECT * FROM " . TBL_SOFUE . " WHERE wtermin=? AND deleted=0", [$termin]); 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) public static function getAfterDate(string $date)
{ {
// Accept YYYYMMDD or YYYY-MM-DD; compare by DATE(wtermin) // 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]); 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 class RepoStatistik
@@ -1679,8 +1761,8 @@ class Commands
'LB_GET_BEO_ADRESSEN' => 'Logbuch: BEOs mit Adressen (km-Berechnung)', 'LB_GET_BEO_ADRESSEN' => 'Logbuch: BEOs mit Adressen (km-Berechnung)',
'LB_UPDATE_BEO_KM' => 'Logbuch: km-Entfernung eines BEOs setzen', 'LB_UPDATE_BEO_KM' => 'Logbuch: km-Entfernung eines BEOs setzen',
'LB_LIST_LOGBUCH' => 'Logbuch: Einträge abrufen', 'LB_LIST_LOGBUCH' => 'Logbuch: Einträge abrufen',
'LB_CREATE_LOGBUCH' => 'Logbuch: Eintrag anlegen', 'LB_CREATE_LOGBUCH' => 'Logbuch: Eintrag anlegen (trägt Sonderführungen in SoFue2 nach)',
'LB_UPDATE_LOGBUCH' => 'Logbuch: Eintrag ändern', 'LB_UPDATE_LOGBUCH' => 'Logbuch: Eintrag ändern (trägt Sonderführungen in SoFue2 nach)',
'LB_DELETE_LOGBUCH' => 'Logbuch: Eintrag löschen', 'LB_DELETE_LOGBUCH' => 'Logbuch: Eintrag löschen',
'LB_GET_BEOS' => 'Logbuch: Guide-BEOs abrufen', 'LB_GET_BEOS' => 'Logbuch: Guide-BEOs abrufen',
'LB_GET_OBJEKTE' => 'Logbuch: Objekte (zuletzt genutzt)', 'LB_GET_OBJEKTE' => 'Logbuch: Objekte (zuletzt genutzt)',
@@ -2192,13 +2274,15 @@ Spendenbescheinigung: \t" . $info['spende'] . "
respond(RepoLogbuch::listLogbuch($input)); respond(RepoLogbuch::listLogbuch($input));
case 'LB_CREATE_LOGBUCH': case 'LB_CREATE_LOGBUCH':
$newId = RepoLogbuch::createLogbuch($input); $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': case 'LB_UPDATE_LOGBUCH':
$id = (int)($input['id'] ?? respondError('id missing')); $id = (int)($input['id'] ?? respondError('id missing'));
$userId = (int)($input['user_id'] ?? respondError('user_id missing')); $userId = (int)($input['user_id'] ?? respondError('user_id missing'));
$userRole = (string)($input['user_role'] ?? ''); $userRole = (string)($input['user_role'] ?? '');
RepoLogbuch::updateLogbuch($id, $userId, $userRole, $input); RepoLogbuch::updateLogbuch($id, $userId, $userRole, $input);
respond(['ok' => true]); respond(['ok' => true, 'sofue' => RepoSoFue::ausLogbuch($input)]);
case 'LB_DELETE_LOGBUCH': case 'LB_DELETE_LOGBUCH':
$id = (int)($input['id'] ?? respondError('id missing')); $id = (int)($input['id'] ?? respondError('id missing'));
$userId = (int)($input['user_id'] ?? respondError('user_id missing')); $userId = (int)($input['user_id'] ?? respondError('user_id missing'));