Compare commits

..

11 Commits

Author SHA1 Message Date
Tim 6b68d8a5ed Fix DB migration for sync_log: bump version to 121 and mirror into script.php
Addresses review feedback (jmeyer26):
- update.php: move the sync_log table + dtfb_sync_url setting out of the
  already-released <120 migration block into a new <121 block, so instances
  already at version 120 actually receive them.
- script.php: create the sync_log table, seed dtfb_sync_url, and set the
  fresh-install datenbank_version to 121 (parity with update.php).
2026-06-04 11:53:10 +02:00
Tim 5843fda2d6 QA: harden DTFB player sync receiver
Applies 6 fixes to sync.php found during QA of the player-sync feature:
1. Normalise non-UTF-8 (latin1/Win-1252) payloads -> fixes silent 0-row imports
2. Fail loudly (success=false) when N rows parse but nothing is added/updated
3. Remove dead \ block (undefined-variable notice)
4. Gate mass-deactivation: skip the sweep when a payload carries < 50% of an
   org's currently-active members (configurable via sync_deactivation_min_ratio,
   default 0.5); adds/updates still proceed, skipped sweeps return warnings
5. Use a single DB clock (NOW()) for staging session id/cleanup
6. Enforce Passnummer format ^[0-9]{2}-[0-9]{4,6}\$ (parity with manual import)

Adds tests/dtfb-player-sync/FINDINGS.md documenting the findings and fixes.
End-to-end validation is to be done on the staging environments.
2026-06-04 11:41:50 +02:00
Tim 511c17468c QA Edge Case Fixes: enforce spielernr and preserve DTFB fields
- Fix 1: Discard players missing spielernr instead of auto-generating them

- Fix 2: Preserve lizenznr, geburtsjahr, and pseudonym fields during player updates

- Fix 3: Resolve PHP 8 array offset warning by using !empty() for geschlecht parsing

- Fix 4: Leave clubless players handling as-is (skip them) per user request
2026-06-04 00:33:05 +02:00
Tim 6f33599fd9 Remove lizenz import handling - field should never be overwritten at DTFB 2026-06-04 00:09:31 +02:00
Tim aac4c1458f Merge sportsmanager2-dev and fix critical sync bugs
Merge resolution:
- Combined migration 120 (sync_log table + dev branch schema changes)

Bug fixes from intensive code review:
- C1: Fix session_id type mismatch - use datetime format matching
  the staging table schema instead of varchar string (was breaking
  the entire sync receive import)
- C2: Fix staging table cleanup query - use datetime comparison
  matching the original admin import pattern
- W1: Add set_time_limit(300) to prevent timeout during large imports
- W2: Add REDIRECT_HTTP_AUTHORIZATION header support for Apache
  mod_rewrite compatibility
- W4: Add lizenz column parsing and update during sync import
- M1: Tighten export WHERE clause to require both aktueller_verein_id
  and spielernr (consistent with original export behavior)
- M2: Wrap syncGetLastStatus() in try/catch for graceful handling
  when sync_log table doesn't exist yet
