Sportfest-Ausgabe der Stadionzeitung zum Heimspiel gegen ATS Wartenfels

Neue Live-Seiten, geschaltet vom chat-gepflegten Feld `veranstaltung` (Slug aus
data/veranstaltungen.json) — Programm und Turnierpläne kommen damit aus derselben
Quelle wie die Website, nicht als Kopie ins Heft:

- Programm-Seite (programm[], rendert zugleich auf /veranstaltungen/sportfest-2026)
- vier Turnierplan-Seiten (turniere[]) mit leerer Ergebnisspalte und leerer
  Abschlusstabelle zum Eintragen am Platz
- Gegnerseite je Mannschaft mit Heimspiel am Fest statt nur der tragenden:
  stadionzeitung_gegner_fixtures() ist die einzige Stelle, die das entscheidet,
  Seitenplanung und Snapshot lesen beide dort. `gegner` ist deshalb eine Map je
  Team-Key; die Vorschau überspringt jedes Spiel mit eigener Gegnerseite
- Danke-Seite mit Partner-Logo-Wand aus data/partners.json und QR auf
  /partner-werden, dazu das Anzeigen-Feld `platzierung` für Motive mit festem
  Platz auf einer Live-Seite (Emons über der Logo-Wand). Vorher klebte der Dank
  als leere Fläche unten an einem Anzeigen-Bild
- Cover einer Sonderausgabe: das Fest als Titelgeschichte statt einer der drei
  Begegnungen, dazu das Hauptplakat schräg gelegt an Stelle des Spielerfotos

Ausgabe 2026-09-04-ats-wartenfels als ENTWURF angelegt, 48 Seiten (12 Bögen),
Tabellen auf gerader Seite, längster Anzeigenblock zwei. Veröffentlicht wird auf
dem Server, dort liegt der frische Matchcenter-Stand.

Nebenbei behoben: bin/anzeige-add.php räumte beim Anlegen alle Motive weg, deren
Slug mit demselben Text beginnt (`emons` löschte `emons-partnerdank` und
`emons-stuebinger`). Muster jetzt auf die Breiten-Endung eingeschränkt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KubsajzuV6N6QsSd3xKNjC
This commit is contained in:
2026-09-03 10:57:52 +02:00
parent b4d2cde149
commit f2f193b223
40 changed files with 2273 additions and 77 deletions

View File

