feat: Add home/away game labels and participant count to friendly match schedule
Some checks failed
Deploy tt-tagebuch / deploy (push) Has been cancelled

This commit is contained in:
Torsten Schulz (local)
2026-05-28 17:40:23 +02:00
parent 211420444e
commit 7942e6108a
3 changed files with 109 additions and 32 deletions

View File

@@ -1484,6 +1484,16 @@
"homeTeam": "Heimmannschaft",
"guestTeam": "Gastmannschaft",
"result": "Ergebnis",
"homeGame": "Heimspiel",
"participants": "Teilnehmer",
"swapButton": "Tauschen",
"away": "Auswärts",
"homeLabel": "Heimspiel",
"awayLabel": "Auswärtsspiel",
"swapSuccess": "Heim/Auswärts getauscht",
"swapFailed": "Heim/Auswärts konnte nicht getauscht werden.",
"plannedShort": "geplant",
"readyShort": "bereit",
"ageClass": "Altersklasse",
"code": "Code",
"homePin": "Heim-PIN",

View File

@@ -103,12 +103,14 @@
<th v-if="!friendlyOnly">{{ $t('schedule.code') }}</th>
<th>{{ friendlyOnly ? 'Aktionen' : $t('schedule.homePin') }}</th>
<th v-if="!friendlyOnly">{{ $t('schedule.guestPin') }}</th>
<th v-if="friendlyOnly"></th>
<th v-if="friendlyOnly">{{ $t('schedule.homeGame') || 'Heimspiel' }}</th>
<th v-if="friendlyOnly">{{ $t('schedule.participants') || 'Teilnehmer' }}</th>
<th v-if="friendlyOnly">&nbsp;</th>
</tr>
</thead>
<tbody>
<tr v-for="match in matches" :key="match.id"
@click="openPlayerSelectionDialog(match)"
@click="friendlyOnly ? openFriendlyMatchDialog(match) : openPlayerSelectionDialog(match)"
:class="getRowClass(match.date)"
style="cursor: pointer;">
<td class="location-info-cell">
@@ -152,6 +154,25 @@
:title="$t('schedule.copyCode') + ': ' + match.code">{{ match.code }}</span>
<span v-else-if="!match.isFriendly" class="no-data">-</span>
</td>
<td v-if="friendlyOnly" class="participants-cell">
<button type="button" class="btn-secondary" @click.stop="openPlayerSelectionDialog(match)">
{{ (Array.isArray(match.playersPlanned) ? match.playersPlanned.length : (match.playersPlanned ? JSON.parse(match.playersPlanned).length : 0)) }} {{ $t('schedule.plannedShort') || 'geplant' }}
·
{{ (Array.isArray(match.playersReady) ? match.playersReady.length : (match.playersReady ? JSON.parse(match.playersReady).length : 0)) }} {{ $t('schedule.readyShort') || 'bereit' }}
</button>
</td>
<td v-if="friendlyOnly" class="home-away-cell">
<div class="home-away-status">
<span class="status-label">
{{ isOurClubPlayingHome(match) ? $t('schedule.homeLabel') || $t('schedule.homeGame') : $t('schedule.awayLabel') || $t('schedule.away') }}
</span>
<button type="button" class="btn-small" @click.stop="toggleHomeAway(match)">
{{ $t('schedule.swapButton') }}
</button>
</div>
</td>
<td class="pin-cell">
<div v-if="match.isFriendly" class="friendly-actions-cell">
<button type="button" class="btn-secondary" @click.stop="openFriendlyResultDialog(match)">Ergebnis</button>
@@ -803,6 +824,20 @@ export default {
return teamName.startsWith(this.currentClubName);
},
isOurClubPlayingHome(match) {
const club = (this.currentClubName || '').toString().trim().toLowerCase();
if (!club || !match) return false;
const homeName = (match.homeTeam?.name || '').toString().toLowerCase();
const guestName = (match.guestTeam?.name || '').toString().toLowerCase();
const homeHas = homeName.includes(club);
const guestHas = guestName.includes(club);
if (homeHas && guestHas) return true; // both teams from our club -> treat as home
if (homeHas) return true;
if (guestHas) return false;
// Fallback: if neither includes club name, prefer original startsWith heuristic
return homeName.startsWith(this.currentClubName || '') || false;
},
// Dialog Helper Methods
async showInfo(title, message, details = '', type = 'info') {
this.infoDialog = {
@@ -1426,6 +1461,40 @@ export default {
await this.showInfo('Fehler', getSafeErrorMessage(error, 'Freundschaftsspiel konnte nicht gespeichert werden.'), '', 'error');
}
},
async toggleHomeAway(match) {
if (!match || !match.id) return;
const originalHome = match.homeTeam ? { ...match.homeTeam } : { name: '' };
const originalGuest = match.guestTeam ? { ...match.guestTeam } : { name: '' };
// Optimistic UI update: swap locally
try {
if (match.homeTeam && match.guestTeam) {
const tmp = match.homeTeam.name;
match.homeTeam.name = match.guestTeam.name;
match.guestTeam.name = tmp;
} else {
match.homeTeam = { name: originalGuest.name };
match.guestTeam = { name: originalHome.name };
}
// Trigger reactivity: replace in matches array
const idx = this.matches.findIndex(m => m.id === match.id);
if (idx !== -1) this.matches.splice(idx, 1, { ...match });
const payload = {
homeTeamName: match.homeTeam?.name || '',
guestTeamName: match.guestTeam?.name || ''
};
await apiClient.put(`/friendly-matches/${this.currentClub}/${match.id}`, payload);
} catch (error) {
console.error('toggleHomeAway error:', error);
// Revert optimistic change
const idx = this.matches.findIndex(m => m.id === match.id);
if (idx !== -1) {
const reverted = { ...match, homeTeam: originalHome, guestTeam: originalGuest };
this.matches.splice(idx, 1, reverted);
}
await this.showInfo(this.$t('messages.error'), getSafeErrorMessage(error, this.$t('schedule.swapFailed') || 'Heim/Auswärts konnte nicht getauscht werden.'), '', 'error');
}
},
async deleteFriendlyMatch() {
if (!this.friendlyMatchDialog.editingId) return;
const confirmed = await this.showConfirm('Freundschaftsspiel löschen', 'Soll dieses Freundschaftsspiel gelöscht werden?', '', 'warning');

View File

@@ -7586,6 +7586,20 @@ private fun DiaryPlanEditableCard(
modifier = Modifier.width(DiaryPlanColStart),
)
Row(modifier = Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) {
if (hasMainVisual) {
DiaryPlanQuickIconAction(
imageVector = Icons.Filled.Visibility,
contentDescription = tr("diary.showImage", "Bild/Zeichnung anzeigen"),
enabled = !planMutating,
onClick = {
if (drawingRaw != null) {
onViewDrawing(drawingRaw, title)
} else {
mainImageUrl?.let(onOpenImage)
}
},
)
}
Text(
title,
fontWeight = FontWeight.SemiBold,
@@ -7602,22 +7616,6 @@ private fun DiaryPlanEditableCard(
maxLines = 1,
)
}
// Always show a quick icon action for viewing/creating a drawing.
DiaryPlanQuickIconAction(
imageVector = Icons.Filled.Visibility,
contentDescription = tr("diary.showImage", "Bild/Zeichnung anzeigen"),
enabled = !planMutating,
onClick = {
if (drawingRaw != null) {
onViewDrawing(drawingRaw, title)
} else if (mainImageUrl != null) {
onOpenImage(mainImageUrl)
} else {
// no visual present yet -> open drawing editor to create one
onOpenDrawing()
}
},
)
}
Text(
groupLine ?: "",
@@ -7702,6 +7700,20 @@ private fun DiaryPlanEditableCard(
modifier = Modifier.fillMaxWidth().padding(start = DiaryPlanColStart, top = 0.dp, bottom = 0.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (hasNestedVisual) {
DiaryPlanQuickIconAction(
imageVector = Icons.Filled.Visibility,
contentDescription = tr("diary.showImage", "Bild/Zeichnung anzeigen"),
enabled = !planMutating,
onClick = {
if (nestedDrawingRaw != null) {
onViewNestedDrawing(nestedDrawingRaw, line)
} else {
nestedImageUrl?.let(onOpenImage)
}
},
)
}
Text(
"· $line",
style = MaterialTheme.typography.caption,
@@ -7720,20 +7732,6 @@ private fun DiaryPlanEditableCard(
}
val nid = ga.id
if (nid != null) {
if (hasNestedVisual) {
DiaryPlanQuickIconAction(
imageVector = Icons.Filled.Visibility,
contentDescription = tr("diary.showImage", "Bild/Zeichnung anzeigen"),
enabled = !planMutating,
onClick = {
if (nestedDrawingRaw != null) {
onViewNestedDrawing(nestedDrawingRaw, line)
} else {
nestedImageUrl?.let(onOpenImage)
}
},
)
}
DiaryPlanQuickIconAction(
imageVector = Icons.Filled.Edit,
contentDescription = tr("common.edit", "Bearbeiten"),