From f39ade0e9d9508a46a9c255caa2258a4141bb129 Mon Sep 17 00:00:00 2001 From: Tim <43742253+TQxTim@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:36:39 +0200 Subject: [PATCH] Implement Player Sync to DTFB (#286) --- .github/workflows/sync-dtfb-nightly.yml | 22 + .../components/com_sportsmanager/admin.php | 51 ++ .../com_sportsmanager/database/update.php | 35 + .../com_sportsmanager/sportsmanager.php | 8 + .../components/com_sportsmanager/sync.php | 833 ++++++++++++++++++ .../views/sportsmanager/view.html.php | 250 +++--- .../views/sportsmanager/view_admin.php | 27 + .../views/sportsmanager/view_ticker.php | 143 +++ 8 files changed, 1244 insertions(+), 125 deletions(-) create mode 100644 .github/workflows/sync-dtfb-nightly.yml create mode 100644 src/structure/components/com_sportsmanager/sync.php diff --git a/.github/workflows/sync-dtfb-nightly.yml b/.github/workflows/sync-dtfb-nightly.yml new file mode 100644 index 0000000..39fc7ed --- /dev/null +++ b/.github/workflows/sync-dtfb-nightly.yml @@ -0,0 +1,22 @@ +name: Nightly DTFB Player Sync +on: + schedule: + - cron: '0 2 * * *' # Every night at 2:00 AM UTC + workflow_dispatch: # Allow manual trigger from GitHub + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Trigger DTFB Sync + run: | + response=$(curl -s -o /dev/null -w "%{http_code}" \ + -X POST \ + -H "Authorization: Bearer ${{ secrets.DTFB_SYNC_KEY }}" \ + -H "Content-Type: application/json" \ + "${{ secrets.DTFB_SYNC_TRIGGER_URL }}") + if [ "$response" != "200" ]; then + echo "Sync failed with HTTP $response" + exit 1 + fi + echo "Sync triggered successfully" diff --git a/src/structure/components/com_sportsmanager/admin.php b/src/structure/components/com_sportsmanager/admin.php index a2cfd9d..f04453f 100644 --- a/src/structure/components/com_sportsmanager/admin.php +++ b/src/structure/components/com_sportsmanager/admin.php @@ -817,6 +817,7 @@ function adminEinstellungen(): void $spielerimport_persoenliche_daten_vorauswahl = $jInput->get('spielerimport_persoenliche_daten_vorauswahl', 0, 'INT'); $api_push_key = $jInput->get('api_push_key', '', 'RAW'); + $dtfb_sync_url = $db->escape(trim($jInput->get('dtfb_sync_url', '', 'RAW'))); $query = "REPLACE #__sportsmanager_einstellungen" . "\n SET name = 'verbands_kuerzel'" @@ -950,6 +951,12 @@ function adminEinstellungen(): void die($db->stderr(true)); } + $query = "REPLACE #__sportsmanager_einstellungen SET name = 'dtfb_sync_url', wert = '$dtfb_sync_url'"; + $db->setQuery($query); + if (!$db->execute()) { + die($db->stderr(true)); + } + redirectSportsManagerURL('&task=admin_uebersicht'); } @@ -3419,6 +3426,50 @@ function adminExportSpielerForm(): void die(); } +#[NoReturn] function adminSyncSpielerToDtfb(): void +{ + $jInput = Factory::getContainer()->get(SiteApplication::class)->input; + + if (!benutzerZugriff("spieler_aendern")) { + keinZugriff(); + } + + if ($jInput->get('cancel', false, 'BOOL')) { + redirectSportsManagerURL('&task=admin_spieler'); + } + + $csvData = syncExportSpielerCSV(); + if (empty($csvData)) { + redirectSportsManagerURL('&task=admin_spieler', 'Keine Spieler zum Synchronisieren gefunden.'); + } + + $res = syncPushToDtfb($csvData); + + // Log the sync + syncLogEntry( + 'push', + 'manual', + $res['success'] ? 'success' : 'error', + $res['spieler_count'] ?? 0, + $res['spieler_updated'] ?? 0, + $res['spieler_added'] ?? 0, + $res['message'] ?? '', + '' + ); + + if ($res['success']) { + $msg = sprintf( + 'Erfolgreich mit DTFB synchronisiert! Spieler gesamt: %d, aktualisiert: %d, neu hinzugefügt: %d.', + $res['spieler_count'] ?? 0, + $res['spieler_updated'] ?? 0, + $res['spieler_added'] ?? 0 + ); + redirectSportsManagerURL('&task=admin_spieler', $msg); + } else { + redirectSportsManagerURL('&task=admin_spieler', 'Sync-Fehler: ' . $res['message']); + } +} + #[NoReturn] function adminExportSpielerSport(): void { $db = getDatabase(); diff --git a/src/structure/components/com_sportsmanager/database/update.php b/src/structure/components/com_sportsmanager/database/update.php index 0fa7438..34595a9 100644 --- a/src/structure/components/com_sportsmanager/database/update.php +++ b/src/structure/components/com_sportsmanager/database/update.php @@ -5708,6 +5708,41 @@ function updateDatabase(): void } } + if ($datenbank_version < 120) { + $query = "CREATE TABLE IF NOT EXISTS `#__sportsmanager_sync_log` (" + . "\n `sync_id` INT(11) NOT NULL AUTO_INCREMENT," + . "\n `sync_timestamp` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP," + . "\n `sync_direction` ENUM('push', 'receive') NOT NULL," + . "\n `sync_trigger` ENUM('manual', 'cron', 'api') NOT NULL," + . "\n `sync_status` ENUM('success', 'error') NOT NULL," + . "\n `spieler_count` INT(11) DEFAULT 0," + . "\n `spieler_updated` INT(11) DEFAULT 0," + . "\n `spieler_added` INT(11) DEFAULT 0," + . "\n `message` TEXT," + . "\n `details` TEXT," + . "\n PRIMARY KEY (`sync_id`)," + . "\n INDEX `idx_timestamp` (`sync_timestamp`)" + . "\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"; + $db->setQuery($query); + if (!$db->execute()) { + die($db->stderr(true)); + } + + $query = "INSERT IGNORE #__sportsmanager_einstellungen SET name = 'dtfb_sync_url', wert = '';"; + $db->setQuery($query); + if (!$db->execute()) { + die($db->stderr(true)); + } + + $query = "UPDATE #__sportsmanager_einstellungen" + . "\n SET wert = '120'" + . "\n WHERE name = 'datenbank_version'"; + $db->setQuery($query); + if (!$db->execute()) { + die($db->stderr(true)); + } + } + if ($termin_aktionen_email_setzen) { $query = "SELECT aktion_user_id, termin_aktion_id" . "\n FROM #__sportsmanager_termin_aktion"; diff --git a/src/structure/components/com_sportsmanager/sportsmanager.php b/src/structure/components/com_sportsmanager/sportsmanager.php index a43a0c6..7e4c131 100644 --- a/src/structure/components/com_sportsmanager/sportsmanager.php +++ b/src/structure/components/com_sportsmanager/sportsmanager.php @@ -44,6 +44,7 @@ require_once JPATH_SITE . '/components/com_sportsmanager/views/sportsmanager/vie require_once JPATH_SITE . '/components/com_sportsmanager/util/image.php'; require_once JPATH_SITE . '/components/com_sportsmanager/util/email.php'; require_once JPATH_SITE . '/components/com_sportsmanager/database/update.php'; // will also include init.php and util.php +require_once JPATH_SITE . '/components/com_sportsmanager/sync.php'; initDatabase(); updateDatabase(); @@ -74,6 +75,10 @@ if ($task == "spielerbild") { terminDokument(); } else if ($task == "spieler_details") { spielerDetails(); +} else if ($task === 'api_sync_spieler_receive') { + apiSyncSpielerReceive(); +} else if ($task === 'api_sync_spieler_trigger') { + apiSyncSpielerTrigger(); } else if ($task !== null && str_starts_with($task, "admin_")) { // in some cases there are no breaks needed due to no return from method switch ($task) { @@ -134,6 +139,9 @@ if ($task == "spielerbild") { case 'admin_spieler_export_sport': adminExportSpielerSport(); break; + case 'admin_spieler_sync_dtfb': + adminSyncSpielerToDtfb(); + break; case 'admin_spieler_remove_inaktive_form': adminRemoveInaktiveSpielerForm(); break; diff --git a/src/structure/components/com_sportsmanager/sync.php b/src/structure/components/com_sportsmanager/sync.php new file mode 100644 index 0000000..4531aee --- /dev/null +++ b/src/structure/components/com_sportsmanager/sync.php @@ -0,0 +1,833 @@ +escape($direction) . "'," + . "\n sync_trigger = '" . $db->escape($trigger) . "'," + . "\n sync_status = '" . $db->escape($status) . "'," + . "\n spieler_count = " . intval($spieler_count) . "," + . "\n spieler_updated = " . intval($spieler_updated) . "," + . "\n spieler_added = " . intval($spieler_added) . "," + . "\n message = '" . $db->escape($message) . "'," + . "\n details = '" . $db->escape($details) . "'"; + $db->setQuery($query); + $db->execute(); + } catch (Exception $e) { + error_log("Failed to write sync log: " . $e->getMessage()); + } +} + +/** + * Returns HTML displaying the status of the last sync operation. + * + * @return string + */ +function syncGetLastStatus(): string +{ + $db = getDatabase(); + $query = "SELECT * FROM #__sportsmanager_sync_log ORDER BY sync_id DESC LIMIT 1"; + $rows = loadObjectList($db, $query); + if (count($rows) === 0) { + return "Noch nie synchronisiert"; + } + $row = $rows[0]; + + $statusClass = $row->sync_status === 'success' ? 'uk-text-success' : 'uk-text-danger'; + $statusText = $row->sync_status === 'success' ? 'Erfolgreich' : 'Fehlgeschlagen'; + $directionText = $row->sync_direction === 'push' ? 'Export (Push)' : 'Import (Receive)'; + $triggerText = $row->sync_trigger === 'manual' ? 'Manuell' : ($row->sync_trigger === 'cron' ? 'Cron' : 'API'); + + $stats = ""; + if ($row->sync_status === 'success') { + $stats = sprintf( + " (Spieler gesamt: %d, Aktualisiert: %d, Hinzugefügt: %d)", + $row->spieler_count, + $row->spieler_updated, + $row->spieler_added + ); + } else { + $stats = " (Fehler: " . htmlspecialchars($row->message) . ")"; + } + + return sprintf( + "%s am %s via %s / %s%s", + $statusClass, + $statusText, + date('d.m.Y H:i:s', strtotime($row->sync_timestamp)), + $directionText, + $triggerText, + $stats + ); +} + +/** + * Exports player data to a tab-separated CSV string. + * Excludes personal contact details and images. + * + * @return string + */ +function syncExportSpielerCSV(): string +{ + $db = getDatabase(); + $jahr = date("Y"); + $query = "SELECT nachname, vorname, spielernr, lizenznr, lizenz, geschlecht"; + $query .= ",\n IF(ISNULL(geburtsjahr), IF(geschlecht = 'M', 'H', 'D'), IF(" . ($jahr - 18) . " <= geburtsjahr, 'J', IF(" . ($jahr - 50) . " > geburtsjahr, 'S', IF(geschlecht = 'M', 'H', 'D')))) AS kategorie"; + $query .= ",\n vereinsname as verein, vereinssitz, veranstalterbezeichnung as organisation, IF(mitgliedsstatus = 1, 'Aktiv', IF(mitgliedsstatus = 0, 'Ausgetreten', IF(mitgliedsstatus = 2, 'Eingeschränkt', 'Passiv'))) AS mitgliedsstatus"; + $query .= ",\n geburtsjahr"; + $query .= "\n FROM #__sportsmanager_spieler"; + $query .= "\n LEFT JOIN #__sportsmanager_mitglied_von_verein ON #__sportsmanager_spieler.spieler_id = #__sportsmanager_mitglied_von_verein.spieler_id AND #__sportsmanager_mitglied_von_verein.verein_id = #__sportsmanager_spieler.aktueller_verein_id" + . "\n LEFT JOIN #__sportsmanager_verein ON #__sportsmanager_verein.verein_id = #__sportsmanager_spieler.aktueller_verein_id" + . "\n LEFT JOIN #__sportsmanager_veranstalter ON #__sportsmanager_veranstalter.veranstalter_id = #__sportsmanager_verein.veranstalter_id"; + $query .= "\n WHERE NOT ISNULL(spielernr) AND spielernr != ''"; + $query .= "\n ORDER BY nachname, vorname"; + + $rows = loadObjectList($db, $query); + if (count($rows) === 0) { + return ""; + } + + $trennzeichen = "\t"; + $header = ""; + foreach ($rows[0] as $field => $value) { + $header .= $field . $trennzeichen; + } + $header = rtrim($header, $trennzeichen); + + $data = ""; + foreach ($rows as $row) { + $line = ''; + foreach ($row as $value) { + if ((!isset($value)) or ($value === "")) { + $value = $trennzeichen; + } else { + $value = str_replace('"', '""', $value); + $value = str_replace("\t", ' ', $value); + $value = str_replace("\r", '', $value); + $value = str_replace("\n", ' ', $value); + $value = '="' . $value . '"' . $trennzeichen; + } + $line .= $value; + } + $data .= rtrim($line, $trennzeichen) . "\n"; + } + $data = str_replace("\r", "", $data); + + return "sep=" . $trennzeichen . "\n" . $header . "\n" . $data; +} + +/** + * Pushes the exported CSV data to DTFB (dtfb_sync_url) via cURL. + * + * @param string $csvData + * @return array + */ +function syncPushToDtfb(string $csvData): array +{ + $push_key = einstellungswert("api_push_key"); + $sync_url = einstellungswert("dtfb_sync_url"); + + if (empty($sync_url)) { + return [ + 'success' => false, + 'message' => 'Sync-URL nicht konfiguriert.' + ]; + } + if (empty($push_key)) { + return [ + 'success' => false, + 'message' => 'API Push Key nicht konfiguriert.' + ]; + } + + $ch = curl_init($sync_url); + if (!$ch) { + return [ + 'success' => false, + 'message' => 'Initialisierung von cURL fehlgeschlagen.' + ]; + } + + curl_setopt_array($ch, array( + CURLOPT_POST => TRUE, + CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_HTTPHEADER => array( + 'Authorization: Bearer ' . $push_key, + 'Content-Type: text/csv; charset=utf-8', + ), + CURLOPT_TIMEOUT => 60, + CURLOPT_POSTFIELDS => $csvData, + )); + + $resp = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + if (curl_errno($ch)) { + $error_msg = curl_error($ch); + curl_close($ch); + return [ + 'success' => false, + 'message' => 'cURL-Fehler: ' . $error_msg + ]; + } + curl_close($ch); + + if ($http_code !== 200) { + return [ + 'success' => false, + 'message' => 'HTTP-Status ' . $http_code . ': ' . $resp + ]; + } + + $result = json_decode($resp, true); + if (json_last_error() === JSON_ERROR_NONE) { + if (isset($result['success']) && $result['success']) { + return [ + 'success' => true, + 'message' => $result['message'] ?? 'Erfolgreich synchronisiert.', + 'spieler_count' => $result['spieler_count'] ?? 0, + 'spieler_updated' => $result['spieler_updated'] ?? 0, + 'spieler_added' => $result['spieler_added'] ?? 0 + ]; + } else { + return [ + 'success' => false, + 'message' => $result['error'] ?? $result['message'] ?? 'Import auf Empfängerseite fehlgeschlagen.' + ]; + } + } + + if (str_contains(strtolower($resp), 'success')) { + return [ + 'success' => true, + 'message' => 'Erfolgreich synchronisiert (Klartext-Antwort).' + ]; + } + + return [ + 'success' => false, + 'message' => 'Unerwartete Antwort vom Server: ' . substr($resp, 0, 200) + ]; +} + +/** + * Processes incoming CSV data and imports it into the local database. + * Aborts and returns an error if any organization name in the CSV cannot + * be matched with an existing local organization. + * + * @param string $csvData + * @return array + */ +function syncReceiveSpielerImport(string $csvData): array +{ + $db = getDatabase(); + + $lines = explode("\n", str_replace("\r", "", $csvData)); + if (count($lines) < 2) { + return [ + 'success' => false, + 'message' => 'Keine Daten in der CSV-Datei gefunden.' + ]; + } + + $lineIdx = 0; + $titelzeile = trim($lines[$lineIdx]); + + if (str_starts_with($titelzeile, "sep=")) { + $trennzeichen = substr($titelzeile, 4); + if ($trennzeichen === "") { + $trennzeichen = "\t"; + } + $lineIdx++; + if (isset($lines[$lineIdx])) { + $titelzeile = trim($lines[$lineIdx]); + } else { + return [ + 'success' => false, + 'message' => 'CSV-Datei enthält nach sep= keine Titelzeile.' + ]; + } + } else { + $trennzeichen = "\t"; + } + + $titel = explode($trennzeichen, strtolower($titelzeile)); + $spalte = array(); + foreach ($titel as $index => $bezeichnung) { + $bezeichnung = trim($bezeichnung); + $len = strlen($bezeichnung); + if ($len >= 2 && $bezeichnung[0] === '"' && $bezeichnung[$len - 1] === '"') { + $bezeichnung = trim(str_replace('""', '"', substr($bezeichnung, 1, $len - 2))); + } + + if ($bezeichnung === "name" || $bezeichnung === "nachname") { + $spalte["nachname"] = $index; + } else if ($bezeichnung === "vorname") { + $spalte["vorname"] = $index; + } else if ($bezeichnung === "name, vorname" || $bezeichnung === "name,vorname") { + $spalte["name,vorname"] = $index; + } else if ($bezeichnung === "pseudonym") { + $spalte["pseudonym"] = $index; + } else if ($bezeichnung === "geschlecht" || $bezeichnung === "anrede") { + $spalte["geschlecht"] = $index; + } else if ($bezeichnung === "spielernr" || $bezeichnung === "spielernr." || $bezeichnung === "spielerpass") { + $spalte["spielernr"] = $index; + } else if ($bezeichnung === "spielernr alt" || $bezeichnung === "spielernr. alt" || $bezeichnung === "spielernr_alt") { + $spalte["spielernr_alt"] = $index; + } else if ($bezeichnung === "lizenznr" || $bezeichnung === "lizenznr.") { + $spalte["lizenznr"] = $index; + } else if ($bezeichnung === "organisation") { + $spalte["organisation"] = $index; + } else if ($bezeichnung === "vereinssitz") { + $spalte["vereinssitz"] = $index; + } else if ($bezeichnung === "vereinsname" || $bezeichnung === "verein") { + $spalte["vereinsname"] = $index; + } else if ($bezeichnung === "geburtsdatum") { + $spalte["geburtsdatum"] = $index; + } else if ($bezeichnung === "geburtsjahr") { + $spalte["geburtsjahr"] = $index; + } else if ($bezeichnung === "email" || $bezeichnung === "e-mail") { + $spalte["email"] = $index; + } else if (str_starts_with($bezeichnung, "stra")) { + $spalte["strasse"] = $index; + } else if ($bezeichnung === "plz/ort") { + $spalte["plz/ort"] = $index; + } else if ($bezeichnung === "plz") { + $spalte["plz"] = $index; + } else if ($bezeichnung === "ort") { + $spalte["ort"] = $index; + } else if ($bezeichnung === "landeskennung") { + $spalte["landeskennung"] = $index; + } else if ($bezeichnung === "telefon") { + $spalte["telefon"] = $index; + } else if ($bezeichnung === "mobil") { + $spalte["mobil"] = $index; + } else if ($bezeichnung === "austritt" || $bezeichnung === "ausgetreten") { + $spalte["ausgetreten"] = $index; + } else if ($bezeichnung === "mitgliedsstatus") { + $spalte["mitgliedsstatus"] = $index; + } + } + + if (((!isset($spalte["nachname"]) || !isset($spalte["vorname"])) && !isset($spalte["name,vorname"])) || !isset($spalte["spielernr"])) { + return [ + 'success' => false, + 'message' => 'Die übergebene Datei ist keine gültige Spielerdatei (erforderliche Spalten fehlen).' + ]; + } + + $lineIdx++; + $session_id = 'sync_recv_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)); + + $organisations = []; + $rows_to_insert = []; + + for ($i = $lineIdx; $i < count($lines); $i++) { + $buffer = trim($lines[$i]); + if ($buffer === "") { + continue; + } + + $daten = explode($trennzeichen, $buffer); + foreach ($daten as $index => $wert) { + $wert = trim($wert); + $len = strlen($wert); + if ($len < 2 || $wert[$len - 1] !== '"' || !($wert[0] === '"' || ($wert[0] === '=' && $wert[1] === '"'))) { + $daten[$index] = $wert; + } else if ($wert[0] === '"') { + $daten[$index] = trim(str_replace('""', '"', substr($wert, 1, $len - 2))); + } else { + $daten[$index] = trim(str_replace('""', '"', substr($wert, 2, $len - 3))); + } + } + + if (isset($spalte["vorname"]) && isset($spalte["nachname"]) && isset($daten[$spalte["vorname"]]) && isset($daten[$spalte["nachname"]])) { + $nachname = $daten[$spalte["nachname"]]; + $vorname = $daten[$spalte["vorname"]]; + } else if (isset($spalte["name,vorname"]) && isset($daten[$spalte["name,vorname"]])) { + $pos = strpos($daten[$spalte["name,vorname"]], ","); + if ($pos === false) { + continue; + } + $nachname = trim(substr($daten[$spalte["name,vorname"]], 0, $pos)); + $vorname = trim(substr($daten[$spalte["name,vorname"]], $pos + 1)); + } else { + continue; + } + + if ($vorname === "" || $nachname === "") { + continue; + } + + $mitgliedsstatus = 1; + if (isset($spalte["mitgliedsstatus"]) && !empty($daten[$spalte["mitgliedsstatus"]])) { + $s = strtolower($daten[$spalte["mitgliedsstatus"]]); + if ($s === "ausgetreten") { + $mitgliedsstatus = 0; + } else if ($s === "passiv") { + $mitgliedsstatus = 3; + } else if (str_starts_with($s, "eingeschr")) { + $mitgliedsstatus = 2; + } + } else if (isset($spalte["ausgetreten"]) && !empty($daten[$spalte["ausgetreten"]])) { + if (strtolower($daten[$spalte["ausgetreten"]]) === "ja") { + $mitgliedsstatus = 0; + } + } + + if ($mitgliedsstatus == 0) { + continue; + } + + $geschlecht = isset($spalte["geschlecht"]) && isset($daten[$spalte["geschlecht"]]) ? (($daten[$spalte["geschlecht"]][0] === "M" || $daten[$spalte["geschlecht"]][0] === "m" || $daten[$spalte["geschlecht"]][0] === "H" || $daten[$spalte["geschlecht"]][0] === "h") ? "M" : "W") : "M"; + $spielernr = isset($daten[$spalte["spielernr"]]) ? trim($daten[$spalte["spielernr"]]) : ""; + if (!empty($spielernr) && !ctype_digit(substr($spielernr, strlen($spielernr) - 1, 1))) { + $spielernr = ""; + } + + $spielernr_alt = isset($spalte["spielernr_alt"]) && isset($daten[$spalte["spielernr_alt"]]) ? trim($daten[$spalte["spielernr_alt"]]) : ""; + $lizenznr = isset($spalte["lizenznr"]) && isset($daten[$spalte["lizenznr"]]) ? $daten[$spalte["lizenznr"]] : ""; + if (!empty($lizenznr) && !ctype_digit(substr($lizenznr, strlen($lizenznr) - 1, 1))) { + $lizenznr = ""; + } + + $pseudonym = isset($spalte["pseudonym"]) && isset($daten[$spalte["pseudonym"]]) ? $daten[$spalte["pseudonym"]] : ""; + $organisation = isset($spalte["organisation"]) && isset($daten[$spalte["organisation"]]) ? $daten[$spalte["organisation"]] : ""; + $vereinssitz = isset($spalte["vereinssitz"]) && isset($daten[$spalte["vereinssitz"]]) ? $daten[$spalte["vereinssitz"]] : ""; + $vereinsname = isset($spalte["vereinsname"]) && isset($daten[$spalte["vereinsname"]]) ? $daten[$spalte["vereinsname"]] : ""; + + $geburtsjahr = isset($spalte["geburtsjahr"]) && isset($daten[$spalte["geburtsjahr"]]) ? $daten[$spalte["geburtsjahr"]] : null; + if (empty($geburtsjahr) || !ctype_digit($geburtsjahr) || $geburtsjahr < 1800) { + $geburtsjahr = null; + } + + if (!empty($organisation)) { + $organisations[trim($organisation)] = true; + } + + $rows_to_insert[] = [ + 'vorname' => $vorname, + 'nachname' => $nachname, + 'spielernr' => $spielernr, + 'spielernr_alt' => $spielernr_alt, + 'lizenznr' => $lizenznr, + 'pseudonym' => $pseudonym, + 'organisation' => $organisation, + 'vereinssitz' => $vereinssitz, + 'vereinsname' => $vereinsname, + 'geburtsjahr' => $geburtsjahr, + 'mitgliedsstatus' => $mitgliedsstatus, + 'geschlecht' => $geschlecht + ]; + } + + if (empty($rows_to_insert)) { + return [ + 'success' => false, + 'message' => 'Keine gültigen Spielerzeilen zum Importieren gefunden.' + ]; + } + + // Auto-match Veranstalter by name. If it does not match, abort. + $org_map = []; + foreach (array_keys($organisations) as $orgName) { + $query = "SELECT veranstalter_id FROM #__sportsmanager_veranstalter WHERE veranstalterbezeichnung = '" . $db->escape($orgName) . "'"; + $res = loadObjectList($db, $query); + if (count($res) === 0) { + return [ + 'success' => false, + 'message' => 'Veranstalter "' . $orgName . '" existiert nicht auf diesem Empfänger-System. Import abgebrochen.' + ]; + } + $org_map[$orgName] = intval($res[0]->veranstalter_id); + } + + // Insert into staging table + foreach ($rows_to_insert as $row) { + $query = "INSERT INTO #__sportsmanager_spieler_import" + . "\n SET session_id = '" . $db->escape($session_id) . "'," + . "\n vorname = '" . $db->escape($row['vorname']) . "'," + . "\n nachname = '" . $db->escape($row['nachname']) . "'," + . "\n spielernr = '" . $db->escape($row['spielernr']) . "'," + . "\n spielernr_alt = '" . $db->escape($row['spielernr_alt']) . "'," + . "\n lizenznr = '" . $db->escape($row['lizenznr']) . "'," + . "\n pseudonym = '" . $db->escape($row['pseudonym']) . "'," + . "\n geschlecht = '" . $db->escape($row['geschlecht']) . "'," + . "\n geburtsjahr = " . ($row['geburtsjahr'] === null ? "NULL" : "'" . $db->escape($row['geburtsjahr']) . "'") . "," + . "\n vereinsname = '" . $db->escape($row['vereinsname']) . "'," + . "\n vereinssitz = '" . $db->escape($row['vereinssitz']) . "'," + . "\n veranstalterbezeichnung = '" . $db->escape($row['organisation']) . "'," + . "\n mitgliedsstatus = '" . $row['mitgliedsstatus'] . "'"; + $db->setQuery($query); + if (!$db->execute()) { + return [ + 'success' => false, + 'message' => 'Fehler beim Schreiben in die Staging-Tabelle: ' . $db->stderr() + ]; + } + } + + // Clean up older sync runs staging data (older than 5 minutes) + $query = "DELETE FROM #__sportsmanager_spieler_import WHERE session_id < SUBTIME(NOW(), '00:05:00') AND session_id LIKE 'sync_recv_%'"; + $db->setQuery($query); + $db->execute(); + + // Fetch staging players with matching ID + $query = "SELECT #__sportsmanager_spieler_import.*, #__sportsmanager_spieler.spieler_id" + . "\n FROM #__sportsmanager_spieler_import" + . "\n LEFT JOIN #__sportsmanager_spieler ON #__sportsmanager_spieler_import.spielernr != '' AND #__sportsmanager_spieler_import.spielernr = #__sportsmanager_spieler.spielernr" + . "\n WHERE session_id = '" . $db->escape($session_id) . "'"; + $spieler_import = loadObjectList($db, $query); + + // Deactivate all memberships for involved organisations temporarily + foreach ($org_map as $orgName => $veranstalterId) { + $query = "UPDATE #__sportsmanager_mitglied_von_verein INNER JOIN #__sportsmanager_verein USING (verein_id)" + . "\n SET mitgliedsstatus = 0," + . "\n #__sportsmanager_mitglied_von_verein.ausgetreten = TRUE" + . "\n WHERE veranstalter_id = " . $veranstalterId; + $db->setQuery($query); + if (!$db->execute()) { + return [ + 'success' => false, + 'message' => 'Fehler beim Deaktivieren der alten Vereinsmitgliedschaften.' + ]; + } + } + + $query = "SELECT * FROM #__sportsmanager_einstellungen WHERE name = 'basis_spielernr'"; + $rows_settings = loadObjectList($db, $query); + $naechste_spielernr = count($rows_settings) > 0 ? $rows_settings[0]->wert : "1"; + if (empty($naechste_spielernr)) { + $naechste_spielernr = "1"; + } + + $spieler_updated = 0; + $spieler_added = 0; + $spielerIdsHinzugefuegt = array(); + + foreach ($spieler_import as $t) { + $orgName = $t->veranstalterbezeichnung; + $veranstalterId = $org_map[$orgName] ?? -1; + + if ($veranstalterId === -1 && !empty($orgName)) { + continue; + } + + $spieler_id = $t->spieler_id; + $nachname = $t->nachname; + $vorname = $t->vorname; + $geschlecht = $t->geschlecht; + $lizenznr = $t->lizenznr; + $pseudonym = $t->pseudonym; + $vereinsname = $t->vereinsname; + $vereinssitz = $t->vereinssitz; + $geburtsjahr = $t->geburtsjahr; + $spielernr = $t->spielernr; + $mitgliedsstatus = $t->mitgliedsstatus; + + if ($spieler_id === null && !empty($spielernr) && isset($spielerIdsHinzugefuegt[$spielernr])) { + $spieler_id = $spielerIdsHinzugefuegt[$spielernr]; + } + + if ($spieler_id === null && empty($vereinsname)) { + continue; + } + + if ($spieler_id !== null) { + $query = "UPDATE #__sportsmanager_spieler" + . "\n SET vorname = '" . $db->escape($vorname) . "'," + . "\n nachname = '" . $db->escape($nachname) . "'," + . "\n geschlecht = '" . $db->escape($geschlecht) . "'"; + if (!empty($lizenznr)) { + $query .= ",\n lizenznr = '" . $db->escape($lizenznr) . "'"; + } + if ($geburtsjahr !== null) { + $query .= ",\n geburtsjahr = '" . $db->escape($geburtsjahr) . "'"; + } + if (!empty($pseudonym)) { + $query .= ",\n pseudonym = '" . $db->escape($pseudonym) . "'"; + } + $query .= "\n WHERE spieler_id = " . intval($spieler_id); + $db->setQuery($query); + if (!$db->execute()) { + return [ + 'success' => false, + 'message' => 'Fehler beim Aktualisieren des Spielers ID ' . $spieler_id + ]; + } + $spieler_updated++; + } else { + if (empty($spielernr)) { + $spielernr = $naechste_spielernr; + } + + $query = "INSERT INTO #__sportsmanager_spieler" + . "\n SET vorname = '" . $db->escape($vorname) . "'," + . "\n nachname = '" . $db->escape($nachname) . "'," + . "\n spielernr = '" . $db->escape($spielernr) . "'," + . "\n lizenznr = '" . $db->escape($lizenznr) . "'," + . "\n geschlecht = '" . $db->escape($geschlecht) . "'," + . "\n geburtsjahr = " . ($geburtsjahr === null ? "NULL" : "'" . $db->escape($geburtsjahr) . "'"); + if (!empty($pseudonym)) { + $query .= ",\n pseudonym = '" . $db->escape($pseudonym) . "'"; + } + $db->setQuery($query); + if (!$db->execute()) { + return [ + 'success' => false, + 'message' => 'Fehler beim Anlegen des neuen Spielers ' . $vorname . ' ' . $nachname + ]; + } + $spieler_id = $db->insertid(); + $spielerIdsHinzugefuegt[$spielernr] = $spieler_id; + + if ($spielernr === $naechste_spielernr) { + do { + for ($idx = strlen($naechste_spielernr) - 1; $idx >= 0; $idx--) { + if ($naechste_spielernr[$idx] < '0' || $naechste_spielernr[$idx] > '9') { + $naechste_spielernr = substr($naechste_spielernr, 0, $idx + 1) . "1" . substr($naechste_spielernr, $idx + 1); + break; + } + if ($naechste_spielernr[$idx] <= '8') { + $naechste_spielernr[$idx] = $naechste_spielernr[$idx] + 1; + break; + } + $naechste_spielernr[$idx] = '0'; + } + if ($idx < 0) { + $naechste_spielernr = "1" . $naechste_spielernr; + } + } while (isset($spielerIdsHinzugefuegt[$naechste_spielernr])); + } + $spieler_added++; + } + + if (!empty($vereinsname) && $veranstalterId !== -1) { + $query = "SELECT spieler_id FROM #__sportsmanager_mitglied_von_verein" + . "\n WHERE spieler_id = $spieler_id AND verein_id = " + . " (SELECT verein_id FROM #__sportsmanager_verein WHERE vereinsname = '" . $db->escape($vereinsname) . "' AND veranstalter_id = $veranstalterId LIMIT 1)"; + $memb_check = loadObjectList($db, $query); + + if (count($memb_check) > 0) { + $query = "UPDATE #__sportsmanager_mitglied_von_verein, #__sportsmanager_verein" + . "\n SET mitgliedsstatus = '$mitgliedsstatus', #__sportsmanager_mitglied_von_verein.ausgetreten = FALSE" + . "\n WHERE spieler_id = $spieler_id AND vereinsname = '" . $db->escape($vereinsname) . "' AND #__sportsmanager_verein.verein_id = #__sportsmanager_mitglied_von_verein.verein_id" + . " AND veranstalter_id = $veranstalterId"; + $db->setQuery($query); + $db->execute(); + } else { + $query = "SELECT verein_id FROM #__sportsmanager_verein" + . "\n WHERE vereinsname = '" . $db->escape($vereinsname) . "' AND veranstalter_id = $veranstalterId"; + $club_rows = loadObjectList($db, $query); + + if (count($club_rows) > 0) { + $verein_id = intval($club_rows[0]->verein_id); + } else { + $query = "INSERT INTO #__sportsmanager_verein" + . "\n SET vereinsname = '" . $db->escape($vereinsname) . "'," + . "\n veranstalter_id = $veranstalterId"; + if (!empty($vereinssitz)) { + $query .= ",\n vereinssitz = '" . $db->escape($vereinssitz) . "'"; + } + $db->setQuery($query); + $db->execute(); + $verein_id = $db->insertid(); + } + + $query = "INSERT INTO #__sportsmanager_mitglied_von_verein" + . "\n SET spieler_id = $spieler_id, verein_id = $verein_id, mitgliedsstatus = '$mitgliedsstatus', ausgetreten = FALSE"; + $db->setQuery($query); + $db->execute(); + } + } + } + + foreach ($org_map as $orgName => $veranstalterId) { + $query = "UPDATE #__sportsmanager_verein" + . "\n SET ausgetreten = TRUE" + . "\n WHERE NOT EXISTS(SELECT * FROM #__sportsmanager_mitglied_von_verein WHERE #__sportsmanager_verein.verein_id = #__sportsmanager_mitglied_von_verein.verein_id AND NOT #__sportsmanager_mitglied_von_verein.ausgetreten) AND NOT ausgetreten AND veranstalter_id = " . $veranstalterId; + $db->setQuery($query); + $db->execute(); + + $query = "UPDATE #__sportsmanager_verein" + . "\n SET ausgetreten = FALSE" + . "\n WHERE EXISTS(SELECT * FROM #__sportsmanager_mitglied_von_verein WHERE #__sportsmanager_verein.verein_id = #__sportsmanager_mitglied_von_verein.verein_id AND NOT #__sportsmanager_mitglied_von_verein.ausgetreten) AND ausgetreten AND veranstalter_id = " . $veranstalterId; + $db->setQuery($query); + $db->execute(); + + $query = "SELECT DISTINCT verein_id, #__sportsmanager_spieler_import.vereinsname, #__sportsmanager_spieler_import.vereinssitz" + . "\n FROM #__sportsmanager_spieler_import" + . "\n INNER JOIN #__sportsmanager_verein ON #__sportsmanager_verein.vereinsname = #__sportsmanager_spieler_import.vereinsname" + . "\n WHERE session_id = '" . $db->escape($session_id) . "' AND #__sportsmanager_spieler_import.veranstalterbezeichnung = '" . $db->escape($orgName) . "' AND #__sportsmanager_spieler_import.vereinsname != '' AND #__sportsmanager_spieler_import.vereinssitz != '' AND (ISNULL(#__sportsmanager_verein.vereinssitz) OR #__sportsmanager_verein.vereinssitz != #__sportsmanager_spieler_import.vereinssitz) AND NOT #__sportsmanager_verein.ausgetreten AND veranstalter_id = " . $veranstalterId; + $rows_headquarters = loadObjectList($db, $query); + foreach ($rows_headquarters as $row) { + $query = "UPDATE #__sportsmanager_verein" + . "\n SET vereinssitz = '" . $db->escape($row->vereinssitz) . "'" + . "\n WHERE verein_id = $row->verein_id"; + $db->setQuery($query); + $db->execute(); + } + } + + $query = "DELETE FROM #__sportsmanager_spieler_import WHERE session_id = '" . $db->escape($session_id) . "'"; + $db->setQuery($query); + $db->execute(); + + aktuellerVereinAktualisieren(); + ranglisteAktualisieren(); + einstufungAktualisieren(); + + return [ + 'success' => true, + 'spieler_count' => count($rows_to_insert), + 'spieler_updated' => $spieler_updated, + 'spieler_added' => $spieler_added + ]; +} + +/** + * Endpoint task: triggered by GitHub Actions or other schedule. + * Authenticates with local api_push_key, exports player data, pushes to DTFB, and returns JSON. + */ +function apiSyncSpielerTrigger(): void +{ + $token = syncGetBearerToken(); + $expected_key = einstellungswert("api_push_key"); + + if (empty($expected_key) || $token !== $expected_key) { + header('HTTP/1.1 401 Unauthorized'); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode(['success' => false, 'error' => 'Ungültiges Authentifizierungs-Token.']); + exit; + } + + $csvData = syncExportSpielerCSV(); + if (empty($csvData)) { + header('Content-Type: application/json; charset=utf-8'); + echo json_encode(['success' => false, 'error' => 'Keine Spieler zum Synchronisieren gefunden.']); + exit; + } + + $res = syncPushToDtfb($csvData); + + // Log sync status + syncLogEntry( + 'push', + 'api', + $res['success'] ? 'success' : 'error', + $res['spieler_count'] ?? 0, + $res['spieler_updated'] ?? 0, + $res['spieler_added'] ?? 0, + $res['message'] ?? '', + '' + ); + + header('Content-Type: application/json; charset=utf-8'); + if ($res['success']) { + echo json_encode($res); + } else { + header('HTTP/1.1 500 Internal Server Error'); + echo json_encode($res); + } + exit; +} + +/** + * Endpoint task: receives CSV data from another Sportsmanager instance, imports it. + * Authenticates with local api_push_key. + */ +function apiSyncSpielerReceive(): void +{ + $token = syncGetBearerToken(); + $expected_key = einstellungswert("api_push_key"); + + if (empty($expected_key) || $token !== $expected_key) { + header('HTTP/1.1 401 Unauthorized'); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode(['success' => false, 'error' => 'Ungültiges Authentifizierungs-Token.']); + exit; + } + + $csvData = file_get_contents('php://input'); + if (empty($csvData)) { + header('HTTP/1.1 400 Bad Request'); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode(['success' => false, 'error' => 'Keine Formulardaten empfangen.']); + exit; + } + + $res = syncReceiveSpielerImport($csvData); + + // Log sync status + syncLogEntry( + 'receive', + 'api', + $res['success'] ? 'success' : 'error', + $res['spieler_count'] ?? 0, + $res['spieler_updated'] ?? 0, + $res['spieler_added'] ?? 0, + $res['message'] ?? '', + '' + ); + + header('Content-Type: application/json; charset=utf-8'); + if ($res['success']) { + echo json_encode($res); + } else { + header('HTTP/1.1 500 Internal Server Error'); + echo json_encode($res); + } + exit; +} diff --git a/src/structure/components/com_sportsmanager/views/sportsmanager/view.html.php b/src/structure/components/com_sportsmanager/views/sportsmanager/view.html.php index b6f7e60..538acf4 100644 --- a/src/structure/components/com_sportsmanager/views/sportsmanager/view.html.php +++ b/src/structure/components/com_sportsmanager/views/sportsmanager/view.html.php @@ -23,7 +23,7 @@ static function aktuelleBegegnungenHeader($titel, $beschreibung, $ticker_anzeige if (!empty($beschreibung)) { ?>
| ") ? $beschreibung : htmlentities_utf8($beschreibung); ?> |