@@ -568,6 +568,27 @@ function veranstaltungen_load(): array
return $items;
}
/**
* Eine Veranstaltung per Slug, UNGEFILTERT (auch Entwürfe) — für die
* Detailseite (app/pages/veranstaltung.php) und die Stadionzeitungs-Live-Seiten
* der Sportfest-Ausgabe (Programm, Turnierpläne, Gegnerseiten je Heimspiel).
* Ungefiltert aus demselben Grund wie die Detailseite: ein Entwurf muss sich
* gegenlesen lassen, und eine Entwurfs-Ausgabe der Stadionzeitung darf auf
* einen Entwurfs-Termin zeigen. null, wenn es den Slug nicht gibt.
*/
function veranstaltung_find(string $slug): ?array
{
if ($slug === '') {
return null;
}
foreach (json_load('veranstaltungen')['veranstaltungen'] ?? [] as $entry) {
if (($entry['slug'] ?? '') === $slug) {
return $entry;
}
}
return null;
}
/**
* Steht ein Termin zum Bezugsdatum noch an? Maßgeblich ist das ENDE, nicht der
* Beginn: ein mehrtägiges Fest (Sportfest, Freitag bis Sonntag) gehört am
@@ -955,6 +976,86 @@ function stadionzeitung_is_published(array $issue): bool
return ($issue['status'] ?? 'veroeffentlicht') !== 'entwurf';
}
/**
* Coverzeile „Im Heft" einer Ausgabe — die EINE Quelle für das Cover
* (stadionzeitung-cover.php) und die Archivkarte (stadionzeitung-cards.php),
* damit beide nie Verschiedenes sagen. Eine Sonderausgabe (Sportfest) setzt
* `cover.teaser` chat-gepflegt; sonst trägt die Coverzeile des Interviews
* (`interview.teaser`). Leer, wenn es weder das eine noch das andere gibt.
*/
function stadionzeitung_teaser(array $issue): string
{
$teaser = trim((string) ($issue['cover']['teaser'] ?? ''));
if ($teaser !== '') {
return $teaser;
}
return trim((string) ($issue['interview']['teaser'] ?? ''));
}
/**
* Welche Mannschaften bekommen in einer Ausgabe eine Gegnerseite — die EINE
* Logik dafür, benutzt von bin/stadionzeitung-add.php (Seitenplanung) UND
* stadionzeitung_build_snapshot() (Daten), damit nie eine Seite ohne Daten
* oder Daten ohne Seite entstehen.
*
* Immer: die tragende Mannschaft (Gegner = `opponent`, Tag = `date`).
* Zusätzlich, wenn die Ausgabe eine `veranstaltung` (Slug) mit Zeitraum trägt
* (Sportfest-Ausgabe): jede weitere Mannschaft aus matchcenter.json mit einem
* HEIMspiel in diesem Zeitraum — drei Heimspiele am Fest, drei Gegnerseiten.
* Der Gegner ist dann der andere Verein der Begegnung. Reihenfolge wie in
* matchcenter.json (= config 'matchcenter.teams').
*
* Rückgabe: [team_key => ['opponent' => string, 'date' => 'YYYY-MM-DD']]
*/
function stadionzeitung_gegner_fixtures(array $issue): array
{
$date = (string) ($issue['date'] ?? '');
$issueTeam = (string) ($issue['team_key'] ?? '');
$issueOpponent = trim((string) ($issue['opponent'] ?? ''));
$von = '';
$bis = '';
$veranstaltung = veranstaltung_find((string) ($issue['veranstaltung'] ?? ''));
if ($veranstaltung !== null) {
$von = (string) ($veranstaltung['date_start'] ?? '');
$bis = (string) ($veranstaltung['date_end'] ?? $von);
}
$fixtures = [];
foreach (json_load('matchcenter')['teams'] ?? [] as $team) {
$key = (string) ($team['key'] ?? '');
if ($key === '') {
continue;
}
if ($key === $issueTeam) {
if ($issueOpponent !== '') {
$fixtures[$key] = ['opponent' => $issueOpponent, 'date' => $date];
}
continue;
}
if ($von === '') {
continue;
}
foreach ($team['next_matches'] ?? [] as $match) {
if (empty($match['is_home'])) {
continue;
}
$matchDate = substr((string) ($match['kickoff'] ?? ''), 0, 10);
if ($matchDate < $von || $matchDate > $bis) {
continue;
}
$opponent = trim((string) ($match['away'] ?? ''));
if ($opponent === '' || $opponent === 'SPIELFREI') {
continue;
}
$fixtures[$key] = ['opponent' => $opponent, 'date' => $matchDate];
break;
}
}
return $fixtures;
}
/**
* Die neueste VERÖFFENTLICHTE Ausgabe (oder null, wenn es keine gibt) — für die
* Kurz-URL /stadionzeitung/aktuell, auf die gedruckte QR-Codes zeigen. Sortiert
@@ -988,12 +1089,15 @@ function stadionzeitung_build_snapshot(array $issue): array
{
$date = (string) ($issue['date'] ?? '');
$issueTeam = (string) ($issue['team_key'] ?? '');
$issueOpponent = trim((string) ($issue['opponent'] ?? ''));
$tabellen = [];
$ergebnisse = [];
$vorschau = [];
$gegner = null;
// Gegnerseiten als Map je Team-Key (wie `tabellen`): welche Mannschaften
// eine bekommen, entscheidet stadionzeitung_gegner_fixtures() — dieselbe
// Funktion, mit der bin/stadionzeitung-add.php die Seiten einplant.
$gegnerFixtures = stadionzeitung_gegner_fixtures($issue);
$gegner = [];
foreach (json_load('matchcenter')['teams'] ?? [] as $team) {
$key = (string) ($team['key'] ?? '');
if ($key === '') {
@@ -1006,11 +1110,14 @@ function stadionzeitung_build_snapshot(array $issue): array
'table' => $team['table'] ?? [],
];
// Gegnerseite der tragenden Mannschaft: ausschließlich aus demselben
// Matchcenter-Datensatz wie die Tabelle ableiten. So gibt es keinen
// zweiten BFV-Abruf und der Stand wird beim Veröffentlichen zusammen
// mit den übrigen Live-Daten eingefroren.
if ($key === $issueTeam && $issueOpponent !== '') {
// Gegnerseite(n): ausschließlich aus demselben Matchcenter-Datensatz
// wie die Tabelle ableiten. So gibt es keinen zweiten BFV-Abruf und der
// Stand wird beim Veröffentlichen zusammen mit den übrigen Live-Daten
// eingefroren. Normal nur die tragende Mannschaft; in der Sportfest-
// Ausgabe jede Mannschaft mit Heimspiel am Fest (s. gegner_fixtures).
if (isset($gegnerFixtures[$key])) {
$gegnerName = $gegnerFixtures[$key]['opponent'];
$gegnerDate = $gegnerFixtures[$key]['date'];
$table = is_array($team['table'] ?? null) ? $team['table'] : [];
$ownRow = null;
$opponentRow = null;
@@ -1018,7 +1125,7 @@ function stadionzeitung_build_snapshot(array $issue): array
if (!empty($row['is_own_club'])) {
$ownRow = $row;
}
if (strcasecmp(trim((string) ($row['club'] ?? '')), $issueOpponent) === 0) {
if (strcasecmp(trim((string) ($row['club'] ?? '')), $gegnerName) === 0) {
$opponentRow = $row;
}
}
@@ -1027,15 +1134,15 @@ function stadionzeitung_build_snapshot(array $issue): array
foreach ($team['next_matches'] ?? [] as $match) {
$matchDate = substr((string) ($match['kickoff'] ?? ''), 0, 10);
$clubs = [(string) ($match['home'] ?? ''), (string) ($match['away'] ?? '')];
if ($matchDate === $date && in_array($issueOpponent, $clubs, true)) {
if ($matchDate === $gegnerDate && in_array($gegnerName, $clubs, true)) {
$fixture = $match;
break;
}
}
$gegner = [
$gegner[$key] = [
'team_name' => (string) ($team['name'] ?? ''),
'opponent' => $issueOpponent,
'opponent' => $gegnerName,
'league' => (string) ($team['league'] ?? ''),
'season' => (string) ($team['season'] ?? ''),
'fixture' => $fixture,
@@ -1064,7 +1171,11 @@ function stadionzeitung_build_snapshot(array $issue): array
// Vorschau: das nächste Spiel dieser Mannschaft NACH dem Spieltag der
// Ausgabe. Das Heft-Spiel selbst fliegt raus (das Cover zeigt es
// schon); ein Spiel einer ANDEREN Mannschaft am selben Tag bleibt drin.
// schon), und ebenso jedes Spiel, das eine eigene Gegnerseite hat
// (Sportfest-Ausgabe: Zweite und Frauen stünden sonst doppelt im Heft).
// Ein Spiel einer ANDEREN Mannschaft am selben Tag ohne Gegnerseite
// bleibt drin.
$gegnerDate = $gegnerFixtures[$key]['date'] ?? null;
foreach ($team['next_matches'] ?? [] as $match) {
// Spielfreie Runden führt der BFV als Pseudo-Begegnung gegen
// "SPIELFREI" — kein echtes Spiel, im Heft überspringen.
@@ -1078,6 +1189,9 @@ function stadionzeitung_build_snapshot(array $issue): array
if ($matchDate === $date && $key === $issueTeam) {
continue;
}
if ($gegnerDate !== null && $matchDate === $gegnerDate) {
continue;
}
$vorschau[] = [
'team_key' => $key,
'team_name' => (string) ($team['name'] ?? ''),