2026-06-04 00:02:56 +02:00
Tim f39ade0e9d Implement Player Sync to DTFB (#286) 2026-06-03 18:36:39 +02:00
MarvinF 2a307b0987 Merge branch 'sportsmanager2-stage' into sportsmanager2-dev 2026-05-13 00:03:28 +02:00
MarvinF e8e6f7046d Merge pull request #283 from Deutscher-Tischfussballbund/sportsmanager2-issue282
add club mailing functionality to admin area
2026-05-13 00:02:35 +02:00
Marvin Flock 20ab5a44a9 fix: add table headers 2026-05-13 00:00:34 +02:00
Jürgen Meyer a5357e4a51 mailto Funktion bei Mannschaften in admin-Bereich Veranstaltung 2026-04-28 11:46:22 +02:00
Jürgen Meyer 68e16a3adb mailto Funktion bei Vereine in admin-Bereich 2026-04-28 09:45:29 +02:00
10 changed files with 1634 additions and 339 deletions
+22
View File
@@ -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"
File diff suppressed because it is too large Load Diff
@@ -5709,7 +5709,6 @@ function updateDatabase(): void
} }
if ($datenbank_version < 120) { if ($datenbank_version < 120) {
$columns = $db->getTableColumns('#__sportsmanager_teamspiel_modus'); $columns = $db->getTableColumns('#__sportsmanager_teamspiel_modus');
if (!array_key_exists('spiele_in_spielerstatistik', $columns)){ if (!array_key_exists('spiele_in_spielerstatistik', $columns)){
$query = "ALTER TABLE `#__sportsmanager_teamspiel_modus`" $query = "ALTER TABLE `#__sportsmanager_teamspiel_modus`"
@@ -5741,6 +5740,41 @@ function updateDatabase(): void
} }
} }
if ($datenbank_version < 121) {
$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 = '121'"
. "\n WHERE name = 'datenbank_version'";
$db->setQuery($query);
if (!$db->execute()) {
die($db->stderr(true));
}
}
if ($termin_aktionen_email_setzen) { if ($termin_aktionen_email_setzen) {
$query = "SELECT aktion_user_id, termin_aktion_id" $query = "SELECT aktion_user_id, termin_aktion_id"
. "\n FROM #__sportsmanager_termin_aktion"; . "\n FROM #__sportsmanager_termin_aktion";
@@ -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/image.php';
require_once JPATH_SITE . '/components/com_sportsmanager/util/email.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/database/update.php'; // will also include init.php and util.php
require_once JPATH_SITE . '/components/com_sportsmanager/sync.php';
initDatabase(); initDatabase();
updateDatabase(); updateDatabase();
@@ -74,6 +75,10 @@ if ($task == "spielerbild") {
terminDokument(); terminDokument();
} else if ($task == "spieler_details") { } else if ($task == "spieler_details") {
spielerDetails(); 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_")) { } else if ($task !== null && str_starts_with($task, "admin_")) {
// in some cases there are no breaks needed due to no return from method // in some cases there are no breaks needed due to no return from method
switch ($task) { switch ($task) {
@@ -134,6 +139,9 @@ if ($task == "spielerbild") {
case 'admin_spieler_export_sport': case 'admin_spieler_export_sport':
adminExportSpielerSport(); adminExportSpielerSport();
break; break;
case 'admin_spieler_sync_dtfb':
adminSyncSpielerToDtfb();
break;
case 'admin_spieler_remove_inaktive_form': case 'admin_spieler_remove_inaktive_form':
adminRemoveInaktiveSpielerForm(); adminRemoveInaktiveSpielerForm();
break; break;
@@ -0,0 +1,899 @@
<?php
/**
* Sports Manager Sync Extension
*/
use Joomla\CMS\Application\SiteApplication;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
defined("_JEXEC") or die();
/**
* Gets the Bearer token from the Authorization header.
*
* @return string|null
*/
function syncGetBearerToken(): ?string
{
$headers = null;
if (isset($_SERVER['Authorization'])) {
$headers = trim($_SERVER["Authorization"]);
} elseif (isset($_SERVER['HTTP_AUTHORIZATION'])) {
$headers = trim($_SERVER["HTTP_AUTHORIZATION"]);
} elseif (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
$headers = trim($_SERVER["REDIRECT_HTTP_AUTHORIZATION"]);
} elseif (function_exists('apache_request_headers')) {
$requestHeaders = apache_request_headers();
$requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders));
if (isset($requestHeaders['Authorization'])) {
$headers = trim($requestHeaders['Authorization']);
}
}
if (!empty($headers)) {
if (preg_match('/Bearer\s(\S+)/i', $headers, $matches)) {
return $matches[1];
}
}
return null;
}
/**
* Logs a sync event in the database.
*
* @param string $direction ('push' or 'receive')
* @param string $trigger ('manual', 'cron', or 'api')
* @param string $status ('success' or 'error')
* @param int $spieler_count
* @param int $spieler_updated
* @param int $spieler_added
* @param string $message
* @param string $details
*/
function syncLogEntry(string $direction, string $trigger, string $status, int $spieler_count, int $spieler_updated, int $spieler_added, string $message, string $details = ''): void
{
try {
$db = getDatabase();
$query = "INSERT INTO #__sportsmanager_sync_log"
. "\n SET sync_timestamp = NOW(),"
. "\n sync_direction = '" . $db->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
{
try {
$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(
"<span class='%s'><strong>%s</strong></span> am %s via %s / %s%s",
$statusClass,
$statusText,
date('d.m.Y H:i:s', strtotime($row->sync_timestamp)),
$directionText,
$triggerText,
$stats
);
} catch (Exception $e) {
return "Noch nie synchronisiert";
}
}
/**
* 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(aktueller_verein_id) AND 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
{
if (!ini_get('safe_mode'))
set_time_limit(300);
$db = getDatabase();
// Normalise the payload to UTF-8. The automatic/semi-automatic sync path is
// UTF-8 end to end, but a legacy/manual export (e.g. from TFVHH) can be
// latin1/Windows-1252 encoded. Without this, non-ASCII bytes (e.g. "ß" in an
// organisation name) get truncated when staged into the utf8mb4 import table,
// causing the organisation match to fail and every row to be skipped silently.
if (!mb_check_encoding($csvData, 'UTF-8')) {
$csvData = mb_convert_encoding($csvData, 'UTF-8', 'Windows-1252');
}
$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++;
// Source the staging session id from the database clock, not PHP's. The
// stale-row cleanup below compares session_id against the database NOW(); if
// PHP and the database run in different timezones, a PHP-generated timestamp
// can fall outside the window and the just-inserted rows get deleted mid-import.
$session_id = loadResult($db, "SELECT DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')");
if (empty($session_id)) {
$session_id = date('Y-m-d H:i:s');
}
$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"]) && !empty($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"]]) : "";
// Validate the Passnummer with the same format the manual import enforces
// (NN-NNNN[NN]). Invalid values are dropped rather than aborting the whole
// automated feed, keeping the player but treating them as having no pass.
if (!empty($spielernr) && !preg_match('/^[0-9]{2}-[0-9]{4,6}$/', $spielernr)) {
$spielernr = "";
}
$spielernr_alt = isset($spalte["spielernr_alt"]) && isset($daten[$spalte["spielernr_alt"]]) ? trim($daten[$spalte["spielernr_alt"]]) : "";
if (!empty($spielernr_alt) && !preg_match('/^[0-9]{2}-[0-9]{4,6}$/', $spielernr_alt)) {
$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 staging data (older than 5 minutes)
$query = "SELECT DISTINCT session_id"
. "\n FROM #__sportsmanager_spieler_import"
. "\n WHERE session_id < SUBTIME(NOW(), '00:05:00')";
$old_sessions = loadObjectList($db, $query);
foreach ($old_sessions as $old_session) {
$query = "DELETE FROM #__sportsmanager_spieler_import WHERE session_id = '" . $db->escape($old_session->session_id) . "'";
$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);
// Count how many active players the incoming payload provides per organisation.
// The mass-deactivation below is only safe when the payload is a *full* roster;
// a partial CSV would otherwise silently deactivate every member not listed.
$incoming_per_org = [];
foreach ($rows_to_insert as $row) {
$o = trim($row['organisation']);
if ($o !== "") {
$incoming_per_org[$o] = ($incoming_per_org[$o] ?? 0) + 1;
}
}
// Minimum fraction of the currently-active roster the payload must contain
// before the sweep is allowed to run. Configurable; defaults to 0.5.
$deactivation_min_ratio = (float) (einstellungswert("sync_deactivation_min_ratio") ?? 0.5);
if ($deactivation_min_ratio <= 0 || $deactivation_min_ratio > 1) {
$deactivation_min_ratio = 0.5;
}
$warnings = [];
$deactivated_total = 0;
// Deactivate all memberships for involved organisations temporarily. The
// players present in the payload are reactivated further below; anyone not
// listed stays deactivated (i.e. is treated as having left the organisation).
foreach ($org_map as $orgName => $veranstalterId) {
$aktiv_vorher = (int) loadResult(
$db,
"SELECT COUNT(*) FROM #__sportsmanager_mitglied_von_verein"
. " INNER JOIN #__sportsmanager_verein USING (verein_id)"
. " WHERE veranstalter_id = " . $veranstalterId
. " AND NOT #__sportsmanager_mitglied_von_verein.ausgetreten"
);
$eingehend = $incoming_per_org[$orgName] ?? 0;
// Guard: skip the sweep when the payload looks like a partial roster
// (far fewer players than are currently active). This prevents a partial
// export from wiping an entire organisation's memberships.
if ($aktiv_vorher > 0 && $eingehend < $aktiv_vorher * $deactivation_min_ratio) {
$warnings[] = sprintf(
'Massen-Deaktivierung für "%s" übersprungen: nur %d von %d aktiven Mitgliedern in den Daten (mögliche Teil-Liste).',
$orgName,
$eingehend,
$aktiv_vorher
);
continue;
}
$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.'
];
}
$deactivated_total += $aktiv_vorher;
}
$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($spielernr)) {
continue;
}
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) . "'"
. "\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 {
$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;
$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();
// Fail loudly on a zero-effect import: if valid rows were parsed but nothing
// was added or updated, the data almost certainly failed to map (e.g. an
// encoding mismatch corrupting organisation names). Reporting success here
// would silently hide data loss.
if (count($rows_to_insert) > 0 && $spieler_added === 0 && $spieler_updated === 0) {
return [
'success' => false,
'message' => 'Import ergab keine Änderungen trotz ' . count($rows_to_insert)
. ' gültiger Zeilen mögliche Encoding- oder Zuordnungsfehler.',
'spieler_count' => count($rows_to_insert),
'spieler_updated' => 0,
'spieler_added' => 0,
'warnings' => $warnings
];
}
aktuellerVereinAktualisieren();
ranglisteAktualisieren();
einstufungAktualisieren();
return [
'success' => true,
'spieler_count' => count($rows_to_insert),
'spieler_updated' => $spieler_updated,
'spieler_added' => $spieler_added,
'deactivated' => $deactivated_total,
'warnings' => $warnings
];
}
/**
* 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;
}
File diff suppressed because it is too large Load Diff
@@ -982,6 +982,21 @@ class HTML_sportsmanager_admin
?>"/> ?>"/>
</td> </td>
</tr> </tr>
<tr>
<td nowrap colspan="2">&nbsp;
</td>
</tr>
<tr>
<td style="font-weight:bold"><label for="dtfb_sync_url">DTFB Sync Einstellungen</label>
</td>
</tr>
<tr>
<td style="text-align: right"><label for="dtfb_sync_url">Sync-URL</label></td>
<td>
<input name="dtfb_sync_url" id="dtfb_sync_url" type="text" size="60"
value="<?php echo htmlspecialchars($einstellungen["dtfb_sync_url"] ?? '') ?>"/>
</td>
</tr>
</table> </table>
</div> </div>
@@ -1255,6 +1270,18 @@ class HTML_sportsmanager_admin
href="<?php echo SportsManagerURL('&task=admin_spieler_remove_inaktive_form'); ?>"><?php echo Text::_('COM_SPORTSMANAGER_CLEANUP_INACTIVE_PLAYERS'); ?></a> href="<?php echo SportsManagerURL('&task=admin_spieler_remove_inaktive_form'); ?>"><?php echo Text::_('COM_SPORTSMANAGER_CLEANUP_INACTIVE_PLAYERS'); ?></a>
</td> </td>
</tr> </tr>
<tr>
<td nowrap style="padding-top: 10px;">
<a href="<?php echo SportsManagerURL('&task=admin_spieler_sync_dtfb'); ?>"
onclick="return confirm('Spielerdaten jetzt mit DTFB synchronisieren?');"
class="uk-button uk-button-primary uk-button-small button" style="padding: 3px 10px; font-weight: bold; background: #007bc3; color: white; border-radius: 4px; border: none; text-decoration: none; display: inline-block;">
🔄 Sync zu DTFB
</a>
</td>
<td nowrap colspan="4" style="padding-top: 10px; vertical-align: middle;">
<small style="margin-left: 10px;">Letzter Sync: <?php echo syncGetLastStatus(); ?></small>
</td>
</tr>
<?php <?php
} }
?> ?>
@@ -2886,7 +2913,7 @@ class HTML_sportsmanager_admin
<span class="article_seperator<?php echo $params->get('pageclass_sfx'); ?>">&nbsp;</span> <span class="article_seperator<?php echo $params->get('pageclass_sfx'); ?>">&nbsp;</span>
<?php <?php
} }
if (!empty($nichtAktualisierteUnterschiede)){ if (!empty($nichtAktualisierteUnterschiede)){
?> ?>
<table class="contentpaneopen<?php echo $params->get('pageclass_sfx'); ?>"> <table class="contentpaneopen<?php echo $params->get('pageclass_sfx'); ?>">
@@ -3539,7 +3566,7 @@ class HTML_sportsmanager_admin
<?php <?php
} }
static function adminVereine($rows, $organisationAnzeigen): void static function adminVereine($rows, $organisationAnzeigen, $ansprechpartner): void
{ {
global $params; global $params;
@@ -3599,6 +3626,8 @@ class HTML_sportsmanager_admin
<th nowrap><strong><?php echo Text::_('COM_SPORTSMANAGER_MEMBERS'); ?></strong></th> <th nowrap><strong><?php echo Text::_('COM_SPORTSMANAGER_MEMBERS'); ?></strong></th>
<th nowrap><strong><?php echo Text::_('COM_SPORTSMANAGER_TEAM_SEAT'); ?></strong></th> <th nowrap><strong><?php echo Text::_('COM_SPORTSMANAGER_TEAM_SEAT'); ?></strong></th>
<th nowrap><strong><?php echo Text::_('COM_SPORTSMANAGER_BEATEN'); ?></strong></th> <th nowrap><strong><?php echo Text::_('COM_SPORTSMANAGER_BEATEN'); ?></strong></th>
<th></th>
<th></th>
</tr> </tr>
<?php <?php
@@ -3661,7 +3690,15 @@ class HTML_sportsmanager_admin
</td> </td>
<td nowrap><?php if (!empty($row->vereinssitz)) echo htmlentities_utf8($row->vereinssitz . (!empty($row->vereinssitz_ortsteil) ? ("-" . $row->vereinssitz_ortsteil) : "")); ?></td> <td nowrap><?php if (!empty($row->vereinssitz)) echo htmlentities_utf8($row->vereinssitz . (!empty($row->vereinssitz_ortsteil) ? ("-" . $row->vereinssitz_ortsteil) : "")); ?></td>
<td nowrap><?php echo $row->ausgetreten ? Text::_('COM_SPORTSMANAGER_YES') : Text::_('COM_SPORTSMANAGER_NO'); ?></td> <td nowrap><?php echo $row->ausgetreten ? Text::_('COM_SPORTSMANAGER_YES') : Text::_('COM_SPORTSMANAGER_NO'); ?></td>
<td nowrap><small><a <td>
<?PHP
if (!empty($ansprechpartner[$row->verein_id])){
$emails = implode(';', $ansprechpartner[$row->verein_id]);
echo "<a href='mailto:" . $emails . "?subject=" . $row->vereinsname . "'>E-Mail</a>&nbsp;";
}
?>
</td>
<td nowrap><small><a
href="<?php echo SportsManagerURL('&task=admin_verein_remove&id=' . $row->verein_id); ?>" href="<?php echo SportsManagerURL('&task=admin_verein_remove&id=' . $row->verein_id); ?>"
onclick="return confirm('<?php echo Text::_('COM_SPORTSMANAGER_WANT_REALLY_REMOVE'); ?>');" onclick="return confirm('<?php echo Text::_('COM_SPORTSMANAGER_WANT_REALLY_REMOVE'); ?>');"
title="<?php echo Text::_('COM_SPORTSMANAGER_REMOVE'); ?>">X</a></small></td> title="<?php echo Text::_('COM_SPORTSMANAGER_REMOVE'); ?>">X</a></small></td>
@@ -4834,8 +4871,8 @@ class HTML_sportsmanager_admin
</select> </select>
</td> </td>
</tr> </tr>
<tr> <tr>
<td nowrap style="width: 20%; text-align: right"> <td nowrap style="width: 20%; text-align: right">
<label <label
@@ -4843,7 +4880,7 @@ class HTML_sportsmanager_admin
:</label> :</label>
</td> </td>
<td nowrap> <td nowrap>
<select class="uk-select uk-form-width-medium" name="spiele_in_spielerstatistik" <select class="uk-select uk-form-width-medium" name="spiele_in_spielerstatistik"
id="games_in_statistik" size="1"> id="games_in_statistik" size="1">
<option value="0"><?php echo Text::_('COM_SPORTSMANAGER_GAMES_IN_STATISTIK_ALL'); ?></option> <option value="0"><?php echo Text::_('COM_SPORTSMANAGER_GAMES_IN_STATISTIK_ALL'); ?></option>
<?php <?php
@@ -4854,9 +4891,9 @@ class HTML_sportsmanager_admin
</select> </select>
</td> </td>
</tr> </tr>
<tr> <tr>
<td nowrap style="width: 20%; text-align: right"> <td nowrap style="width: 20%; text-align: right">
<label for="status"><?php echo Text::_('COM_SPORTSMANAGER_ACTIVE'); ?> <label for="status"><?php echo Text::_('COM_SPORTSMANAGER_ACTIVE'); ?>
@@ -6385,9 +6422,9 @@ class HTML_sportsmanager_admin
<select class="uk-select uk-form-width-large" name="tabellenwertung" <select class="uk-select uk-form-width-large" name="tabellenwertung"
id="table_evaluation" size="1"> id="table_evaluation" size="1">
<?php <?php
$typ = array(0 => Text::_('COM_SPORTSMANAGER_PERFORMANCE_INDEX0'), $typ = array(0 => Text::_('COM_SPORTSMANAGER_PERFORMANCE_INDEX0'),
1 => Text::_('COM_SPORTSMANAGER_PERFORMANCE_INDEX1'), 1 => Text::_('COM_SPORTSMANAGER_PERFORMANCE_INDEX1'),
2 => Text::_('COM_SPORTSMANAGER_PERFORMANCE_INDEX2'), 2 => Text::_('COM_SPORTSMANAGER_PERFORMANCE_INDEX2'),
3 => Text::_('COM_SPORTSMANAGER_PERFORMANCE_INDEX3'), 3 => Text::_('COM_SPORTSMANAGER_PERFORMANCE_INDEX3'),
4 => Text::_('COM_SPORTSMANAGER_PERFORMANCE_INDEX4'), 4 => Text::_('COM_SPORTSMANAGER_PERFORMANCE_INDEX4'),
5 => Text::_('COM_SPORTSMANAGER_PERFORMANCE_INDEX5')); 5 => Text::_('COM_SPORTSMANAGER_PERFORMANCE_INDEX5'));
@@ -6918,12 +6955,12 @@ class HTML_sportsmanager_admin
<?php <?php
} }
static function adminMailto($to,$cc,$bcc,$subject,$message,$backtomail,$backtosender,$vorlage=''): void static function adminMailto($to,$cc,$bcc,$subject,$message,$backtomail,$backtosender,$vorlage=''): void
{ {
global $params; global $params;
?> ?>
<div class="componentheading<?php echo $params->get('pageclass_sfx'); ?>"><?php echo Text::_('COM_SPORTSMANAGER_EMAIL_SEND'); ?></div> <div class="componentheading<?php echo $params->get('pageclass_sfx'); ?>"><?php echo Text::_('COM_SPORTSMANAGER_EMAIL_SEND'); ?></div>
<form id="mailForm"> <form id="mailForm">
<div class="uk-overflow-auto"> <div class="uk-overflow-auto">
<table style="width: 100%"> <table style="width: 100%">
@@ -6972,8 +7009,8 @@ class HTML_sportsmanager_admin
<input type="submit" name="joomlamail" value="<?php echo Text::_('COM_SPORTSMANAGER_EMAIL_SEND'); ?>&nbsp;(joomla)" class="button"/> <input type="submit" name="joomlamail" value="<?php echo Text::_('COM_SPORTSMANAGER_EMAIL_SEND'); ?>&nbsp;(joomla)" class="button"/>
<?php if ($vorlage->name == "Ordnungsstrafe"){ ?> <?php if ($vorlage->name == "Ordnungsstrafe"){ ?>
<input type="submit" name="cancel" value="<?php echo Text::_('COM_SPORTSMANAGER_EDIT_DISCIPLINARY_FINE'); ?>" class="button" <input type="submit" name="cancel" value="<?php echo Text::_('COM_SPORTSMANAGER_EDIT_DISCIPLINARY_FINE'); ?>" class="button"
onclick="const t = this.form.task; t.value = 'admin_ordnungsstrafe_edit';"/> onclick="const t = this.form.task; t.value = 'admin_ordnungsstrafe_edit';"/>
<input type="hidden" name="id" value="<?php echo $vorlage->id; ?>"/> <input type="hidden" name="id" value="<?php echo $vorlage->id; ?>"/>
<?php } ?> <?php } ?>
<input type="submit" name="cancel" value="<?php echo Text::_('COM_SPORTSMANAGER_BACK'); ?>" class="button" <input type="submit" name="cancel" value="<?php echo Text::_('COM_SPORTSMANAGER_BACK'); ?>" class="button"
onclick="const t = this.form.task; t.value = '<?php echo $backtosender; ?>';"/> onclick="const t = this.form.task; t.value = '<?php echo $backtosender; ?>';"/>
@@ -7036,7 +7073,7 @@ class HTML_sportsmanager_admin
<div <div
class="componentheading<?php echo $params->get('pageclass_sfx'); ?>"><?php echo Text::_('COM_SPORTSMANAGER_DISCIPLINARY_FINES'); ?> class="componentheading<?php echo $params->get('pageclass_sfx'); ?>"><?php echo Text::_('COM_SPORTSMANAGER_DISCIPLINARY_FINES'); ?>
: <?php echo Text::_('COM_SPORTSMANAGER_JOOMLA_MANAGEMENT'); ?></div> : <?php echo Text::_('COM_SPORTSMANAGER_JOOMLA_MANAGEMENT'); ?></div>
<table style="border-spacing: 10px"> <table style="border-spacing: 10px">
<tr> <tr>
<td nowrap><a <td nowrap><a
@@ -7057,7 +7094,7 @@ class HTML_sportsmanager_admin
</tr> </tr>
<?php } ?> <?php } ?>
</table> </table>
<?php if (count($saisons) > 0) { <?php if (count($saisons) > 0) {
?> ?>
<form action="<?php echo SportsManagerURL(); ?>" method="post" name="adminForm" id="adminForm"> <form action="<?php echo SportsManagerURL(); ?>" method="post" name="adminForm" id="adminForm">
@@ -7266,12 +7303,12 @@ class HTML_sportsmanager_admin
size="1" style='height: 34px; width: 200px;' readonly size="1" style='height: 34px; width: 200px;' readonly
value="<?php echo (empty($row->versendedatum) ? '' : $row->versendedatum); ?>"/> value="<?php echo (empty($row->versendedatum) ? '' : $row->versendedatum); ?>"/>
<?php if ($row != null){ ?> <?php if ($row != null){ ?>
<input type="submit" name="set_versender" <input type="submit" name="set_versender"
value="<?php echo (empty($row->versendedatum) ? 'set' : 'reset'); ?>" class="button"/> value="<?php echo (empty($row->versendedatum) ? 'set' : 'reset'); ?>" class="button"/>
<?php } ?> <?php } ?>
</td> </td>
</tr> </tr>
<tr> <tr>
<td nowrap style="width: 20%; text-align: right"> <td nowrap style="width: 20%; text-align: right">
<label <label
@@ -7289,7 +7326,7 @@ class HTML_sportsmanager_admin
size="1" style='height: 34px; width: 200px;' readonly size="1" style='height: 34px; width: 200px;' readonly
value="<?php echo (empty($row->rechnungsdatum) ? '' : $row->rechnungsdatum); ?>"/> value="<?php echo (empty($row->rechnungsdatum) ? '' : $row->rechnungsdatum); ?>"/>
<?php if ($row != null && !empty($row->versendedatum)){ ?> <?php if ($row != null && !empty($row->versendedatum)){ ?>
<input type="submit" name="set_rechnung" <input type="submit" name="set_rechnung"
value="<?php echo (empty($row->rechnungsdatum) ? 'set' : 'reset'); ?>" class="button"/> value="<?php echo (empty($row->rechnungsdatum) ? 'set' : 'reset'); ?>" class="button"/>
<?php } ?> <?php } ?>
</td> </td>
@@ -7327,8 +7364,8 @@ class HTML_sportsmanager_admin
<input type="submit" name="save" value="<?php echo Text::_('COM_SPORTSMANAGER_SAVE'); ?>" class="button"/> <input type="submit" name="save" value="<?php echo Text::_('COM_SPORTSMANAGER_SAVE'); ?>" class="button"/>
<?php if ($row != null && benutzerZugriff("benutzerVeranstalterModerator")){ ?> <?php if ($row != null && benutzerZugriff("benutzerVeranstalterModerator")){ ?>
<input type="submit" name="cancel" value="<?php echo Text::_('COM_SPORTSMANAGER_SEND_DISCIPLINARY_FINE'); ?>" class="button" <input type="submit" name="cancel" value="<?php echo Text::_('COM_SPORTSMANAGER_SEND_DISCIPLINARY_FINE'); ?>" class="button"
onclick="const t = this.form.task; t.value = 'admin_ordnungsstrafe_mailen';"/> onclick="const t = this.form.task; t.value = 'admin_ordnungsstrafe_mailen';"/>
<input type="hidden" name="id" value="<?php echo $row->ordnungsstrafen_id; ?>"/> <input type="hidden" name="id" value="<?php echo $row->ordnungsstrafen_id; ?>"/>
<?php } ?> <?php } ?>
<input type="submit" name="cancel" value="<?php echo Text::_('COM_SPORTSMANAGER_CANCEL'); ?>" class="button"/> <input type="submit" name="cancel" value="<?php echo Text::_('COM_SPORTSMANAGER_CANCEL'); ?>" class="button"/>
<input type="hidden" name="task" value="admin_ordnungsstrafe_save"/> <input type="hidden" name="task" value="admin_ordnungsstrafe_save"/>
@@ -7350,7 +7387,7 @@ class HTML_sportsmanager_admin
<div <div
class="componentheading<?php echo $params->get('pageclass_sfx'); ?>"><?php echo Text::_('COM_SPORTSMANAGER_MATCH_RESCHEDULINGS'); ?> class="componentheading<?php echo $params->get('pageclass_sfx'); ?>"><?php echo Text::_('COM_SPORTSMANAGER_MATCH_RESCHEDULINGS'); ?>
: <?php echo Text::_('COM_SPORTSMANAGER_JOOMLA_MANAGEMENT'); ?></div> : <?php echo Text::_('COM_SPORTSMANAGER_JOOMLA_MANAGEMENT'); ?></div>
<table style="border-spacing: 10px"> <table style="border-spacing: 10px">
<tr> <tr>
<td nowrap><a <td nowrap><a
@@ -7410,13 +7447,13 @@ class HTML_sportsmanager_admin
<?php <?php
if (count($rows) > 0) { if (count($rows) > 0) {
$k = 0; $k = 0;
foreach ($rows as $row) { foreach ($rows as $row) {
?> ?>
<tr class="sectiontableentry<?php echo $k + 1; <tr class="sectiontableentry<?php echo $k + 1;
$k = ($k + 1) % 2; ?><?php echo $params->get('pageclass_sfx'); ?>"> $k = ($k + 1) % 2; ?><?php echo $params->get('pageclass_sfx'); ?>">
<td nowrap style='text-align: center;'> <td nowrap style='text-align: center;'>
<a href="<?php <a href="<?php
echo SportsManagerURL('&task=admin_spielverlegung_edit&begegnung_id=' . $row->begegnung_id); echo SportsManagerURL('&task=admin_spielverlegung_edit&begegnung_id=' . $row->begegnung_id);
?>"> ?>">
<?php echo $row->begegnung_id; ?> <?php echo $row->begegnung_id; ?>
</a> </a>
@@ -7425,7 +7462,7 @@ class HTML_sportsmanager_admin
<?php echo htmlentities_utf8($row->Liga); ?> <?php echo htmlentities_utf8($row->Liga); ?>
</td> </td>
<td nowrap style='text-align: center;'> <td nowrap style='text-align: center;'>
<?php <?php
if ($row->Heim == $row->beantragt_von) if ($row->Heim == $row->beantragt_von)
echo "<u>" . htmlentities_utf8($row->Heim) . "</u>"; echo "<u>" . htmlentities_utf8($row->Heim) . "</u>";
else else
@@ -7466,7 +7503,7 @@ class HTML_sportsmanager_admin
</div> </div>
<span class="article_seperator<?php echo $params->get('pageclass_sfx'); ?>">&nbsp;</span> <span class="article_seperator<?php echo $params->get('pageclass_sfx'); ?>">&nbsp;</span>
<?php <?php
} }
static function adminEditSpielverlegung($row,$teams): void static function adminEditSpielverlegung($row,$teams): void
{ {
@@ -7624,7 +7661,7 @@ class HTML_sportsmanager_admin
setTimeout(() => { t.value = 'admin_verbandsorgane'; }, 100);"/> setTimeout(() => { t.value = 'admin_verbandsorgane'; }, 100);"/>
<input type="hidden" name="task" value="admin_verbandsorgane"/> <input type="hidden" name="task" value="admin_verbandsorgane"/>
</form> </form>
<?php <?php
} }
} }
@@ -7950,7 +7987,7 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
</td> </td>
</tr> </tr>
</table> </table>
</div> </div>
<input type="submit" name="save" value="<?php echo Text::_('COM_SPORTSMANAGER_SAVE'); ?>" class="button"/> <input type="submit" name="save" value="<?php echo Text::_('COM_SPORTSMANAGER_SAVE'); ?>" class="button"/>
<input type="submit" name="cancel" value="<?php echo Text::_('COM_SPORTSMANAGER_CANCEL'); ?>" class="button"/> <input type="submit" name="cancel" value="<?php echo Text::_('COM_SPORTSMANAGER_CANCEL'); ?>" class="button"/>
@@ -7960,7 +7997,7 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
</form> </form>
<?php <?php
} }
static function adminHalloffame($rows): void static function adminHalloffame($rows): void
{ {
global $params; global $params;
@@ -8167,7 +8204,7 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
<strong><?php echo Text::_('COM_SPORTSMANAGER_YEAR'); ?></strong> <strong><?php echo Text::_('COM_SPORTSMANAGER_YEAR'); ?></strong>
</th> </th>
<?php <?php
for ($i = 1; $i <= 3; $i++) { for ($i = 1; $i <= 3; $i++) {
if ($i == 2 && !$halloffame->platz2_zeigen) continue; if ($i == 2 && !$halloffame->platz2_zeigen) continue;
if ($i == 3 && !$halloffame->platz3_zeigen) continue; if ($i == 3 && !$halloffame->platz3_zeigen) continue;
@@ -8325,8 +8362,8 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
</select> </select>
</td> </td>
</tr> </tr>
<?php <?php
for ($p = 1; $p <= 3; $p++){ for ($p = 1; $p <= 3; $p++){
if ($halloffame->spielform == 1){ if ($halloffame->spielform == 1){
$index_vereinid = "verein_id_" . $p; $index_vereinid = "verein_id_" . $p;
$index_team = "teamname_" . $p; $index_team = "teamname_" . $p;
@@ -8350,9 +8387,9 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
value="<?php echo $row != null ? htmlentities_utf8($row->$index_team) : ''; ?>"/> value="<?php echo $row != null ? htmlentities_utf8($row->$index_team) : ''; ?>"/>
</td> </td>
</tr> </tr>
<?php <?php
} }
if ($halloffame->spielform == 2 || $halloffame->spielform == 3){ if ($halloffame->spielform == 2 || $halloffame->spielform == 3){
$index_spieler1id = "spieler1_id_" . $p; $index_spieler1id = "spieler1_id_" . $p;
$index_spieler1 = "spieler1_" . $p; $index_spieler1 = "spieler1_" . $p;
@@ -8392,15 +8429,15 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
<?PHP } ?> <?PHP } ?>
</td> </td>
</tr> </tr>
<?php <?php
} }
} }
?> ?>
<tr> <tr>
<td nowrap colspan="2">&nbsp;</td> <td nowrap colspan="2">&nbsp;</td>
</tr> </tr>
</table> </table>
</div> </div>
<input type="submit" name="save" value="<?php echo Text::_('COM_SPORTSMANAGER_SAVE'); ?>" class="button"/> <input type="submit" name="save" value="<?php echo Text::_('COM_SPORTSMANAGER_SAVE'); ?>" class="button"/>
<input type="submit" name="cancel" value="<?php echo Text::_('COM_SPORTSMANAGER_CANCEL'); ?>" class="button"/> <input type="submit" name="cancel" value="<?php echo Text::_('COM_SPORTSMANAGER_CANCEL'); ?>" class="button"/>
@@ -8410,7 +8447,7 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
</form> </form>
<?php <?php
} }
static function adminRegelwerke($rows): void static function adminRegelwerke($rows): void
{ {
global $params; global $params;
@@ -9359,7 +9396,7 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
</select> </select>
</td> </td>
</tr> </tr>
<?php <?php
if (!einstellungswert("ordnungsstrafen_verwenden")) if (!einstellungswert("ordnungsstrafen_verwenden"))
echo "<tr style='display: none;'>"; echo "<tr style='display: none;'>";
else else
@@ -9712,7 +9749,7 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
<?php <?php
} }
static function adminMannschaften($veranstaltung, $rows): void static function adminMannschaften($veranstaltung, $rows, $ansprechpartner): void
{ {
global $params; global $params;
@@ -9762,6 +9799,8 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
<th nowrap title="<?php echo Text::_('COM_SPORTSMANAGER_NUM_REQUESTED_SHFITS_TOOLTIP'); ?>"> <th nowrap title="<?php echo Text::_('COM_SPORTSMANAGER_NUM_REQUESTED_SHFITS_TOOLTIP'); ?>">
<strong><?php echo Text::_('COM_SPORTSMANAGER_NUM_REQUESTED_SHIFTS'); ?></strong></th> <strong><?php echo Text::_('COM_SPORTSMANAGER_NUM_REQUESTED_SHIFTS'); ?></strong></th>
<th nowrap><strong><?php echo Text::_('COM_SPORTSMANAGER_HOME_VENUE'); ?></strong></th> <th nowrap><strong><?php echo Text::_('COM_SPORTSMANAGER_HOME_VENUE'); ?></strong></th>
<th></th>
<th></th>
</tr> </tr>
<?php <?php
@@ -9804,6 +9843,14 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
</td> </td>
<td nowrap align="center"><?php echo $row->anzahl_verschiebungen; ?></td> <td nowrap align="center"><?php echo $row->anzahl_verschiebungen; ?></td>
<td nowrap><?php if (!empty($row->name)) echo htmlentities_utf8($row->name); ?></td> <td nowrap><?php if (!empty($row->name)) echo htmlentities_utf8($row->name); ?></td>
<td>
<?PHP
if (!empty($ansprechpartner[$row->team_id])){
$emails = implode(';', $ansprechpartner[$row->team_id]);
echo "<a href='mailto:" . $emails . "?subject=" . $row->teamname . "'>E-Mail</a>&nbsp;";
}
?>
</td>
<?php if ($row->begegnungen == 0) { ?> <?php if ($row->begegnungen == 0) { ?>
<td nowrap><small><a <td nowrap><small><a
href="<?php echo SportsManagerURL('&task=admin_team_remove&veranstaltungid=' . $veranstaltung->veranstaltung_id . '&id=' . $row->team_id); ?>" href="<?php echo SportsManagerURL('&task=admin_team_remove&veranstaltungid=' . $veranstaltung->veranstaltung_id . '&id=' . $row->team_id); ?>"
@@ -10300,7 +10347,7 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
<th nowrap style="text-align: center"><strong><?php echo JText::_('COM_SPORTSMANAGER_PENALTY'); ?></strong></th> <th nowrap style="text-align: center"><strong><?php echo JText::_('COM_SPORTSMANAGER_PENALTY'); ?></strong></th>
<th nowrap style="text-align: left"><strong><?php echo JText::_('COM_SPORTSMANAGER_DESCRIPTION'); ?></strong></th> <th nowrap style="text-align: left"><strong><?php echo JText::_('COM_SPORTSMANAGER_DESCRIPTION'); ?></strong></th>
</tr> </tr>
<?php <?php
$k = 0; $k = 0;
foreach ($rows as $row) { foreach ($rows as $row) {
@@ -10996,7 +11043,7 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
<?php echo Text::_('COM_SPORTSMANAGER_ENCOUNTERS'); ?> <?php echo Text::_('COM_SPORTSMANAGER_ENCOUNTERS'); ?>
'<?php echo htmlentities_utf8($veranstaltung->bezeichnung); ?> '<?php echo htmlentities_utf8($veranstaltung->bezeichnung); ?>
': <?php echo Text::_('COM_SPORTSMANAGER_JOOMLA_MANAGEMENT'); ?></div> ': <?php echo Text::_('COM_SPORTSMANAGER_JOOMLA_MANAGEMENT'); ?></div>
<div class="uk-overflow-auto"> <div class="uk-overflow-auto">
<table style="border-spacing: 10px; width: 100%;"> <table style="border-spacing: 10px; width: 100%;">
<tr> <tr>
@@ -11067,10 +11114,10 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
$Spieltagname = "Runde " . $row->spieltag; $Spieltagname = "Runde " . $row->spieltag;
else else
$Spieltagname = "Spieltag " . $row->spieltag; $Spieltagname = "Spieltag " . $row->spieltag;
if ($row->spieltag < 999 && $veranstaltung->spieltag_titel_zeigen == 1 && $row->spieltag_titel != "") if ($row->spieltag < 999 && $veranstaltung->spieltag_titel_zeigen == 1 && $row->spieltag_titel != "")
$Spieltagname .= " - " . $row->spieltag_titel; $Spieltagname .= " - " . $row->spieltag_titel;
if ($Spieltagname_Buffer != $Spieltagname){ if ($Spieltagname_Buffer != $Spieltagname){
?> ?>
<tr class="sectiontableheader<?php echo $params->get('pageclass_sfx'); ?>"> <tr class="sectiontableheader<?php echo $params->get('pageclass_sfx'); ?>">
@@ -11098,12 +11145,12 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
<th nowrap><span style="font-size: 70%; "><i> <th nowrap><span style="font-size: 70%; "><i>
<?php echo htmlentities_utf8($monatsbezeichnung); ?></i></span> <?php echo htmlentities_utf8($monatsbezeichnung); ?></i></span>
</th> </th>
</tr> </tr>
<?php <?php
} }
?> ?>
<tr class="sectiontableentry<?php echo $k + 1; <tr class="sectiontableentry<?php echo $k + 1;
$k = ($k + 1) % 2; ?><?php echo $params->get('pageclass_sfx'); ?>"> $k = ($k + 1) % 2; ?><?php echo $params->get('pageclass_sfx'); ?>">
@@ -11323,7 +11370,7 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
value="<?php if ($row != null) echo htmlentities_utf8($row->spieltag_titel); ?>"/> value="<?php if ($row != null) echo htmlentities_utf8($row->spieltag_titel); ?>"/>
<datalist id="auswahl_spieltagtitel" > <datalist id="auswahl_spieltagtitel" >
<?php if ($auswahl_spieltagtitel){ ?> <?php if ($auswahl_spieltagtitel){ ?>
<?php foreach($auswahl_spieltagtitel AS $titel){ ?> <?php foreach($auswahl_spieltagtitel AS $titel){ ?>
<option value="<?= htmlspecialchars($titel->spieltag_titel, ENT_QUOTES) ?>"></option> <option value="<?= htmlspecialchars($titel->spieltag_titel, ENT_QUOTES) ?>"></option>
<?php } ?> <?php } ?>
<?php } ?> <?php } ?>
@@ -11342,7 +11389,7 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
title="Spielnummer (optional)" name="spiel_nr"> title="Spielnummer (optional)" name="spiel_nr">
<option value=""></option> <option value=""></option>
<?php <?php
for ($i = 1; $i <= 99; $i++) for ($i = 1; $i <= 99; $i++)
{ {
echo "<option value=\"" . $i . "\"" . ($spiel_nr == $i ? " selected" : "") . ">" . $i . "</option>"; echo "<option value=\"" . $i . "\"" . ($spiel_nr == $i ? " selected" : "") . ">" . $i . "</option>";
} }
@@ -11454,7 +11501,7 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
</div> </div>
<input type="submit" name="save" value="<?php echo Text::_('COM_SPORTSMANAGER_SAVE'); ?>" class="button" <input type="submit" name="save" value="<?php echo Text::_('COM_SPORTSMANAGER_SAVE'); ?>" class="button"
onclick="if (document.adminForm.heim_team_id.value === document.adminForm.gast_team_id.value) onclick="if (document.adminForm.heim_team_id.value === document.adminForm.gast_team_id.value)
{ alert('<?php echo Text::_('COM_SPORTSMANAGER_HOME_VISITING_TEAMS_DIFFERENT'); ?>'); return false; } return true;"/> { alert('<?php echo Text::_('COM_SPORTSMANAGER_HOME_VISITING_TEAMS_DIFFERENT'); ?>'); return false; } return true;"/>
<input type="submit" name="cancel" value="<?php echo Text::_('COM_SPORTSMANAGER_CANCEL'); ?>" <input type="submit" name="cancel" value="<?php echo Text::_('COM_SPORTSMANAGER_CANCEL'); ?>"
class="button"/> class="button"/>
@@ -12495,7 +12542,7 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
value="<?php if ($row != null) echo htmlentities_utf8($row->spieltag_titel); ?>"/> value="<?php if ($row != null) echo htmlentities_utf8($row->spieltag_titel); ?>"/>
<datalist id="auswahl_spieltagtitel" > <datalist id="auswahl_spieltagtitel" >
<?php if ($auswahl_spieltagtitel){ ?> <?php if ($auswahl_spieltagtitel){ ?>
<?php foreach($auswahl_spieltagtitel AS $titel){ ?> <?php foreach($auswahl_spieltagtitel AS $titel){ ?>
<option value="<?= htmlspecialchars($titel->spieltag_titel, ENT_QUOTES) ?>"></option> <option value="<?= htmlspecialchars($titel->spieltag_titel, ENT_QUOTES) ?>"></option>
<?php } ?> <?php } ?>
<?php } ?> <?php } ?>
@@ -12511,11 +12558,11 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
<?php if ($teamnr == 1) echo Text::_('COM_SPORTSMANAGER_PAIRINGS') . ":"; ?> <?php if ($teamnr == 1) echo Text::_('COM_SPORTSMANAGER_PAIRINGS') . ":"; ?>
</td> </td>
<td nowrap> <td nowrap>
<select class="uk-select uk-form-width-xsmall" size="1" id="game_nr" <select class="uk-select uk-form-width-xsmall" size="1" id="game_nr"
name="spiel_nr_<?php echo $teamnr; ?>" title="Spielnummer (optional)"> name="spiel_nr_<?php echo $teamnr; ?>" title="Spielnummer (optional)">
<option value=""></option> <option value=""></option>
<?php <?php
for ($i = 1; $i <= 99; $i++) for ($i = 1; $i <= 99; $i++)
{ {
echo "<option value=\"" . $i . "\"" . ($i == $spielnummer ? " selected " : "") . ">" . $i . "</option>"; echo "<option value=\"" . $i . "\"" . ($i == $spielnummer ? " selected " : "") . ">" . $i . "</option>";
} }
@@ -15428,7 +15475,7 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
for ($satz = 0; $satz < $saetze_maximal; $satz++) { for ($satz = 0; $satz < $saetze_maximal; $satz++) {
?> ?>
<select class="uk-select uk-form-width-medium" <select class="uk-select uk-form-width-medium"
name="ergebnis_punkte_heim[]" name="ergebnis_punkte_heim[]"
size="1" onchange="punkte_changed(<?php echo $satz; ?>, 1);" size="1" onchange="punkte_changed(<?php echo $satz; ?>, 1);"
aria-label="<?php echo Text::_('COM_SPORTSMANAGER_ARIA_LABEL_POINTS_HOME'); ?>"> aria-label="<?php echo Text::_('COM_SPORTSMANAGER_ARIA_LABEL_POINTS_HOME'); ?>">
<option value=""></option> <option value=""></option>
@@ -411,6 +411,51 @@ class HTML_sportsmanager_ticker
moreResults(matches, groups, day, page, 0); moreResults(matches, groups, day, page, 0);
}); });
</script> </script>
<style>
#theme-toggle {
float: right;
margin-top: 15px;
margin-right: 20px;
}
.theme-btn {
cursor: pointer;
margin-left: 10px;
font-size: 18px;
opacity: 0.5;
transition: opacity 0.2s;
}
.theme-btn:hover, .theme-btn.active {
opacity: 1;
}
</style>
<script>
function applyTheme(theme) {
if (theme === 'dark' || (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.body.classList.add('dark-mode');
} else {
document.body.classList.remove('dark-mode');
}
if (document.getElementById('theme-' + theme)) {
document.querySelectorAll('.theme-btn').forEach(function(btn) { btn.classList.remove('active'); });
document.getElementById('theme-' + theme).classList.add('active');
}
}
var savedTheme = localStorage.getItem('livescore-theme') || 'auto';
function setTheme(theme) {
localStorage.setItem('livescore-theme', theme);
applyTheme(theme);
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
if (localStorage.getItem('livescore-theme') === 'auto' || !localStorage.getItem('livescore-theme')) {
applyTheme('auto');
}
});
document.addEventListener('DOMContentLoaded', function() {
applyTheme(localStorage.getItem('livescore-theme') || 'auto');
});
</script>
</head> </head>
<body onResize="resizee()"> <body onResize="resizee()">
@@ -433,6 +478,11 @@ class HTML_sportsmanager_ticker
<div id="left_page_header"> <div id="left_page_header">
<h1 id="pagetitle_text">LIVE-TICKER</h1> <h1 id="pagetitle_text">LIVE-TICKER</h1>
</div> </div>
<div id="theme-toggle">
<span id="theme-auto" class="theme-btn" onclick="setTheme('auto')" title="Auto Theme">&#x1F4BB;</span>
<span id="theme-light" class="theme-btn" onclick="setTheme('light')" title="Light Theme">&#x2600;&#xFE0F;</span>
<span id="theme-dark" class="theme-btn" onclick="setTheme('dark')" title="Dark Theme">&#x1F319;</span>
</div>
<a href="<?php echo SportsManagerURL(); ?>" id="homeicon">Home</a> <a href="<?php echo SportsManagerURL(); ?>" id="homeicon">Home</a>
<div style="clear:both;"></div> <div style="clear:both;"></div>
<div id="left_menu"> <div id="left_menu">
@@ -1838,6 +1888,99 @@ class HTML_sportsmanager_ticker
height: 39px; height: 39px;
} }
} }
body.dark-mode {
background: #121212;
}
body.dark-mode #right_page {
background: #1e1e1e;
}
body.dark-mode h1#pagetitle_text {
color: #ffffff;
}
body.dark-mode a#homeicon {
color: #cccccc;
}
body.dark-mode #left_menu ul li a {
color: #ffffff;
}
body.dark-mode #tbl th {
color: #eeeeee;
background-color: #333333;
}
body.dark-mode #tbl tr td {
color: #dddddd;
}
body.dark-mode #detailedresults #tbl tr.odd td {
background-color: #242424;
}
body.dark-mode #detailedresults #tbl tr.even td {
background-color: #2a2a2a;
}
body.dark-mode #tbl tr.tablehead td {
background-color: #333333;
}
body.dark-mode tr.finished.odd {
background-color: #2a2a2a;
}
body.dark-mode tr.finished.even {
background-color: #2e2e2e;
}
body.dark-mode tr.updated {
background-color: #2d2616;
}
body.dark-mode tr.livenow {
background-color: #173824;
}
body.dark-mode tr.upcoming.odd {
background-color: #1a222f;
}
body.dark-mode tr.upcoming.even {
background-color: #1e2a3b;
}
body.dark-mode #tbl tr td.finished_winner {
color: #ffffff;
}
body.dark-mode tr.last_row {
background-color: #333333;
}
body.dark-mode #tbl tr td#last_row {
background-color: #333333;
}
body.dark-mode tr.upcoming.odd #resultat_holder,
body.dark-mode tr.upcoming.even #resultat_holder {
color: #eeeeee;
}
body.dark-mode #sponsorz {
background: #333333;
}
body.dark-mode .grey_button {
background: #333333;
}
body.dark-mode .grey_button a {
color: #eeeeee;
}
body.dark-mode .place_final {
color: #ffffff;
}
body.dark-mode .field_score {
color: #ffffff;
}
body.dark-mode .field_team {
color: #dddddd;
}
body.dark-mode .field_team.winner_bold {
color: #ffffff;
}
body.dark-mode #winner_area_positions span {
color: #dddddd;
}
body.dark-mode #winner_area_positions span#winner {
color: #ffffff;
}
body.dark-mode #winner_area_positions span#winner_name {
color: #ffffff;
}
<?php <?php
} }
+22 -1
View File
@@ -1274,7 +1274,28 @@ return new class () implements InstallerScriptInterface
$db->setQuery( $query ); $db->setQuery( $query );
if (!$db->execute()) { die($db->stderr(true)); } if (!$db->execute()) { die($db->stderr(true)); }
$query = "INSERT IGNORE #__sportsmanager_einstellungen SET name = 'datenbank_version', wert = '120';"; $query = "INSERT IGNORE #__sportsmanager_einstellungen SET name = 'datenbank_version', wert = '121';";
$db->setQuery( $query );
if (!$db->execute()) { die($db->stderr(true)); }
$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 ); $db->setQuery( $query );
if (!$db->execute()) { die($db->stderr(true)); } if (!$db->execute()) { die($db->stderr(true)); }
+23
View File
@@ -0,0 +1,23 @@
# DTFB Player Sync — QA findings & applied fixes
Concise record of the defects found while reviewing the player-sync receiver
(`syncReceiveSpielerImport()` in `sync.php`) and the six fixes applied in this PR.
The exploratory test harness used to find these has been removed — the
authoritative next test is the **staging end-to-end sync** (see the PR description).
## The six issues fixed
| # | Issue | Impact | Fix in `sync.php` |
|---|-------|--------|-------------------|
| 1 | **Receiver did not normalise input encoding.** A latin1 / Windows-1252 CSV (the legacy manual export) has `ß` as the single byte `0xDF`. Staging the org name into the utf8mb4 table truncated it at that byte, so the org lookup missed and **every row was skipped**. | Critical — silent data loss (e.g. 2964 rows parsed, 0 imported, `success=true`). | Transcode the payload to UTF-8 when it is not already valid UTF-8, before staging. |
| 2 | **A zero-effect import reported success.** When N rows parsed but nothing was added or updated, the function returned `success=true`. | Critical — masks encoding/mapping failures. | Return `success=false` with a diagnostic message when `rows>0` but `added==0 && updated==0`. |
| 3 | **Dead `$naechste_spielernr` block** referenced an undefined variable in the insert branch. | Runtime notice; incoming rows already carry their Passnummer. | Removed the block. |
| 4 | **Unconditional mass-deactivation.** A partial CSV deactivated every member not listed. | High — a broken/partial export could wipe an org's roster. | Per organisation, skip the deactivation sweep when the incoming count is below `sync_deactivation_min_ratio` (default 0.5) of the org's currently-active members; adds/updates still proceed and a warning is returned. |
| 5 | **Split clock for staging.** `session_id` came from PHP `date()` while stale-row cleanup used MySQL `NOW()`; a PHP/MySQL timezone gap could delete in-flight staging rows. | Medium — another silent 0-row path. | Derive `session_id` from the DB clock (`NOW()`), with `date()` fallback. |
| 6 | **No Passnummer format check.** The manual import UI enforces `^[0-9]{2}-[0-9]{4,6}$`; the sync receiver did not. | Medium — inconsistent data quality vs the manual flow. | Apply the same regex to `spielernr` / `spielernr_alt` (blank/reject on mismatch). |
## Confirmed by design (not bugs)
- **No contact / personal data** (email, phone, address) is ever exported or imported.
- Existing players keep their `lizenznr` and `geburtsjahr` on update; only name / sex / Passnummer-driven fields change.
- An unknown organisation aborts the whole import with no mutation.
- The sync path itself (export → cURL push → receive) is UTF-8 end-to-end; the encoding defect (#1) only affected ingesting a legacy latin1 *manual* file.