Compare commits

..
Author SHA1 Message Date
svennickel 5aee02830d Merge pull request #305 from Deutscher-Tischfussballbund/sportsmanager2-issue289
Zusatzinfo-Feld für Spielorte
2026-09-06 21:16:49 +02:00
Jürgen MeyerandClaude Sonnet 5 cb2e614604 Zusatzinfo-Feld für Spielort im Admin-Formular ergänzt und Frontend-Anzeige der Heimspielstätte um Weitere Infos, Ruhetag und Training erweitert.
Adresse der Heimspielstätte verlinkt jetzt zusätzlich auf Google Maps.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 17:50:42 +02:00
svennickel 5d126b0d28 Merge pull request #303 from Deutscher-Tischfussballbund/sportsmanager2-issue300
Überarbeitung Hall of Fame
2026-09-04 23:24:10 +02:00
Jürgen Meyer 69cf0e46ab Datenbankvorbereitung für issue 289 2026-09-04 11:34:07 +02:00
Jürgen Meyer df794966df Hall of Fame: Jahres-/Id-Filter korrigiert, inaktive Vereine bei Namens-Links berücksichtigt
- halloffame_id: Bereichssuche (z.B. "1-5") ist nicht mehr möglich, nur noch einzelne Werte.
- jahr: Bereichs- und Einzelwerte werden auf 1900..aktuelles Jahr+5 begrenzt; liegt bei einem Bereich der 2. Wert unter dem 1., werden die Werte vertauscht statt den Bereich zu verwerfen.
- halloffameNameLink verlinkt bei Vereinen jetzt nur noch, wenn der Verein nicht ausgetreten ist (analog zur bestehenden Spieler-Aktiv-Prüfung).
2026-09-04 08:35:27 +02:00
Jürgen MeyerandClaude Sonnet 5 12da0a9c00 Überarbeitung Hall of Fame
## Neue Frontend-Darstellungen (gruppierung)
- gruppierung=jahr - Jahresrückblick: eine Tabelle je Jahr mit allen Disziplinen, inkl. mehrerer Plätze, Team-Mitspielern und Spalten-/Zeilen-Layout wie auf der Detailseite.
- gruppierung=summe - Ranking über alle Disziplinen hinweg: ein Eintrag je Spieler/Team mit mindestens einem 1. Platz, inkl. Titelanzahl und Auflistung aller Titel (Disziplin, Jahre).
- gruppierung=summedisziplin - wie summe, aber eine eigene Rangliste je Disziplin.

## Neue/erweiterte Steuerungsparameter
- platzierung (inspalten/inzeilen), plaetzezeigen, sortierung (kontextabhängig: Jahresreihenfolge oder anzahl/name in den Summen-Ansichten).
- anzahltitel - Mindestanzahl an Titeln für die Summen-Ansichten.
- halloffame_id und jahr als Filter für alle Hall-of-Fame-Seiten, mit Komma- und Bereichs-Syntax (2006-2010,2015).
- Titel fällt auf den Joomla-Menütitel zurück, wenn kein eigener Titel im Menüpunkt gesetzt ist.
- Überschrift semantisch als h1 statt div.

## Mannschaftsspieler-Feld
- Neues Datenbankfeld teamspieler (TEXT) an #__sportsmanager_mitglied_von_halloffame, Eingabe im Backend als Textarea (nur bei Mannschaftsdisziplinen).
- Anzeige im Frontend bei platzierung=inzeilen oder wenn nur ein Platz gezeigt wird, sofern mindestens ein Eintrag >= 3 Zeichen vorhanden ist.

## Verlinkung & Bilder
- Spieler-, Vereins- und Mannschaftsnamen sind jetzt überall anklickbar (Detailseite, Jahresübersicht, Summen-Ansichten) und führen zur jeweiligen Profilseite - inaktive Spieler (ohne aktuellen Verein) bleiben unverlinkt.
- Dummybilder: neues geschlechtsneutrales Bild (spieler-n.png) für Spieler ohne Datenbank-ID; deren Geschlecht wird zusätzlich anhand der übrigen Spieler mit ID derselben Disziplin geschätzt, bevor auf das neutrale Bild zurückgefallen wird.
- Fehlt der Name zu einer Platzierung, wird in der Spaltenansicht nur das Bild weggelassen, in der Zeilenansicht die komplette Zeile.

## Neues Feld "nicht_ausgespielt"
- Neue Spalte nicht_ausgespielt (BOOL) hinter jahr an #__sportsmanager_mitglied_von_halloffame (Teil von Datenbank-Version 121).
- Admin-Formular: Checkbox neben dem Jahr-Feld; bei Aktivierung werden alle anderen Felder deaktiviert, beim Speichern werden Platz 2/3 gelöscht und Platz 1 auf reines nicht_ausgespielt=1 ohne Team-/Spielerdaten gesetzt.
- Frontend-Detailseite und Backend-Jahresliste zeigen bei solchen Einträgen "Nicht ausgespielt" statt Team-/Spielername an.
- Jahresübersicht und beide Summen-Ansichten überspringen solche Einträge vollständig.

## Vereins-Dropdown im Backend gruppiert
- Die Vereinsliste im Auswahlfeld (Plätze 1-3) ist jetzt in zwei optgroup-Blöcke aufgeteilt: "Aktive Vereine" / "Inaktive Vereine" (neue Sprachkonstanten COM_SPORTSMANAGER_ACTIVE_CLUBS / COM_SPORTSMANAGER_INACTIVE_CLUBS). Innerhalb jeder Gruppe bleibt die alphabetische Sortierung erhalten.

## Routing-Fix
- task=team_details und task=verein_details funktionierten nur, wenn der aktuelle Menüpunkt bereits auf content=teams bzw. content=vereine stand (z. B. beim Verlinken von der Hall-of-Fame-Seite aus nicht). Beide Tasks laufen jetzt wie spieler_details über eine content-unabhängige Route.

## Performance
- Options-Liste für Vereins-/Spieler-Dropdowns im Backend-Formular wurde bis zu 6x komplett neu gerendert (3 Plätze x 2 Spieler bzw. 3x Vereinsliste). Jetzt wird sie nur noch 1x aufgebaut, in einem <template>-Element abgelegt und per JavaScript in die <select>-Felder befüllt.
- Team-ID-Auflösung für Mannschaften ohne Vereinszuordnung läuft jetzt über eine einmalig geladene Liste statt einer Datenbankabfrage pro Zeile.

## Bugfixes
- halloffameTeamIdFuerTeamname() erwartete einen nicht-nullbaren string und stürzte bei nicht_ausgespielt-Einträgen (teamname = NULL) mit einem TypeError ab. Parameter ist jetzt nullable, leerer/NULL-Teamname liefert direkt null zurück.
- Bei Einzel-Disziplinen wurden Jahre in den Summen-Ansichten doppelt gezählt, wenn in der Datenbank (z. B. aus Altdaten) noch ein spieler2_id hinterlegt war; wird jetzt nur noch bei echten Doppel-Disziplinen berücksichtigt.

## Migration
- Datenbank-Version 121: neues Feld teamspieler, neue Spalte nicht_ausgespielt, Bereitstellung des neutralen Dummybilds für Bestandsinstallationen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 11:33:38 +02:00
svennickel ca827a049f Merge pull request #302 from Deutscher-Tischfussballbund/sportsmanager2-issue301
Export von kompletten Ligen im json-Format
2026-08-30 15:16:26 +02:00
svennickel 70fb4d97b7 Merge pull request #299 from Deutscher-Tischfussballbund/sportsmanager2-issue298
Zusätzliche Link-Parameter für Menüeinträge "Allgemeine Inhalte"
2026-08-30 13:43:04 +02:00
Jürgen Meyer d39dc45cec Änderungsvorschläge abgearbeitet. 2026-08-30 11:57:17 +02:00
Jürgen Meyer c290986bac Änderung wie Vorgeschlagen übernommen. 2026-08-30 10:15:48 +02:00
Jürgen Meyer 9541bc0652 Export von kompletten Ligen im json-Format 2026-08-28 15:52:39 +02:00
jmeyer26 50e6872214 Merge pull request #288 from Deutscher-Tischfussballbund/claude/season-selection-url-persist-6yz518
Persist season selection in URL instead of cookie-only
2026-08-28 12:33:47 +02:00
Jürgen Meyer a953ebf130 Zusätzliche Link-Parameter für Menüeinträge "Allgemeine Inhalte" 2026-08-27 15:03:45 +02:00
MarvinF e8051bbcf6 Merge pull request #297 from Deutscher-Tischfussballbund/sportsmanager2-issue296
Mail sending silently fails
2026-08-25 22:51:32 +02:00
Jürgen Meyer a01efae2e9 Exception Fehler bei E-Mail Versand 2026-08-25 20:05:50 +02:00
MarvinF 04e4ea6cdf Merge pull request #295 from Deutscher-Tischfussballbund/294-fix-string-comparison-spielpunkte-bedingung
Match report: goals compared as strings – winner swapped for two-digit results
2026-08-25 12:40:01 +02:00
svennickel f787681fec fix: compare goals numerically in spielpunkte_bedingung()
Refs #294
2026-08-24 23:10:16 +02:00
MarvinF 541af8fb7f Merge branch 'sportsmanager2-stage' into sportsmanager2-dev 2026-07-16 23:16:50 +02:00
MarvinF cc92b938be Merge pull request #291 from Deutscher-Tischfussballbund/sportsmanager2-issue290
Bei Teilnehmerzahl 1 oder weniger eines Ranglistenturnieres werden oh…
2026-07-16 23:14:36 +02:00
Jürgen Meyer 4c21d66fbe Bei Teilnehmerzahl 1 oder weniger eines Ranglistenturnieres werden ohne weitere Berechnung 0 Punkte vergeben. 2026-07-16 14:27:34 +02:00
MarvinF afb89dba6e Rename dev_preview to dev_preview.yml 2026-06-16 21:07:55 +02:00
MarvinF c4d599834a Create dev_preview 2026-06-16 21:06:47 +02:00
Claude dcea759ab2 Persist season selection in URL instead of cookie-only
Season filter dropdowns in all frontend views (Mannschaftswettbewerbe,
Turniere, Teams, Spielerstatistiken, Individualwettbewerbe, Ranglisten)
now navigate via JS URL update (window.location with searchParams) instead
of submitting a POST form. This puts filter_saison_id in the URL, making
season-filtered views bookmarkable and shareable.

The PHP backend already reads filter_saison_id from $_REQUEST (covers GET),
and the cookie is still set as fallback for links that don't carry the param.

For veranstaltungHeaderAlone (single competition detail view), the JS also
resets task=veranstaltungen and removes veranstaltungid on season change,
matching the previous form-based behaviour of navigating back to the list.

https://claude.ai/code/session_01VyKCAgyEfixxR6RhAkBSEL
2026-06-13 08:31:37 +00:00
MarvinF 19037d0729 Merge branch 'sportsmanager2-prod' into sportsmanager2-stage 2026-05-13 00:06:44 +02:00
MarvinF bfc65d6030 Merge pull request #284 from Deutscher-Tischfussballbund/sportsmanager2-dev
dev to stage
2026-05-13 00:05:08 +02:00
MarvinF 7d47b3ed24 Merge pull request #281 from Deutscher-Tischfussballbund/sportsmanager2-stage
stage to prod
2026-04-14 19:10:16 +02:00
19 changed files with 2266 additions and 1748 deletions
+84
View File
@@ -0,0 +1,84 @@
# Builds the codebase and publishes a rolling dev-preview release (no version bump).
# The release uses a fixed tag "dev-preview" so each run replaces the previous one.
name: Sportsmanager Dev Preview
on:
workflow_dispatch:
inputs:
branch:
description: 'Branch to build'
required: true
default: 'sportsmanager2-prod'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.branch }}
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install npm dependencies
run: npm ci
- name: Run build script
run: npm run release
- name: Delete existing dev-preview release (if any)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
run: |
release_id=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$REPO/releases/tags/dev-preview" \
| jq -r '.id // empty')
if [ -n "$release_id" ]; then
curl -s -X DELETE -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$REPO/releases/$release_id"
echo "Deleted existing dev-preview release (ID: $release_id)"
else
echo "No existing dev-preview release found"
fi
- name: Delete existing dev-preview tag (if any)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
run: |
status=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$REPO/git/refs/tags/dev-preview")
if [ "$status" = "200" ]; then
curl -s -X DELETE -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$REPO/git/refs/tags/dev-preview"
echo "Deleted existing dev-preview tag"
else
echo "No existing dev-preview tag found"
fi
- name: Publish dev-preview release
uses: softprops/action-gh-release@v2
with:
tag_name: dev-preview
name: "Dev Preview (${{ github.run_number }})"
body: |
Automated dev preview build from branch `${{ inputs.branch }}`.
Commit: ${{ github.sha }}
Run: ${{ github.run_number }}
> This is a temporary preview release and will be replaced on the next run.
files: package/packages/com_sportsmanager.zip
draft: false
prerelease: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-22
View File
@@ -1,22 +0,0 @@
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"
+19 -5
View File
@@ -52,7 +52,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.0.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.0.tgz",
"integrity": "sha512-iV7Gwg0DePKvdDZZWRTkj4MW+6/AbVWd4ZCg+zk8H1RVt5xBpUZS6vLQWwb3pyLg4BFTaGiQCPoJ4Ibmbne4fA==", "integrity": "sha512-iV7Gwg0DePKvdDZZWRTkj4MW+6/AbVWd4ZCg+zk8H1RVt5xBpUZS6vLQWwb3pyLg4BFTaGiQCPoJ4Ibmbne4fA==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.10.4", "@babel/code-frame": "^7.10.4",
"@babel/generator": "^7.12.0", "@babel/generator": "^7.12.0",
@@ -2695,10 +2694,25 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001148", "version": "1.0.30001810",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001148.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
"integrity": "sha512-E66qcd0KMKZHNJQt9hiLZGE3J4zuTqE1OnU53miEVtylFbwOEmeA5OsRu90noZful+XGSQOni1aT2tiqu/9yYw==", "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
"dev": true "dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "CC-BY-4.0"
}, },
"node_modules/chalk": { "node_modules/chalk": {
"version": "1.1.3", "version": "1.1.3",
@@ -30,6 +30,8 @@ COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_DESCRIPTION="Beschreibung"
COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_DESCRIPTION_DESC="Beschreibung, die unterhalb des Titels angezeigt wird (WICHTIG: Werden HTML-Tags verwendet, müssen auch Umlaute in HTML-Code angeben werden)" COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_DESCRIPTION_DESC="Beschreibung, die unterhalb des Titels angezeigt wird (WICHTIG: Werden HTML-Tags verwendet, müssen auch Umlaute in HTML-Code angeben werden)"
COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_CATEGORIES="Kategorien" COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_CATEGORIES="Kategorien"
COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_CATEGORIES_DESC="Eine optionale Auswahl von Kategorienummern durch Kommata oder Spiegelstrich getrennt" COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_CATEGORIES_DESC="Eine optionale Auswahl von Kategorienummern durch Kommata oder Spiegelstrich getrennt"
COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_EXTRA_PARAMS="Zusätzliche Parameter"
COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_EXTRA_PARAMS_DESC="Optionale zusätzliche Parameter für den Link in der Form Name1=Wert1&Name2=Wert2, die bei Aufruf dieses Menüpunkts wie GET-Parameter zur Verfügung stehen"
COM_SPORTSMANAGER_LAYOUT_ELO_RANKING_TITLE="Layout: Elo-Rangliste" COM_SPORTSMANAGER_LAYOUT_ELO_RANKING_TITLE="Layout: Elo-Rangliste"
COM_SPORTSMANAGER_LAYOUT_ELO_RANKING_DESC="Auflistung der Spieler sortiert nach Elo-Wertung" COM_SPORTSMANAGER_LAYOUT_ELO_RANKING_DESC="Auflistung der Spieler sortiert nach Elo-Wertung"
COM_SPORTSMANAGER_LAYOUT_ELO_RANKING_OPTION_ELO_RANKING="Elo-Rangliste" COM_SPORTSMANAGER_LAYOUT_ELO_RANKING_OPTION_ELO_RANKING="Elo-Rangliste"
@@ -30,6 +30,8 @@ COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_DESCRIPTION="Description"
COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_DESCRIPTION_DESC="Description that will be shows below the titel (IMPORTANT: if html tags are used, special characters must be maskeraded)" COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_DESCRIPTION_DESC="Description that will be shows below the titel (IMPORTANT: if html tags are used, special characters must be maskeraded)"
COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_CATEGORIES="Categories" COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_CATEGORIES="Categories"
COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_CATEGORIES_DESC="An optional selection of category numbers seperated by commas or bullet point" COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_CATEGORIES_DESC="An optional selection of category numbers seperated by commas or bullet point"
COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_EXTRA_PARAMS="Additional parameters"
COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_EXTRA_PARAMS_DESC="Optional additional parameters for the link in the form name1=value1&name2=value2, made available as GET parameters whenever this menu item is called"
COM_SPORTSMANAGER_LAYOUT_ELO_RANKING_TITLE="Layout: elo ranking" COM_SPORTSMANAGER_LAYOUT_ELO_RANKING_TITLE="Layout: elo ranking"
COM_SPORTSMANAGER_LAYOUT_ELO_RANKING_DESC="Listing of players sorted by elo rating" COM_SPORTSMANAGER_LAYOUT_ELO_RANKING_DESC="Listing of players sorted by elo rating"
COM_SPORTSMANAGER_LAYOUT_ELO_RANKING_OPTION_ELO_RANKING="Elo ranking" COM_SPORTSMANAGER_LAYOUT_ELO_RANKING_OPTION_ELO_RANKING="Elo ranking"
File diff suppressed because it is too large Load Diff
@@ -5709,6 +5709,7 @@ 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,29 +5742,40 @@ function updateDatabase(): void
} }
if ($datenbank_version < 121) { if ($datenbank_version < 121) {
$query = "CREATE TABLE IF NOT EXISTS `#__sportsmanager_sync_log` ("
. "\n `sync_id` INT(11) NOT NULL AUTO_INCREMENT," $columns = $db->getTableColumns('#__sportsmanager_mitglied_von_halloffame');
. "\n `sync_timestamp` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP," if (!array_key_exists('teamspieler', $columns)){
. "\n `sync_direction` ENUM('push', 'receive') NOT NULL," $query = "ALTER TABLE `#__sportsmanager_mitglied_von_halloffame`"
. "\n `sync_trigger` ENUM('manual', 'cron', 'api') NOT NULL," . "\n ADD `teamspieler` TEXT NULL DEFAULT NULL AFTER `teamname`;";
. "\n `sync_status` ENUM('success', 'error') NOT NULL," $db->setQuery($query);
. "\n `spieler_count` INT(11) DEFAULT 0," if (!$db->execute()) {
. "\n `spieler_updated` INT(11) DEFAULT 0," die($db->stderr(true));
. "\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 = '';"; if (!array_key_exists('nicht_ausgespielt', $columns)){
$db->setQuery($query); $query = "ALTER TABLE `#__sportsmanager_mitglied_von_halloffame`"
if (!$db->execute()) { . "\n ADD `nicht_ausgespielt` TINYINT(1) NOT NULL DEFAULT '0' AFTER `jahr`;";
die($db->stderr(true)); $db->setQuery($query);
if (!$db->execute()) {
die($db->stderr(true));
}
}
$columns = $db->getTableColumns('#__sportsmanager_spielort');
if (!array_key_exists('zusatzinfo', $columns)){
$query = "ALTER TABLE `#__sportsmanager_spielort`"
. "\n ADD `zusatzinfo` TEXT NULL DEFAULT NULL AFTER `ruhetage`;";
$db->setQuery($query);
if (!$db->execute()) {
die($db->stderr(true));
}
}
$zielpfad = JPATH_ROOT . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'sportsmanager' . DIRECTORY_SEPARATOR . 'spieler' . DIRECTORY_SEPARATOR . 'n.png';
if (!is_file($zielpfad)) {
$quellpfad = JPATH_SITE . DIRECTORY_SEPARATOR . 'components' . DIRECTORY_SEPARATOR . 'com_sportsmanager' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'spieler-n.png';
bildKopierenAngepasst($quellpfad, $zielpfad, 180, 240, 1);
} }
$query = "UPDATE #__sportsmanager_einstellungen" $query = "UPDATE #__sportsmanager_einstellungen"
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -44,7 +44,6 @@ 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();
@@ -62,8 +61,27 @@ global $params;
$app = Factory::getContainer()->get(SiteApplication::class); $app = Factory::getContainer()->get(SiteApplication::class);
$jInput = $app->input; $jInput = $app->input;
$task = $jInput->getCmd('task');
$params = $app->getParams('com_sportsmanager'); $params = $app->getParams('com_sportsmanager');
// zusätzliche, im Menüpunkt hinterlegte Parameter (z.B. "test=test&foo=bar") wie GET-Parameter verfügbar machen;
// tatsächlich in der URL übergebene Werte haben weiterhin Vorrang (def() setzt nur, wenn noch nicht vorhanden);
// muss vor dem Auslesen von task/content erfolgen, damit diese die Zusatzparameter berücksichtigen
$zusatzparameter = trim((string) $params->get('zusatzparameter', ''));
if ($zusatzparameter !== '') {
// Namen, die den gesamten Seitenaufbau umstellen würden, nicht über einen Menüparameter
$gesperrteNamen = ['option', 'Itemid', 'format', 'tmpl', 'lang'];
parse_str($zusatzparameter, $zusatzparameterWerte);
foreach ($zusatzparameterWerte as $zusatzparameterName => $zusatzparameterWert) {
// Arrays (z.B. "task[]=x") würden weiter unten zu einem TypeError führen
if (!is_scalar($zusatzparameterWert)
|| in_array($zusatzparameterName, $gesperrteNamen, true)) {
continue;
}
$jInput->def($zusatzparameterName, $zusatzparameterWert);
}
}
$task = $jInput->getCmd('task');
$content = isJson() && $jInput->getCmd('content', NULL) != NULL ? $jInput->getCmd('content') : $params->get('content'); $content = isJson() && $jInput->getCmd('content', NULL) != NULL ? $jInput->getCmd('content') : $params->get('content');
if (berechnungen()) if (berechnungen())
@@ -75,10 +93,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') { } else if ($task == "team_details") {
apiSyncSpielerReceive(); mannschaftDetails($content == "teams_vereinigt", $content != "teams" && $content != "teams_vereinigt");
} else if ($task === 'api_sync_spieler_trigger') { } else if ($task == "verein_details") {
apiSyncSpielerTrigger(); vereinDetails();
} 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) {
@@ -139,9 +157,6 @@ 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;
@@ -855,9 +870,6 @@ if ($task == "spielerbild") {
case 'begegnung_spielplan': case 'begegnung_spielplan':
begegnungSpielplan(true); begegnungSpielplan(true);
break; break;
case 'team_details':
mannschaftDetails($content == "teams_vereinigt");
break;
case 'team_spielplan_xml': case 'team_spielplan_xml':
teamSpielplanXML(); teamSpielplanXML();
case 'team_begegnungen_ical': case 'team_begegnungen_ical':
@@ -868,9 +880,6 @@ if ($task == "spielerbild") {
} }
} else if ($content == "vereine") { } else if ($content == "vereine") {
switch ($task) { switch ($task) {
case 'verein_details':
vereinDetails();
break;
case 'verein_begegnungen_ical': case 'verein_begegnungen_ical':
vereinBegegnungeniCal(); vereinBegegnungeniCal();
break; break;
@@ -1154,7 +1163,7 @@ if ($task == "spielerbild") {
return; return;
#[NoReturn] function redirectSportsManagerURL($weitereParameter = NULL, $nachricht = ''): void #[NoReturn] function redirectSportsManagerURL($weitereParameter = NULL, $nachricht = '', $nachrichtTyp = 'message'): void
{ {
global $redirect_session_id; global $redirect_session_id;
if (!empty($redirect_session_id)) { if (!empty($redirect_session_id)) {
@@ -1164,7 +1173,7 @@ return;
} }
$app = Factory::getContainer()->get(SiteApplication::class); $app = Factory::getContainer()->get(SiteApplication::class);
if ($nachricht != '') { if ($nachricht != '') {
$app->enqueueMessage($nachricht); $app->enqueueMessage($nachricht, $nachrichtTyp);
} }
$app->redirect(SportsManagerURL($weitereParameter), 200); $app->redirect(SportsManagerURL($weitereParameter), 200);
exit; exit;
@@ -4458,11 +4467,34 @@ function halloffame(): void
{ {
$db = getDatabase(); $db = getDatabase();
global $params; global $params;
$jInput = Factory::getContainer()->get(SiteApplication::class)->input;
$gruppierung = $jInput->get('gruppierung', '', 'STRING');
if ($gruppierung !== 'jahr' && $gruppierung !== 'summe' && $gruppierung !== 'summedisziplin')
$gruppierung = '';
if ($gruppierung === 'jahr') {
halloffameNachJahrGruppiert();
return;
}
if ($gruppierung === 'summe') {
halloffameNachSummeGruppiert();
return;
}
if ($gruppierung === 'summedisziplin') {
halloffameNachSummeDisziplinGruppiert();
return;
}
$query = "SELECT t1.*, COUNT(DISTINCT t2.jahr) AS anzahl," $query = "SELECT t1.*, COUNT(DISTINCT t2.jahr) AS anzahl,"
. "\n IF (COUNT(DISTINCT t2.jahr) > 0, CONCAT(min(t2.jahr), ' - ', max(t2.jahr)), '" . Text::_('COM_SPORTSMANAGER_NO_ENTRY') . "') AS zeitspanne" . "\n IF (COUNT(DISTINCT t2.jahr) > 0, CONCAT(min(t2.jahr), ' - ', max(t2.jahr)), '" . Text::_('COM_SPORTSMANAGER_NO_ENTRY') . "') AS zeitspanne"
. "\n FROM #__sportsmanager_halloffame t1" . "\n FROM #__sportsmanager_halloffame t1"
. "\n LEFT JOIN #__sportsmanager_mitglied_von_halloffame t2 ON t2.halloffame_id = t1.halloffame_id" . "\n LEFT JOIN #__sportsmanager_mitglied_von_halloffame t2 ON t2.halloffame_id = t1.halloffame_id"
. "\n WHERE 1=1"
. halloffameIdFilter("AND t1.halloffame_id IN")
. halloffameJahrFilter("AND t2.jahr IN")
. "\n GROUP BY t1.halloffame_id" . "\n GROUP BY t1.halloffame_id"
. kategorieFilter("HAVING t1.kategorie IN") . kategorieFilter("HAVING t1.kategorie IN")
. "\n ORDER BY t1.reihenfolge;"; . "\n ORDER BY t1.reihenfolge;";
@@ -4474,11 +4506,363 @@ function halloffame(): void
if (isJson()) { if (isJson()) {
echo json_encode($halloffame); echo json_encode($halloffame);
} else { } else {
HTML_sportsmanager::halloffame($params->get('titel'), $params->get('beschreibung'), $halloffame); HTML_sportsmanager::halloffame(halloffameTitel(), $params->get('beschreibung'), $halloffame);
} }
} }
} }
function halloffameNachJahrGruppiert(): void
{
$db = getDatabase();
global $params;
$jInput = Factory::getContainer()->get(SiteApplication::class)->input;
$platzierung = $jInput->get('platzierung', '', 'STRING');
if ($platzierung !== 'inspalten' && $platzierung !== 'inzeilen')
$platzierung = '';
$plaetzezeigen = $jInput->get('plaetzezeigen', 0, 'INT');
$sortierung = $jInput->get('sortierung', 'jahrabwaerts', 'STRING');
if ($sortierung !== 'jahraufwaerts')
$sortierung = 'jahrabwaerts';
$jahrSortierung = $sortierung === 'jahraufwaerts' ? 'ASC' : 'DESC';
$query = "SELECT t2.*, t1.halloffame, t1.spielform, t1.reihenfolge"
. "\n FROM #__sportsmanager_halloffame t1"
. "\n LEFT JOIN #__sportsmanager_mitglied_von_halloffame t2 ON t2.halloffame_id = t1.halloffame_id"
. "\n WHERE t2.jahr IS NOT NULL"
. kategorieFilter("AND t1.kategorie IN")
. halloffameIdFilter("AND t1.halloffame_id IN")
. halloffameJahrFilter("AND t2.jahr IN")
. "\n ORDER BY t2.jahr $jahrSortierung, t1.reihenfolge, t2.platz ASC;";
$rows = loadObjectList($db, $query);
$alleTeams = halloffameAlleTeams($db);
$alleSpielerInfo = halloffameAlleSpielerInfo($db);
$alleVereineInfo = halloffameAlleVereineInfo($db);
$disziplinen = [];
$jahresDaten = [];
foreach ($rows as $row) {
if (!isset($disziplinen[$row->halloffame_id])) {
$disziplinen[$row->halloffame_id] = (object) [
'halloffame_id' => $row->halloffame_id,
'halloffame' => $row->halloffame,
'spielform' => $row->spielform,
'reihenfolge' => $row->reihenfolge,
'platz2_zeigen' => 0,
'platz3_zeigen' => 0,
'teamspieler_zeigen' => 0,
'anzahl_m' => 0,
'anzahl_w' => 0,
];
}
$disziplin = $disziplinen[$row->halloffame_id];
if (!empty($row->nicht_ausgespielt)) continue;
if (!isset($jahresDaten[$row->jahr]))
$jahresDaten[$row->jahr] = [];
if (!isset($jahresDaten[$row->jahr][$row->halloffame_id]))
$jahresDaten[$row->jahr][$row->halloffame_id] = new stdClass();
$ziel = $jahresDaten[$row->jahr][$row->halloffame_id];
if ($disziplin->spielform == 1) {
$index_vereinid = "verein_id_" . $row->platz;
$index_teamid = "team_id_" . $row->platz;
$index_team = "teamname_" . $row->platz;
$index_teamspieler = "teamspieler_" . $row->platz;
$ziel->$index_vereinid = $row->verein_id;
$ziel->$index_team = $row->teamname;
$ziel->$index_teamspieler = $row->teamspieler;
if (empty($row->verein_id)) {
$ziel->$index_teamid = halloffameTeamIdFuerTeamname($alleTeams, $row->teamname);
} else {
$ziel->$index_teamid = "";
}
if ($row->platz == 2 && !empty($row->teamname))
$disziplin->platz2_zeigen = 1;
if ($row->platz == 3 && !empty($row->teamname))
$disziplin->platz3_zeigen = 1;
if (mb_strlen(trim((string) $row->teamspieler)) >= 3)
$disziplin->teamspieler_zeigen = 1;
} else {
$index_spieler1id = "spieler1_id_" . $row->platz;
$index_spieler1 = "spieler1_" . $row->platz;
$index_spieler2id = "spieler2_id_" . $row->platz;
$index_spieler2 = "spieler2_" . $row->platz;
$ziel->$index_spieler1id = $row->spieler1_id;
$ziel->$index_spieler1 = $row->spieler1;
$ziel->$index_spieler2id = $row->spieler2_id;
$ziel->$index_spieler2 = $row->spieler2;
if ($row->platz == 2 && (!empty($row->spieler1) || !empty($row->spieler2)))
$disziplin->platz2_zeigen = 1;
if ($row->platz == 3 && (!empty($row->spieler1) || !empty($row->spieler2)))
$disziplin->platz3_zeigen = 1;
halloffameGeschlechtZaehlen($disziplin, $alleSpielerInfo, $row->spieler1_id, $disziplin->spielform == 2 ? $row->spieler2_id : null);
}
}
foreach ($disziplinen as $disziplin)
$disziplin->geschaetztes_geschlecht = halloffameGeschaetztesGeschlecht($disziplin->anzahl_m, $disziplin->anzahl_w);
if ($plaetzezeigen > 0) {
foreach ($disziplinen as $disziplin) {
if ($plaetzezeigen < 2)
$disziplin->platz2_zeigen = 0;
if ($plaetzezeigen < 3)
$disziplin->platz3_zeigen = 0;
}
}
usort($disziplinen, fn($a, $b) => $a->reihenfolge <=> $b->reihenfolge);
if (isJson()) {
echo json_encode(['disziplinen' => $disziplinen, 'jahre' => $jahresDaten]);
} else {
HTML_sportsmanager::halloffameNachJahrGruppiert(halloffameTitel(), $params->get('beschreibung'), $disziplinen, $jahresDaten, $platzierung, $alleSpielerInfo, $alleVereineInfo);
}
}
function halloffameSummeTitelHinzufuegen(object $traegerEintrag, object $row): void
{
if (!isset($traegerEintrag->titel[$row->halloffame_id])) {
$traegerEintrag->titel[$row->halloffame_id] = (object) [
'halloffame' => $row->halloffame,
'jahre' => [],
];
}
$traegerEintrag->titel[$row->halloffame_id]->jahre[] = $row->jahr;
}
function halloffameNachSummeGruppiert(): void
{
$db = getDatabase();
global $params;
$jInput = Factory::getContainer()->get(SiteApplication::class)->input;
$anzahltitel = $jInput->get('anzahltitel', 0, 'INT');
$sortierung = $jInput->get('sortierung', 'anzahl', 'STRING');
if ($sortierung !== 'name')
$sortierung = 'anzahl';
$query = "SELECT t2.*, t1.halloffame, t1.spielform, t1.reihenfolge"
. "\n FROM #__sportsmanager_halloffame t1"
. "\n LEFT JOIN #__sportsmanager_mitglied_von_halloffame t2 ON t2.halloffame_id = t1.halloffame_id"
. "\n WHERE t2.jahr IS NOT NULL AND t2.platz = 1"
. kategorieFilter("AND t1.kategorie IN")
. halloffameIdFilter("AND t1.halloffame_id IN")
. halloffameJahrFilter("AND t2.jahr IN")
. "\n ORDER BY t1.reihenfolge, t2.jahr ASC;";
$rows = loadObjectList($db, $query);
$alleTeams = halloffameAlleTeams($db);
$alleSpielerInfo = halloffameAlleSpielerInfo($db);
$alleVereineInfo = halloffameAlleVereineInfo($db);
$traeger = [];
foreach ($rows as $row) {
if (!empty($row->nicht_ausgespielt)) continue;
if ($row->spielform == 1) {
$name = trim($row->teamname);
if ($name === '') continue;
$key = 'team:' . $name;
if (!isset($traeger[$key])) {
if (empty($row->verein_id)) {
$bild_type = 'mannschaften';
$bild_id = halloffameTeamIdFuerTeamname($alleTeams, $row->teamname);
} else {
$bild_type = 'vereine';
$bild_id = $row->verein_id;
}
$traeger[$key] = (object) [
'name' => $name,
'bild_type' => $bild_type,
'bild_id' => $bild_id,
'aktiv' => $bild_type === 'vereine' ? halloffameVereinAktiv($alleVereineInfo, $bild_id) : true,
'geschlecht' => null,
'titel' => [],
];
}
halloffameSummeTitelHinzufuegen($traeger[$key], $row);
} else {
if (!empty($row->spieler1_id)) {
$key = 'spieler:' . $row->spieler1_id;
if (!isset($traeger[$key])) {
$traeger[$key] = (object) [
'name' => $row->spieler1,
'bild_type' => 'spieler',
'bild_id' => $row->spieler1_id,
'aktiv' => halloffameSpielerAktiv($alleSpielerInfo, $row->spieler1_id),
'geschlecht' => $alleSpielerInfo[$row->spieler1_id]->geschlecht ?? null,
'titel' => [],
];
}
halloffameSummeTitelHinzufuegen($traeger[$key], $row);
}
if ($row->spielform == 2 && !empty($row->spieler2_id)) {
$key = 'spieler:' . $row->spieler2_id;
if (!isset($traeger[$key])) {
$traeger[$key] = (object) [
'name' => $row->spieler2,
'bild_type' => 'spieler',
'bild_id' => $row->spieler2_id,
'aktiv' => halloffameSpielerAktiv($alleSpielerInfo, $row->spieler2_id),
'geschlecht' => $alleSpielerInfo[$row->spieler2_id]->geschlecht ?? null,
'titel' => [],
];
}
halloffameSummeTitelHinzufuegen($traeger[$key], $row);
}
}
}
foreach ($traeger as $t) {
$t->anzahl = 0;
foreach ($t->titel as $titel)
$t->anzahl += count($titel->jahre);
}
if ($anzahltitel > 0)
$traeger = array_filter($traeger, fn($t) => $t->anzahl >= $anzahltitel);
if ($sortierung === 'name') {
usort($traeger, fn($a, $b) => strcasecmp($a->name, $b->name));
} else {
usort($traeger, fn($a, $b) => ($b->anzahl <=> $a->anzahl) ?: strcasecmp($a->name, $b->name));
}
if (isJson()) {
echo json_encode($traeger);
} else {
HTML_sportsmanager::halloffameNachSummeGruppiert(halloffameTitel(), $params->get('beschreibung'), $traeger);
}
}
function halloffameNachSummeDisziplinGruppiert(): void
{
$db = getDatabase();
global $params;
$jInput = Factory::getContainer()->get(SiteApplication::class)->input;
$anzahltitel = $jInput->get('anzahltitel', 0, 'INT');
$sortierung = $jInput->get('sortierung', 'anzahl', 'STRING');
if ($sortierung !== 'name')
$sortierung = 'anzahl';
$query = "SELECT t2.*, t1.halloffame, t1.spielform, t1.reihenfolge"
. "\n FROM #__sportsmanager_halloffame t1"
. "\n LEFT JOIN #__sportsmanager_mitglied_von_halloffame t2 ON t2.halloffame_id = t1.halloffame_id"
. "\n WHERE t2.jahr IS NOT NULL AND t2.platz = 1"
. kategorieFilter("AND t1.kategorie IN")
. halloffameIdFilter("AND t1.halloffame_id IN")
. halloffameJahrFilter("AND t2.jahr IN")
. "\n ORDER BY t1.reihenfolge, t2.jahr ASC;";
$rows = loadObjectList($db, $query);
$alleTeams = halloffameAlleTeams($db);
$alleSpielerInfo = halloffameAlleSpielerInfo($db);
$alleVereineInfo = halloffameAlleVereineInfo($db);
$disziplinen = [];
foreach ($rows as $row) {
if (!isset($disziplinen[$row->halloffame_id])) {
$disziplinen[$row->halloffame_id] = (object) [
'halloffame_id' => $row->halloffame_id,
'halloffame' => $row->halloffame,
'reihenfolge' => $row->reihenfolge,
'traeger' => [],
];
}
$disziplin = $disziplinen[$row->halloffame_id];
if (!empty($row->nicht_ausgespielt)) continue;
if ($row->spielform == 1) {
$name = trim($row->teamname);
if ($name === '') continue;
$key = 'team:' . $name;
if (!isset($disziplin->traeger[$key])) {
if (empty($row->verein_id)) {
$bild_type = 'mannschaften';
$bild_id = halloffameTeamIdFuerTeamname($alleTeams, $row->teamname);
} else {
$bild_type = 'vereine';
$bild_id = $row->verein_id;
}
$disziplin->traeger[$key] = (object) [
'name' => $name,
'bild_type' => $bild_type,
'bild_id' => $bild_id,
'aktiv' => $bild_type === 'vereine' ? halloffameVereinAktiv($alleVereineInfo, $bild_id) : true,
'geschlecht' => null,
'jahre' => [],
];
}
$disziplin->traeger[$key]->jahre[] = $row->jahr;
} else {
if (!empty($row->spieler1_id)) {
$key = 'spieler:' . $row->spieler1_id;
if (!isset($disziplin->traeger[$key])) {
$disziplin->traeger[$key] = (object) [
'name' => $row->spieler1,
'bild_type' => 'spieler',
'bild_id' => $row->spieler1_id,
'aktiv' => halloffameSpielerAktiv($alleSpielerInfo, $row->spieler1_id),
'geschlecht' => $alleSpielerInfo[$row->spieler1_id]->geschlecht ?? null,
'jahre' => [],
];
}
$disziplin->traeger[$key]->jahre[] = $row->jahr;
}
if ($row->spielform == 2 && !empty($row->spieler2_id)) {
$key = 'spieler:' . $row->spieler2_id;
if (!isset($disziplin->traeger[$key])) {
$disziplin->traeger[$key] = (object) [
'name' => $row->spieler2,
'bild_type' => 'spieler',
'bild_id' => $row->spieler2_id,
'aktiv' => halloffameSpielerAktiv($alleSpielerInfo, $row->spieler2_id),
'geschlecht' => $alleSpielerInfo[$row->spieler2_id]->geschlecht ?? null,
'jahre' => [],
];
}
$disziplin->traeger[$key]->jahre[] = $row->jahr;
}
}
}
foreach ($disziplinen as $disziplin) {
foreach ($disziplin->traeger as $t)
$t->anzahl = count($t->jahre);
if ($anzahltitel > 0)
$disziplin->traeger = array_filter($disziplin->traeger, fn($t) => $t->anzahl >= $anzahltitel);
if ($sortierung === 'name') {
usort($disziplin->traeger, fn($a, $b) => strcasecmp($a->name, $b->name));
} else {
usort($disziplin->traeger, fn($a, $b) => ($b->anzahl <=> $a->anzahl) ?: strcasecmp($a->name, $b->name));
}
}
usort($disziplinen, fn($a, $b) => $a->reihenfolge <=> $b->reihenfolge);
if (isJson()) {
echo json_encode($disziplinen);
} else {
HTML_sportsmanager::halloffameNachSummeDisziplinGruppiert(halloffameTitel(), $params->get('beschreibung'), $disziplinen);
}
}
function halloffameDetails($uebergabe_id = 0): void function halloffameDetails($uebergabe_id = 0): void
{ {
$db = getDatabase(); $db = getDatabase();
@@ -4492,9 +4876,21 @@ function halloffameDetails($uebergabe_id = 0): void
else else
$id = $uebergabe_id; $id = $uebergabe_id;
$platzierung = $jInput->get('platzierung', '', 'STRING');
if ($platzierung !== 'inspalten' && $platzierung !== 'inzeilen')
$platzierung = '';
$plaetzezeigen = $jInput->get('plaetzezeigen', 0, 'INT');
$sortierung = $jInput->get('sortierung', 'jahrabwaerts', 'STRING');
if ($sortierung !== 'jahraufwaerts')
$sortierung = 'jahrabwaerts';
$jahrSortierung = $sortierung === 'jahraufwaerts' ? 'ASC' : 'DESC';
$mitglieder = null; $mitglieder = null;
$query = "SELECT * FROM #__sportsmanager_halloffame WHERE halloffame_id = $id"; $query = "SELECT * FROM #__sportsmanager_halloffame WHERE halloffame_id = $id"
. halloffameIdFilter("AND halloffame_id IN");
$rows = loadObjectList($db, $query); $rows = loadObjectList($db, $query);
if (count($rows) < 1) { if (count($rows) < 1) {
abortWithError("Wrong id!"); abortWithError("Wrong id!");
@@ -4502,36 +4898,46 @@ function halloffameDetails($uebergabe_id = 0): void
$halloffame = $rows[0]; $halloffame = $rows[0];
$halloffame->platz2_zeigen = 0; $halloffame->platz2_zeigen = 0;
$halloffame->platz3_zeigen = 0; $halloffame->platz3_zeigen = 0;
$halloffame->teamspieler_zeigen = 0;
$halloffame->anzahl_m = 0;
$halloffame->anzahl_w = 0;
$query = "SELECT t2.*, t1.halloffame" $query = "SELECT t2.*, t1.halloffame"
. "\n FROM #__sportsmanager_halloffame t1" . "\n FROM #__sportsmanager_halloffame t1"
. "\n LEFT JOIN #__sportsmanager_mitglied_von_halloffame t2 ON t2.halloffame_id = t1.halloffame_id" . "\n LEFT JOIN #__sportsmanager_mitglied_von_halloffame t2 ON t2.halloffame_id = t1.halloffame_id"
. "\n WHERE t2.halloffame_id = $id" . "\n WHERE t2.halloffame_id = $id"
. "\n ORDER BY t2.jahr DESC, platz ASC;"; . halloffameJahrFilter("AND t2.jahr IN")
. "\n ORDER BY t2.jahr $jahrSortierung, platz ASC;";
$rows = loadObjectList($db, $query); $rows = loadObjectList($db, $query);
$alleSpielerInfo = halloffameAlleSpielerInfo($db);
$alleVereineInfo = halloffameAlleVereineInfo($db);
if (count($rows) > 0){ if (count($rows) > 0){
$mitglieder = []; $mitglieder = [];
if ($halloffame->spielform == 1){ if ($halloffame->spielform == 1){
$alleTeams = halloffameAlleTeams($db);
foreach ($rows as $row) { foreach ($rows as $row) {
$index_vereinid = "verein_id_" . $row->platz; $index_vereinid = "verein_id_" . $row->platz;
$index_teamid = "team_id_" . $row->platz; $index_teamid = "team_id_" . $row->platz;
$index_team = "teamname_" . $row->platz; $index_team = "teamname_" . $row->platz;
$index_teamspieler = "teamspieler_" . $row->platz;
if (!isset($mitglieder[$row->jahr])) { if (!isset($mitglieder[$row->jahr])) {
$mitglieder[$row->jahr] = new stdClass(); $mitglieder[$row->jahr] = new stdClass();
} }
$mitglieder[$row->jahr]->jahr = $row->jahr; $mitglieder[$row->jahr]->jahr = $row->jahr;
$mitglieder[$row->jahr]->$index_vereinid = $row->verein_id; $mitglieder[$row->jahr]->$index_vereinid = $row->verein_id;
$mitglieder[$row->jahr]->$index_team = $row->teamname; $mitglieder[$row->jahr]->$index_team = !empty($row->nicht_ausgespielt) ? Text::_('COM_SPORTSMANAGER_HALL_OF_FAME_NOT_HELD') : $row->teamname;
$mitglieder[$row->jahr]->$index_teamspieler = $row->teamspieler;
if ($row->platz == 2 && !empty($row->teamname)) if ($row->platz == 2 && !empty($row->teamname))
$halloffame->platz2_zeigen = 1; $halloffame->platz2_zeigen = 1;
if ($row->platz == 3 && !empty($row->teamname)) if ($row->platz == 3 && !empty($row->teamname))
$halloffame->platz3_zeigen = 1; $halloffame->platz3_zeigen = 1;
if (mb_strlen(trim((string) $row->teamspieler)) >= 3)
$halloffame->teamspieler_zeigen = 1;
//Suche team_id wenn keine verein_id vorhanden //Suche team_id wenn keine verein_id vorhanden
if (empty($row->verein_id)){ if (empty($row->verein_id)){
$query = "SELECT team_id FROM #__sportsmanager_team WHERE teamname LIKE '$row->teamname%' ORDER BY team_id DESC LIMIT 1;"; $mitglieder[$row->jahr]->$index_teamid = halloffameTeamIdFuerTeamname($alleTeams, $row->teamname);
$mitglieder[$row->jahr]->$index_teamid = loadResult($db, $query);
} else { } else {
$mitglieder[$row->jahr]->$index_teamid = ""; $mitglieder[$row->jahr]->$index_teamid = "";
} }
@@ -4549,21 +4955,31 @@ function halloffameDetails($uebergabe_id = 0): void
} }
$mitglieder[$row->jahr]->jahr = $row->jahr; $mitglieder[$row->jahr]->jahr = $row->jahr;
$mitglieder[$row->jahr]->$index_spieler1id = $row->spieler1_id; $mitglieder[$row->jahr]->$index_spieler1id = $row->spieler1_id;
$mitglieder[$row->jahr]->$index_spieler1 = $row->spieler1; $mitglieder[$row->jahr]->$index_spieler1 = !empty($row->nicht_ausgespielt) ? Text::_('COM_SPORTSMANAGER_HALL_OF_FAME_NOT_HELD') : $row->spieler1;
$mitglieder[$row->jahr]->$index_spieler2id = $row->spieler2_id; $mitglieder[$row->jahr]->$index_spieler2id = $row->spieler2_id;
$mitglieder[$row->jahr]->$index_spieler2 = $row->spieler2; $mitglieder[$row->jahr]->$index_spieler2 = $row->spieler2;
if ($row->platz == 2 && (!empty($row->spieler1) || !empty($row->spieler2))) if ($row->platz == 2 && (!empty($row->spieler1) || !empty($row->spieler2)))
$halloffame->platz2_zeigen = 1; $halloffame->platz2_zeigen = 1;
if ($row->platz == 3 && (!empty($row->spieler1) || !empty($row->spieler2))) if ($row->platz == 3 && (!empty($row->spieler1) || !empty($row->spieler2)))
$halloffame->platz3_zeigen = 1; $halloffame->platz3_zeigen = 1;
halloffameGeschlechtZaehlen($halloffame, $alleSpielerInfo, $row->spieler1_id, $halloffame->spielform == 2 ? $row->spieler2_id : null);
} }
} }
} }
$halloffame->geschaetztes_geschlecht = halloffameGeschaetztesGeschlecht($halloffame->anzahl_m, $halloffame->anzahl_w);
if ($plaetzezeigen > 0) {
if ($plaetzezeigen < 2)
$halloffame->platz2_zeigen = 0;
if ($plaetzezeigen < 3)
$halloffame->platz3_zeigen = 0;
}
if (isJson()) { if (isJson()) {
echo json_encode($mitglieder); echo json_encode($mitglieder);
} else { } else {
HTML_sportsmanager::halloffameDetails($params->get('titel'), $params->get('beschreibung'), $mitglieder, $halloffame); HTML_sportsmanager::halloffameDetails(halloffameTitel(), $params->get('beschreibung'), $mitglieder, $halloffame, $platzierung, $alleSpielerInfo, $alleVereineInfo);
} }
} }
@@ -1,899 +0,0 @@
<?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;
}
@@ -19,11 +19,17 @@ require_once JPATH_SITE . '/components/com_sportsmanager/database/init.php';
/** @noinspection PhpUnused */ /** @noinspection PhpUnused */
function mathParserVerteilung($rohpunkte, $platz, $teilnehmer, $multiplikator) { function mathParserVerteilung($rohpunkte, $platz, $teilnehmer, $multiplikator) {
if ($teilnehmer <= 1){
return 0;
}
return max(round($multiplikator * round(((($rohpunkte - 1) * (-log($platz / $teilnehmer) * (1 - ($platz / $teilnehmer)))) / (-log(1 / $teilnehmer) * (1 - (1 / $teilnehmer)))) + 1)), 1); return max(round($multiplikator * round(((($rohpunkte - 1) * (-log($platz / $teilnehmer) * (1 - ($platz / $teilnehmer)))) / (-log(1 / $teilnehmer) * (1 - (1 / $teilnehmer)))) + 1)), 1);
} }
/** @noinspection PhpUnused */ /** @noinspection PhpUnused */
function mathParserVerteilungR($rohpunkte, $platz, $teilnehmer, $multiplikator) { function mathParserVerteilungR($rohpunkte, $platz, $teilnehmer, $multiplikator) {
if ($teilnehmer <= 1){
return 0;
}
return max(round(((($multiplikator * $rohpunkte - 1) * (-log($platz / $teilnehmer) * (1 - ($platz / $teilnehmer)))) / (-log(1 / $teilnehmer) * (1 - (1 / $teilnehmer)))) + 1), 1); return max(round(((($multiplikator * $rohpunkte - 1) * (-log($platz / $teilnehmer) * (1 - ($platz / $teilnehmer)))) / (-log(1 / $teilnehmer) * (1 - (1 / $teilnehmer)))) + 1), 1);
} }
@@ -226,6 +232,217 @@ function kategorieFilter($prefix, $suffix = ""): string
return " $prefix (" . implode(", ", $filter) . ") $suffix"; return " $prefix (" . implode(", ", $filter) . ") $suffix";
} }
function idListeParameterFilter(string $parameterName, string $prefix, string $suffix = ""): string
{
$jInput = Factory::getContainer()->get(SiteApplication::class)->input;
$werte = $jInput->get($parameterName, '', 'STRING');
$result = [];
foreach (explode(",", $werte) as $item) {
$item = trim($item);
if ($item === '') continue;
$num = intval($item);
if ($num > 0) {
$result[$num] = true; // Duplikate vermeiden
}
}
if (empty($result)) {
return "";
}
$filter = array_keys($result);
sort($filter, SORT_NUMERIC);
return " $prefix (" . implode(", ", $filter) . ") $suffix";
}
function halloffameIdFilter($prefix): string
{
// Keine Bereichssuche bei der Halloffame-Id, nur einzelne Werte
return idListeParameterFilter('halloffame_id', $prefix);
}
function halloffameJahrFilter($prefix): string
{
$minJahr = 1900;
$maxJahr = (int) date('Y') + 5;
$jInput = Factory::getContainer()->get(SiteApplication::class)->input;
$werte = $jInput->get('jahr', '', 'STRING');
$result = [];
foreach (explode(",", $werte) as $item) {
$item = trim($item);
if ($item === '') continue;
// Prüfen, ob es ein Bereich ist
if (strpos($item, '-') !== false) {
$rangeParts = explode('-', $item);
// genau 2 Teile für einen gültigen Bereich
if (count($rangeParts) !== 2) continue;
$start = intval(trim($rangeParts[0]));
$end = intval(trim($rangeParts[1]));
if ($start <= 0 || $end <= 0) continue;
// Werte vertauschen, falls der 2. Wert kleiner als der 1. ist
if ($end < $start) {
[$start, $end] = [$end, $start];
}
// Bereich auf 1900 .. aktuelles Jahr+5 begrenzen
$start = max($start, $minJahr);
$end = min($end, $maxJahr);
if ($start > $end) continue;
for ($i = $start; $i <= $end; $i++) {
$result[$i] = true; // Duplikate vermeiden
}
} else {
$num = intval($item);
if ($num <= 0) continue;
// Einzelwert auf 1900 .. aktuelles Jahr+5 begrenzen
$num = max($minJahr, min($maxJahr, $num));
$result[$num] = true;
}
}
if (empty($result)) {
return "";
}
$filter = array_keys($result);
sort($filter, SORT_NUMERIC);
return " $prefix (" . implode(", ", $filter) . ")";
}
function halloffameAlleTeams($db): array
{
return loadObjectList($db, "SELECT team_id, teamname FROM #__sportsmanager_team ORDER BY team_id DESC");
}
function halloffameTeamIdFuerTeamname(array $alleTeams, ?string $teamname)
{
if (empty($teamname))
return null;
foreach ($alleTeams as $team) {
if (str_starts_with($team->teamname, $teamname))
return $team->team_id;
}
return null;
}
function halloffameAlleSpielerInfo($db): array
{
// "Aktiv" = hat einen aktuellen Verein; ohne aktuellen Verein liefert spieler_details ohnehin keine Daten
$rows = loadObjectList($db, "SELECT spieler_id, geschlecht, aktueller_verein_id FROM #__sportsmanager_spieler");
$result = [];
foreach ($rows as $row) {
$result[$row->spieler_id] = (object) [
'geschlecht' => $row->geschlecht,
'aktiv' => !empty($row->aktueller_verein_id),
];
}
return $result;
}
function halloffameSpielerAktiv(array $alleSpielerInfo, $spieler_id): bool
{
return empty($spieler_id) || !isset($alleSpielerInfo[$spieler_id]) || $alleSpielerInfo[$spieler_id]->aktiv;
}
function halloffameSpielerBildId(array $alleSpielerInfo, $spieler_id)
{
return halloffameSpielerAktiv($alleSpielerInfo, $spieler_id) ? $spieler_id : '';
}
function halloffameSpielerBildAlternativ(array $alleSpielerInfo, $spieler_id, string $geschaetztesGeschlecht = ''): string
{
if (!empty($spieler_id) && isset($alleSpielerInfo[$spieler_id]))
return $alleSpielerInfo[$spieler_id]->geschlecht == 'M' ? 'm' : 'w';
if ($geschaetztesGeschlecht === 'M')
return 'm';
if ($geschaetztesGeschlecht === 'W')
return 'w';
return 'n';
}
function halloffameGeschlechtZaehlen(object $ziel, array $alleSpielerInfo, $spieler1_id, $spieler2_id): void
{
foreach ([$spieler1_id, $spieler2_id] as $id) {
if (empty($id) || !isset($alleSpielerInfo[$id]))
continue;
if ($alleSpielerInfo[$id]->geschlecht == 'M')
$ziel->anzahl_m++;
else
$ziel->anzahl_w++;
}
}
function halloffameGeschaetztesGeschlecht($anzahl_m, $anzahl_w): string
{
if ($anzahl_m > $anzahl_w)
return 'M';
if ($anzahl_w > $anzahl_m)
return 'W';
return '';
}
function halloffameAlleVereineInfo($db): array
{
$rows = loadObjectList($db, "SELECT verein_id, ausgetreten FROM #__sportsmanager_verein");
$result = [];
foreach ($rows as $row) {
$result[$row->verein_id] = (object) [
'aktiv' => empty($row->ausgetreten),
];
}
return $result;
}
function halloffameVereinAktiv(array $alleVereineInfo, $verein_id): bool
{
return empty($verein_id) || !isset($alleVereineInfo[$verein_id]) || $alleVereineInfo[$verein_id]->aktiv;
}
function halloffameNameLink(string $bild_type, $id, array $alleSpielerInfo, array $alleVereineInfo, string $nameHtml): string
{
if (empty($id))
return $nameHtml;
if ($bild_type === 'spieler' && !halloffameSpielerAktiv($alleSpielerInfo, $id))
return $nameHtml;
if ($bild_type === 'vereine' && !halloffameVereinAktiv($alleVereineInfo, $id))
return $nameHtml;
$tasks = ['spieler' => 'spieler_details', 'vereine' => 'verein_details', 'mannschaften' => 'team_details'];
if (!isset($tasks[$bild_type]))
return $nameHtml;
return '<a href="' . SportsManagerURL('&task=' . $tasks[$bild_type] . '&id=' . $id) . '">' . $nameHtml . '</a>';
}
function halloffameTitel(): string
{
global $params;
$titel = trim((string) $params->get('titel', ''));
if ($titel !== '')
return $titel;
$menu = Factory::getContainer()->get(SiteApplication::class)->getMenu()->getActive();
if ($menu !== null && trim((string) $menu->title) !== '')
return $menu->title;
return '';
}
function turnierFilter($prefix): string function turnierFilter($prefix): string
{ {
$user_id = isExternalDatabase() ? 0 : Factory::getContainer()->get(SiteApplication::class)->getIdentity()->id; $user_id = isExternalDatabase() ? 0 : Factory::getContainer()->get(SiteApplication::class)->getIdentity()->id;
@@ -45,6 +45,11 @@
default="" default=""
label="COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_CATEGORIES" label="COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_CATEGORIES"
description="COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_CATEGORIES_DESC" /> description="COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_CATEGORIES_DESC" />
<field name="zusatzparameter"
type="text"
default=""
label="COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_EXTRA_PARAMS"
description="COM_SPORTSMANAGER_LAYOUT_GENERAL_CONTENT_OPTION_EXTRA_PARAMS_DESC" />
</fieldset> </fieldset>
</fields> </fields>
</metadata> </metadata>
File diff suppressed because it is too large Load Diff
@@ -982,21 +982,6 @@ 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>
@@ -1270,18 +1255,6 @@ 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
} }
?> ?>
@@ -4463,6 +4436,17 @@ class HTML_sportsmanager_admin
value="<?php if ($row != null) echo htmlentities_utf8($row->ruhetage); ?>"/> value="<?php if ($row != null) echo htmlentities_utf8($row->ruhetage); ?>"/>
</td> </td>
</tr> </tr>
<tr>
<td nowrap style="width: 20%; text-align: right; vertical-align: top">
<label
for="zusatzinfo"><?php echo Text::_('COM_SPORTSMANAGER_ADDITIONAL_INFO'); ?>
:</label>
</td>
<td nowrap>
<textarea name="zusatzinfo" id="zusatzinfo" cols="60"
rows="8"><?php if ($row != null) echo htmlentities_utf8($row->zusatzinfo); ?></textarea>
</td>
</tr>
<tr> <tr>
<td nowrap style="width: 20%; text-align: right; vertical-align: top"> <td nowrap style="width: 20%; text-align: right; vertical-align: top">
<label <label
@@ -8194,6 +8178,7 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
<?php <?php
if ($rows != null) { if ($rows != null) {
$einspaltigesDoppel = ($halloffame->spielform == 2 && !$halloffame->platz2_zeigen && !$halloffame->platz3_zeigen);
?> ?>
<div class="uk-overflow-auto"> <div class="uk-overflow-auto">
<table style='border-collapse: collapse;' <table style='border-collapse: collapse;'
@@ -8209,7 +8194,7 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
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;
?> ?>
<th style="text-align:center;" colspan="2" nowrap> <th style="text-align:center;" colspan="<?php echo $einspaltigesDoppel ? 4 : 2; ?>" nowrap>
<strong><?php echo Text::_('COM_SPORTSMANAGER_PLACE') . " " . $i; ?></strong> <strong><?php echo Text::_('COM_SPORTSMANAGER_PLACE') . " " . $i; ?></strong>
</th> </th>
<?php } ?> <?php } ?>
@@ -8225,7 +8210,42 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
$rowclass = "sectiontableentry" . ($k + 1) . $params->get('pageclass_sfx'); $rowclass = "sectiontableentry" . ($k + 1) . $params->get('pageclass_sfx');
$k = ($k + 1) % 2; $k = ($k + 1) % 2;
if ($halloffame->spielform == 2) { if ($halloffame->spielform == 2 && $einspaltigesDoppel) {
?>
<tr class="<?php echo $rowclass; ?>">
<td nowrap style="text-align:center;">
<a href="<?php echo SportsManagerURL('&task=admin_halloffame_mitglied_edit&halloffame_id=' . $halloffame->halloffame_id . '&jahr=' . $row->jahr); ?>">
<?php echo $row->jahr; ?>
</a>
</td>
<td nowrap style="text-align:center; width:70px;">
<?php echo bildHTML("spieler", $row->spieler1_id_1, 45, 60, 0, 0, 'border="0"'); ?>
</td>
<td nowrap style="text-align:left; width:300px;">
<?php echo htmlentities_utf8($row->spieler1_1); ?>
</td>
<td nowrap style="text-align:center; width:70px;">
<?php echo bildHTML("spieler", $row->spieler2_id_1, 45, 60, 0, 0, 'border="0"'); ?>
</td>
<td nowrap style="text-align:left; width:300px;">
<?php echo htmlentities_utf8($row->spieler2_1); ?>
</td>
<td nowrap>
<small>
<a href="<?php echo SportsManagerURL('&task=admin_halloffame_mitglied_remove&halloffame_id=' . $halloffame->halloffame_id . '&jahr=' . $row->jahr); ?>"
onclick="return confirm('<?php echo Text::_('COM_SPORTSMANAGER_REALLY_REMOVE_HALL_OF_FAME_YEAR'); ?>');"
title="<?php echo Text::_('COM_SPORTSMANAGER_REMOVE'); ?>">
X
</a>
</small>
</td>
</tr>
<?php
} else if ($halloffame->spielform == 2) {
?> ?>
<!-- Erste Zeile --> <!-- Erste Zeile -->
@@ -8337,11 +8357,58 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
static function adminEditHalloffameMitglied($row,$halloffame,$vereine,$spieler,$jahre): void static function adminEditHalloffameMitglied($row,$halloffame,$vereine,$spieler,$jahre): void
{ {
global $params; global $params;
// Options nur einmal aufbauen (statt bis zu 6x je Formular) - bei grossen Spieler-/Vereinslisten deutlich schneller
$vereineOptionsHtml = '';
$spielerOptionsHtml = '';
if ($halloffame->spielform == 1) {
$vereineAktiv = array();
$vereineInaktiv = array();
foreach ($vereine as $v) {
if (empty($v->ausgetreten))
$vereineAktiv[] = $v;
else
$vereineInaktiv[] = $v;
}
$ausgetretenSuffix = " (" . Text::_('COM_SPORTSMANAGER_BEATEN') . ")";
$vereineOptionsHtml = '<option value="0"></option>';
$vereineOptionsHtml .= '<optgroup label="' . htmlentities_utf8(Text::_('COM_SPORTSMANAGER_ACTIVE_CLUBS')) . '">';
foreach ($vereineAktiv as $v)
$vereineOptionsHtml .= '<option value="' . $v->verein_id . '">' . htmlentities_utf8($v->verein) . '</option>';
$vereineOptionsHtml .= '</optgroup>';
$vereineOptionsHtml .= '<optgroup label="' . htmlentities_utf8(Text::_('COM_SPORTSMANAGER_INACTIVE_CLUBS')) . '">';
foreach ($vereineInaktiv as $v)
$vereineOptionsHtml .= '<option value="' . $v->verein_id . '">' . htmlentities_utf8($v->verein . $ausgetretenSuffix) . '</option>';
$vereineOptionsHtml .= '</optgroup>';
} else {
$spielerOptionsHtml = '<option value="0"></option>';
foreach ($spieler as $s)
$spielerOptionsHtml .= '<option value="' . $s->spieler_id . '">' . htmlentities_utf8($s->spieler) . '</option>';
}
?> ?>
<div <div
class="componentheading<?php echo $params->get('pageclass_sfx'); ?>"><?php echo htmlentities_utf8($halloffame->halloffame); ?> class="componentheading<?php echo $params->get('pageclass_sfx'); ?>"><?php echo htmlentities_utf8($halloffame->halloffame); ?>
: <?php echo($row != null ? Text::_('COM_SPORTSMANAGER_CHANGING') : Text::_('COM_SPORTSMANAGER_ADD')); ?></div> : <?php echo($row != null ? Text::_('COM_SPORTSMANAGER_CHANGING') : Text::_('COM_SPORTSMANAGER_ADD')); ?></div>
<template id="halloffame-vereine-options"><?php echo $vereineOptionsHtml; ?></template>
<template id="halloffame-spieler-options"><?php echo $spielerOptionsHtml; ?></template>
<script type="text/javascript">
function halloffameNichtAusgespieltUmschalten(deaktiviert) {
document.querySelectorAll('.halloffame-platz-zeile input, .halloffame-platz-zeile select, .halloffame-platz-zeile textarea').forEach(function (feld) {
feld.disabled = deaktiviert;
});
}
function halloffameOptionenFuellen(selectId, templateId, wert) {
var select = document.getElementById(selectId);
var template = document.getElementById(templateId);
select.innerHTML = template.innerHTML;
if (wert) select.value = wert;
}
</script>
<form action="<?php echo SportsManagerURL(); ?>" method="post" name="adminForm" id="adminForm" <form action="<?php echo SportsManagerURL(); ?>" method="post" name="adminForm" id="adminForm"
enctype="multipart/form-data"> enctype="multipart/form-data">
<div class="uk-overflow-auto"> <div class="uk-overflow-auto">
@@ -8360,6 +8427,13 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
} }
?> ?>
</select> </select>
&nbsp;&nbsp;
<label for="nicht_ausgespielt">
<input type="checkbox" name="nicht_ausgespielt" id="nicht_ausgespielt" value="1"
onclick="halloffameNichtAusgespieltUmschalten(this.checked);"
<?php echo ($row != null && !empty($row->nicht_ausgespielt)) ? " checked" : ""; ?> />
<?php echo Text::_('COM_SPORTSMANAGER_HALL_OF_FAME_NOT_HELD'); ?>
</label>
</td> </td>
</tr> </tr>
<?php <?php
@@ -8367,26 +8441,35 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
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;
$index_teamspieler = "teamspieler_" . $p;
?> ?>
<tr> <tr class="halloffame-platz-zeile">
<td nowrap style="width: 20%; text-align: right"> <td nowrap style="width: 20%; text-align: right">
<label for="verein1_<?php echo $p; ?>"><?php echo Text::_('COM_SPORTSMANAGER_PLACE') . " " . $p; ?> <label for="verein1_<?php echo $p; ?>"><?php echo Text::_('COM_SPORTSMANAGER_PLACE') . " " . $p; ?>
:</label> :</label>
</td> </td>
<td nowrap> <td nowrap>
<select class="uk-select uk-form-width-large" name="verein_id_<?php echo $p; ?>" <select class="uk-select uk-form-width-large" name="verein_id_<?php echo $p; ?>"
id="verein_id_<?php echo $p; ?>" size="1" style="width: 370px;"> id="verein_id_<?php echo $p; ?>" size="1" style="width: 370px;"></select>
<?php <script type="text/javascript">
echo "<option value=\"0\"></option>"; halloffameOptionenFuellen('verein_id_<?php echo $p; ?>', 'halloffame-vereine-options', '<?php echo $row != null ? (int) $row->$index_vereinid : 0; ?>');
foreach ($vereine as $v) </script>
echo "<option value=\"" . $v->verein_id . "\"" . ($row != null ? ($row->$index_vereinid == $v->verein_id ? " selected" : "") : "") . ">" . htmlentities_utf8($v->verein) . "</option>";
?>
</select>
<input class="inputbox uk-select" type="text" style="width: 370px; height: 35px;" <input class="inputbox uk-select" type="text" style="width: 370px; height: 35px;"
name="teamname_<?php echo $p; ?>" id="verein_<?php echo $p; ?>" size="50" maxlength="64" name="teamname_<?php echo $p; ?>" id="verein_<?php echo $p; ?>" size="50" maxlength="64"
value="<?php echo $row != null ? htmlentities_utf8($row->$index_team) : ''; ?>"/> value="<?php echo $row != null ? htmlentities_utf8($row->$index_team) : ''; ?>"/>
</td> </td>
</tr> </tr>
<tr class="halloffame-platz-zeile">
<td nowrap style="width: 20%; text-align: right">
<label for="teamspieler_<?php echo $p; ?>"><?php echo Text::_('COM_SPORTSMANAGER_HALL_OF_FAME_TEAM_PLAYERS'); ?>
:</label>
</td>
<td nowrap>
<textarea class="uk-textarea" style="width: 740px; height: 70px;"
name="teamspieler_<?php echo $p; ?>" id="teamspieler_<?php echo $p; ?>"
><?php echo $row != null ? htmlentities_utf8($row->$index_teamspieler) : ''; ?></textarea>
</td>
</tr>
<?php <?php
} }
@@ -8396,33 +8479,27 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
$index_spieler2id = "spieler2_id_" . $p; $index_spieler2id = "spieler2_id_" . $p;
$index_spieler2 = "spieler2_" . $p; $index_spieler2 = "spieler2_" . $p;
?> ?>
<tr> <tr class="halloffame-platz-zeile">
<td nowrap style="width: 20%; text-align: right"> <td nowrap style="width: 20%; text-align: right">
<label for="player1_<?php echo $p; ?>"><?php echo Text::_('COM_SPORTSMANAGER_PLACE') . " " . $p; ?> <label for="player1_<?php echo $p; ?>"><?php echo Text::_('COM_SPORTSMANAGER_PLACE') . " " . $p; ?>
:</label> :</label>
</td> </td>
<td nowrap> <td nowrap>
<select class="uk-select uk-form-width-large" name="spieler1_id_<?php echo $p; ?>" <select class="uk-select uk-form-width-large" name="spieler1_id_<?php echo $p; ?>"
id="player1_id_<?php echo $p; ?>" size="1" style="width: 370px;"> id="player1_id_<?php echo $p; ?>" size="1" style="width: 370px;"></select>
<?php <script type="text/javascript">
echo "<option value=\"0\"></option>"; halloffameOptionenFuellen('player1_id_<?php echo $p; ?>', 'halloffame-spieler-options', '<?php echo $row != null ? (int) $row->$index_spieler1id : 0; ?>');
foreach ($spieler as $s) </script>
echo "<option value=\"" . $s->spieler_id . "\"" . ($row != null ? ($row->$index_spieler1id == $s->spieler_id ? " selected" : "") : "") . ">" . htmlentities_utf8($s->spieler) . "</option>";
?>
</select>
<input class="inputbox uk-select" type="text" style="width: 370px; height: 35px;" <input class="inputbox uk-select" type="text" style="width: 370px; height: 35px;"
name="spieler1_<?php echo $p; ?>" id="player1_<?php echo $p; ?>" size="50" maxlength="64" name="spieler1_<?php echo $p; ?>" id="player1_<?php echo $p; ?>" size="50" maxlength="64"
value="<?php echo $row != null ? htmlentities_utf8($row->$index_spieler1) : ''; ?>"/> value="<?php echo $row != null ? htmlentities_utf8($row->$index_spieler1) : ''; ?>"/>
<?PHP if ($halloffame->spielform == 2){ ?> <?PHP if ($halloffame->spielform == 2){ ?>
<br> <br>
<select class="uk-select uk-form-width-large" name="spieler2_id_<?php echo $p; ?>" <select class="uk-select uk-form-width-large" name="spieler2_id_<?php echo $p; ?>"
id="player2_id_<?php echo $p; ?>" size="1" style="width: 370px;"> id="player2_id_<?php echo $p; ?>" size="1" style="width: 370px;"></select>
<?php <script type="text/javascript">
echo "<option value=\"0\"></option>"; halloffameOptionenFuellen('player2_id_<?php echo $p; ?>', 'halloffame-spieler-options', '<?php echo $row != null ? (int) $row->$index_spieler2id : 0; ?>');
foreach ($spieler as $s) </script>
echo "<option value=\"" . $s->spieler_id . "\"" . ($row != null ? ($row->$index_spieler2id == $s->spieler_id ? " selected" : "") : "") . ">" . htmlentities_utf8($s->spieler) . "</option>";
?>
</select>
<input class="inputbox uk-select" type="text" style="width: 370px; height: 35px;" <input class="inputbox uk-select" type="text" style="width: 370px; height: 35px;"
name="spieler2_<?php echo $p; ?>" id="player2_<?php echo $p; ?>" size="50" maxlength="64" name="spieler2_<?php echo $p; ?>" id="player2_<?php echo $p; ?>" size="50" maxlength="64"
value="<?php echo $row != null ? htmlentities_utf8($row->$index_spieler2) : ''; ?>"/> value="<?php echo $row != null ? htmlentities_utf8($row->$index_spieler2) : ''; ?>"/>
@@ -8445,6 +8522,9 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
<input type="hidden" name="halloffame_id" value="<?php echo $halloffame->halloffame_id; ?>"/> <input type="hidden" name="halloffame_id" value="<?php echo $halloffame->halloffame_id; ?>"/>
<input type="hidden" name="spielform" value="<?php echo $halloffame->spielform; ?>"/> <input type="hidden" name="spielform" value="<?php echo $halloffame->spielform; ?>"/>
</form> </form>
<script type="text/javascript">
halloffameNichtAusgespieltUmschalten(document.getElementById('nicht_ausgespielt').checked);
</script>
<?php <?php
} }
@@ -11868,8 +11948,9 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
$spiel_einzel = $spieltypen[$i - 1][0] != "D"; $spiel_einzel = $spieltypen[$i - 1][0] != "D";
if (($spiel_einzel && $spielpunkte_wertung_einzel == 2) || (!$spiel_einzel && $spielpunkte_wertung_doppel == 2)) if (($spiel_einzel && $spielpunkte_wertung_einzel == 2) || (!$spiel_einzel && $spielpunkte_wertung_doppel == 2))
continue; continue;
echo " punkte_heim = document.adminForm.spiel_" . $i . "_heim_punkte.value != '' ? document.adminForm.spiel_" . $i . "_heim_punkte.value : 0;\n" // Number(): .value ist eine Zeichenkette, "10" > "9" wäre sonst unwahr
. " punkte_gast = document.adminForm.spiel_" . $i . "_gast_punkte.value != '' ? document.adminForm.spiel_" . $i . "_gast_punkte.value : 0;\n" echo " punkte_heim = document.adminForm.spiel_" . $i . "_heim_punkte.value != '' ? Number(document.adminForm.spiel_" . $i . "_heim_punkte.value) : 0;\n"
. " punkte_gast = document.adminForm.spiel_" . $i . "_gast_punkte.value != '' ? Number(document.adminForm.spiel_" . $i . "_gast_punkte.value) : 0;\n"
. " if (punkte_heim > punkte_gast)\n" . " if (punkte_heim > punkte_gast)\n"
. " spielpunkte_heim += " . ($spiel_einzel ? "spielpunkte_sieg_einzel" : "spielpunkte_sieg_doppel") . ";\n" . " spielpunkte_heim += " . ($spiel_einzel ? "spielpunkte_sieg_einzel" : "spielpunkte_sieg_doppel") . ";\n"
. " else if (punkte_heim < punkte_gast)\n" . " else if (punkte_heim < punkte_gast)\n"
@@ -12869,6 +12950,12 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
{ {
global $params; global $params;
?> ?>
<script type="text/javascript">
function anzeige_aktualisieren() {
document.getElementById("export_row").style.display = document.adminForm.exportformat.value !== "jsondatei" ? '' : 'none';
}
window.addEventListener("pageshow", anzeige_aktualisieren);
</script>
<form action="<?php echo SportsManagerURL(); ?>" method="post" name="adminForm" id="adminForm"> <form action="<?php echo SportsManagerURL(); ?>" method="post" name="adminForm" id="adminForm">
<div class="uk-overflow-auto"> <div class="uk-overflow-auto">
<table style="width: 500px"> <table style="width: 500px">
@@ -12888,7 +12975,7 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
</select> </select>
</td> </td>
</tr> </tr>
<tr> <tr id="export_row">
<td nowrap style="text-align: right"> <td nowrap style="text-align: right">
<label for="export">Exportieren</label> <label for="export">Exportieren</label>
</td> </td>
@@ -12906,8 +12993,9 @@ static function adminVerbandsorganMitglieder($rows,$verbandsorgan): void
<label for="exportformat">Exportformat</label> <label for="exportformat">Exportformat</label>
</td> </td>
<td nowrap> <td nowrap>
<select class="uk-select uk-form-width-large" style='width: 320px;' name="exportformat" id="exportformat" size="1"> <select class="uk-select uk-form-width-large" style='width: 320px;' name="exportformat" id="exportformat" size="1" onchange="anzeige_aktualisieren();">
<option value='csvdatei'>CSV-Datei</option> <option value='csvdatei'>CSV-Datei</option>
<option value='jsondatei'>JSON-Datei</option>
</select> </select>
</td> </td>
</tr> </tr>
@@ -411,51 +411,6 @@ 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()">
@@ -478,11 +433,6 @@ 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">
@@ -1888,99 +1838,6 @@ 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
} }
@@ -395,6 +395,8 @@ COM_SPORTSMANAGER_BEATEN="Ausgetreten"
COM_SPORTSMANAGER_HIDE="Verstecken" COM_SPORTSMANAGER_HIDE="Verstecken"
COM_SPORTSMANAGER_PASSIVE="Passiv" COM_SPORTSMANAGER_PASSIVE="Passiv"
COM_SPORTSMANAGER_BEATEN_CLUB="Verein ausgetreten" COM_SPORTSMANAGER_BEATEN_CLUB="Verein ausgetreten"
COM_SPORTSMANAGER_ACTIVE_CLUBS="Aktive Vereine"
COM_SPORTSMANAGER_INACTIVE_CLUBS="Inaktive Vereine"
COM_SPORTSMANAGER_SINGLE_SEED="Elo-Startwert Einzel" COM_SPORTSMANAGER_SINGLE_SEED="Elo-Startwert Einzel"
COM_SPORTSMANAGER_DOUBLE_SEED="Elo-Startwert Doppel" COM_SPORTSMANAGER_DOUBLE_SEED="Elo-Startwert Doppel"
COM_SPORTSMANAGER_PLAYER_EXPORT="Spieler: Exportieren" COM_SPORTSMANAGER_PLAYER_EXPORT="Spieler: Exportieren"
@@ -1087,6 +1089,7 @@ COM_SPORTSMANAGER_MATCH_SWAPPING_HELP="Bei Heimrechttausch gleichen Termin eintr
COM_SPORTSMANAGER_NOT_VALID_TIME="Ung&uuml;ltige Uhrzeit" COM_SPORTSMANAGER_NOT_VALID_TIME="Ung&uuml;ltige Uhrzeit"
COM_SPORTSMANAGER_REALLY_MATCH_RESCHEDULING="Willst Du diesen Spielverlegung wirklich entfernen?" COM_SPORTSMANAGER_REALLY_MATCH_RESCHEDULING="Willst Du diesen Spielverlegung wirklich entfernen?"
COM_SPORTSMANAGER_REST_DAYS="Ruhetage" COM_SPORTSMANAGER_REST_DAYS="Ruhetage"
COM_SPORTSMANAGER_ADDITIONAL_INFO="Zusatzinfo"
COM_SPORTSMANAGER_TRAINING_DAYS="Trainingstage" COM_SPORTSMANAGER_TRAINING_DAYS="Trainingstage"
COM_SPORTSMANAGER_NOT_ACTUALIZED_DATA="Nicht aktualisierte Daten" COM_SPORTSMANAGER_NOT_ACTUALIZED_DATA="Nicht aktualisierte Daten"
COM_SPORTSMANAGER_ASSOCIATION_BODIES="Verbandsorgane" COM_SPORTSMANAGER_ASSOCIATION_BODIES="Verbandsorgane"
@@ -1107,6 +1110,10 @@ COM_SPORTSMANAGER_MATCH_TYPE="Spielform"
COM_SPORTSMANAGER_REALLY_REMOVE_HALL_OF_FAME_YEAR="Willst Du wirklich dieses Hall of Fame Jahr l&ouml;schen?" COM_SPORTSMANAGER_REALLY_REMOVE_HALL_OF_FAME_YEAR="Willst Du wirklich dieses Hall of Fame Jahr l&ouml;schen?"
COM_SPORTSMANAGER_YEARS="Jahre" COM_SPORTSMANAGER_YEARS="Jahre"
COM_SPORTSMANAGER_ADD_HALL_OF_FAME_YEAR="Hall of Fame Jahr hinzuf&uuml;gen" COM_SPORTSMANAGER_ADD_HALL_OF_FAME_YEAR="Hall of Fame Jahr hinzuf&uuml;gen"
COM_SPORTSMANAGER_HALL_OF_FAME_TITLE_COUNT="Anzahl"
COM_SPORTSMANAGER_HALL_OF_FAME_TITLES="Titel"
COM_SPORTSMANAGER_HALL_OF_FAME_TEAM_PLAYERS="Mannschaftsspieler"
COM_SPORTSMANAGER_HALL_OF_FAME_NOT_HELD="Nicht ausgespielt"
COM_SPORTSMANAGER_NO_ENTRY="kein Eintrag" COM_SPORTSMANAGER_NO_ENTRY="kein Eintrag"
COM_SPORTSMANAGER_REALLY_SWAP_MATCH="Willst Du wirklich das Heimrecht tauschen?" COM_SPORTSMANAGER_REALLY_SWAP_MATCH="Willst Du wirklich das Heimrecht tauschen?"
COM_SPORTSMANAGER_SWAP_MATCH="Heimrechttausch" COM_SPORTSMANAGER_SWAP_MATCH="Heimrechttausch"
@@ -1115,4 +1122,6 @@ COM_SPORTSMANAGER_MATCH_REPORT_DELETED="Spielbericht gel&ouml;scht"
COM_SPORTSMANAGER_MATCH_REPORT_WAS_DELETED="Der Spielbericht wurde erfolgreich gel&ouml;scht!" COM_SPORTSMANAGER_MATCH_REPORT_WAS_DELETED="Der Spielbericht wurde erfolgreich gel&ouml;scht!"
COM_SPORTSMANAGER_MATCH_REPORT_CORRECTED="Spielberichtskorrektur" COM_SPORTSMANAGER_MATCH_REPORT_CORRECTED="Spielberichtskorrektur"
COM_SPORTSMANAGER_MIN_MATCHES="Mindestzahl Spiele" COM_SPORTSMANAGER_MIN_MATCHES="Mindestzahl Spiele"
COM_SPORTSMANAGER_SELECT_ALL="Alle" COM_SPORTSMANAGER_SELECT_ALL="Alle"
COM_SPORTSMANAGER_MAIL_SEND_ERROR="Die Benachrichtigungs-E-Mail zum Ergebnis konnte nicht versendet werden."
COM_SPORTSMANAGER_TERMIN_MAIL_SEND_ERROR="Mindestens eine Benachrichtigungs-E-Mail zum Termin konnte nicht versendet werden."
@@ -395,6 +395,8 @@ COM_SPORTSMANAGER_BEATEN="Excreted"
COM_SPORTSMANAGER_HIDE="Hide" COM_SPORTSMANAGER_HIDE="Hide"
COM_SPORTSMANAGER_PASSIVE="Passive" COM_SPORTSMANAGER_PASSIVE="Passive"
COM_SPORTSMANAGER_BEATEN_CLUB="Club excreted" COM_SPORTSMANAGER_BEATEN_CLUB="Club excreted"
COM_SPORTSMANAGER_ACTIVE_CLUBS="Active clubs"
COM_SPORTSMANAGER_INACTIVE_CLUBS="Inactive clubs"
COM_SPORTSMANAGER_SINGLE_SEED="Elo starting value singles" COM_SPORTSMANAGER_SINGLE_SEED="Elo starting value singles"
COM_SPORTSMANAGER_DOUBLE_SEED="Elo starting value doubles" COM_SPORTSMANAGER_DOUBLE_SEED="Elo starting value doubles"
COM_SPORTSMANAGER_PLAYER_EXPORT="Players: Export" COM_SPORTSMANAGER_PLAYER_EXPORT="Players: Export"
@@ -1087,6 +1089,7 @@ COM_SPORTSMANAGER_MATCH_SWAPPING_HELP="If home advantage is swapped, enter the s
COM_SPORTSMANAGER_NOT_VALID_TIME="Not valid time" COM_SPORTSMANAGER_NOT_VALID_TIME="Not valid time"
COM_SPORTSMANAGER_REALLY_MATCH_RESCHEDULING="Do you really want to remove this match rescheduling?" COM_SPORTSMANAGER_REALLY_MATCH_RESCHEDULING="Do you really want to remove this match rescheduling?"
COM_SPORTSMANAGER_REST_DAYS="Rest days" COM_SPORTSMANAGER_REST_DAYS="Rest days"
COM_SPORTSMANAGER_ADDITIONAL_INFO="Additional info"
COM_SPORTSMANAGER_TRAINING_DAYS="Training days" COM_SPORTSMANAGER_TRAINING_DAYS="Training days"
COM_SPORTSMANAGER_NOT_ACTUALIZED_DATA="Data not updated" COM_SPORTSMANAGER_NOT_ACTUALIZED_DATA="Data not updated"
COM_SPORTSMANAGER_ASSOCIATION_BODIES="Association bodies" COM_SPORTSMANAGER_ASSOCIATION_BODIES="Association bodies"
@@ -1107,6 +1110,10 @@ COM_SPORTSMANAGER_MATCH_TYPE="Game Type"
COM_SPORTSMANAGER_REALLY_REMOVE_HALL_OF_FAME_YEAR="Are you sure you want to delete this Hall of Fame year?" COM_SPORTSMANAGER_REALLY_REMOVE_HALL_OF_FAME_YEAR="Are you sure you want to delete this Hall of Fame year?"
COM_SPORTSMANAGER_YEARS="Years" COM_SPORTSMANAGER_YEARS="Years"
COM_SPORTSMANAGER_ADD_HALL_OF_FAME_YEAR="Add Hall of Fame Year" COM_SPORTSMANAGER_ADD_HALL_OF_FAME_YEAR="Add Hall of Fame Year"
COM_SPORTSMANAGER_HALL_OF_FAME_TITLE_COUNT="Number"
COM_SPORTSMANAGER_HALL_OF_FAME_TITLES="Titles"
COM_SPORTSMANAGER_HALL_OF_FAME_TEAM_PLAYERS="Team Players"
COM_SPORTSMANAGER_HALL_OF_FAME_NOT_HELD="Not held"
COM_SPORTSMANAGER_NO_ENTRY="no entry" COM_SPORTSMANAGER_NO_ENTRY="no entry"
COM_SPORTSMANAGER_REALLY_SWAP_MATCH="Do you really want to swap the home advantage?" COM_SPORTSMANAGER_REALLY_SWAP_MATCH="Do you really want to swap the home advantage?"
COM_SPORTSMANAGER_SWAP_MATCH="Swap home advantage" COM_SPORTSMANAGER_SWAP_MATCH="Swap home advantage"
@@ -1115,4 +1122,6 @@ COM_SPORTSMANAGER_MATCH_REPORT_DELETED="Match report deleted"
COM_SPORTSMANAGER_MATCH_REPORT_WAS_DELETED="The match report has been successfully deleted!" COM_SPORTSMANAGER_MATCH_REPORT_WAS_DELETED="The match report has been successfully deleted!"
COM_SPORTSMANAGER_MATCH_REPORT_CORRECTED="Match report corrected" COM_SPORTSMANAGER_MATCH_REPORT_CORRECTED="Match report corrected"
COM_SPORTSMANAGER_MIN_MATCHES="Min count matches" COM_SPORTSMANAGER_MIN_MATCHES="Min count matches"
COM_SPORTSMANAGER_SELECT_ALL="All" COM_SPORTSMANAGER_SELECT_ALL="All"
COM_SPORTSMANAGER_MAIL_SEND_ERROR="The notification e-mail for the result could not be sent."
COM_SPORTSMANAGER_TERMIN_MAIL_SEND_ERROR="At least one notification e-mail for the appointment could not be sent."
+9 -21
View File
@@ -94,6 +94,12 @@ return new class () implements InstallerScriptInterface
return false; return false;
} }
if (!bildKopierenAngepasst(JPATH_ROOT.DIRECTORY_SEPARATOR.'components'.DIRECTORY_SEPARATOR.'com_sportsmanager'.DIRECTORY_SEPARATOR.'images'.DIRECTORY_SEPARATOR.'spieler-n.png', JPATH_ROOT.DIRECTORY_SEPARATOR.'images'.DIRECTORY_SEPARATOR.'sportsmanager'.DIRECTORY_SEPARATOR.'spieler'.DIRECTORY_SEPARATOR.'n.png', 180, 240, 1)) {
Log::add('Image /components/com_sportsmanager/images/spieler-n.png could not be copied to /images/sportsmanager/spieler/n.png', Log::ERROR);
echo '<p>Fehler: Bild spieler-n.png konnte nicht nach /images/sportsmanager/spieler/n.png verschoben werden</p>';
return false;
}
} }
$adapter->getParent()->setRedirectURL('index.php?option=com_sportsmanager'); $adapter->getParent()->setRedirectURL('index.php?option=com_sportsmanager');
return true; return true;
@@ -674,6 +680,7 @@ return new class () implements InstallerScriptInterface
. "\n `telefon` varchar(64) DEFAULT NULL," . "\n `telefon` varchar(64) DEFAULT NULL,"
. "\n `email` varchar(64) DEFAULT NULL," . "\n `email` varchar(64) DEFAULT NULL,"
. "\n `ruhetage` varchar(64) DEFAULT NULL," . "\n `ruhetage` varchar(64) DEFAULT NULL,"
. "\n `zusatzinfo` text DEFAULT NULL,"
. "\n `beschreibung` varchar(500) DEFAULT NULL," . "\n `beschreibung` varchar(500) DEFAULT NULL,"
. "\n `status` tinyint(1) NOT NULL DEFAULT '0'," . "\n `status` tinyint(1) NOT NULL DEFAULT '0',"
. "\n PRIMARY KEY (`spielort_id`)" . "\n PRIMARY KEY (`spielort_id`)"
@@ -1251,9 +1258,11 @@ return new class () implements InstallerScriptInterface
. "\n `mitglied_halloffame_id` int(11) NOT NULL AUTO_INCREMENT," . "\n `mitglied_halloffame_id` int(11) NOT NULL AUTO_INCREMENT,"
. "\n `halloffame_id` int(11) NOT NULL," . "\n `halloffame_id` int(11) NOT NULL,"
. "\n `jahr` int(4) DEFAULT NULL," . "\n `jahr` int(4) DEFAULT NULL,"
. "\n `nicht_ausgespielt` tinyint(1) NOT NULL DEFAULT '0',"
. "\n `platz` int(11) DEFAULT NULL," . "\n `platz` int(11) DEFAULT NULL,"
. "\n `verein_id` int(11) DEFAULT NULL," . "\n `verein_id` int(11) DEFAULT NULL,"
. "\n `teamname` varchar(64) DEFAULT NULL," . "\n `teamname` varchar(64) DEFAULT NULL,"
. "\n `teamspieler` text DEFAULT NULL,"
. "\n `spieler1_id` int(11) DEFAULT NULL," . "\n `spieler1_id` int(11) DEFAULT NULL,"
. "\n `spieler1` varchar(64) DEFAULT NULL," . "\n `spieler1` varchar(64) DEFAULT NULL,"
. "\n `spieler2_id` int(11) DEFAULT NULL," . "\n `spieler2_id` int(11) DEFAULT NULL,"
@@ -1278,27 +1287,6 @@ 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 = "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 = "INSERT IGNORE #__sportsmanager_einstellungen SET name = 'basis_spielernr', wert = '';"; $query = "INSERT IGNORE #__sportsmanager_einstellungen SET name = 'basis_spielernr', wert = '';";
$db->setQuery( $query ); $db->setQuery( $query );
if (!$db->execute()) { die($db->stderr(true)); } if (!$db->execute()) { die($db->stderr(true)); }
-23
View File
@@ -1,23 +0,0 @@
# 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.