feat(matches): add manual match creation functionality and related fields
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 59s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 59s
This commit is contained in:
@@ -22,6 +22,19 @@ export const uploadCSV = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const createManualMatch = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId } = req.params;
|
||||
const result = await MatchService.createManualMatch(userToken, clubId, req.body);
|
||||
emitScheduleMatchUpdated(result.clubId, result.id, result.match);
|
||||
return res.status(201).json({ message: 'Spiel erfolgreich angelegt', data: result.match });
|
||||
} catch (error) {
|
||||
console.error('Error creating manual match:', error);
|
||||
return res.status(error.statusCode || 500).json({ error: error.message || 'Spiel konnte nicht angelegt werden' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getLeaguesForCurrentSeason = async (req, res) => {
|
||||
try {
|
||||
devLog(req.headers, req.params);
|
||||
|
||||
3
backend/migrations/20260819_add_manual_match_fields.sql
Normal file
3
backend/migrations/20260819_add_manual_match_fields.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE `match`
|
||||
ADD COLUMN `fixture_type` VARCHAR(32) NOT NULL DEFAULT 'league',
|
||||
ADD COLUMN `notes` TEXT NULL;
|
||||
@@ -127,6 +127,17 @@ const Match = sequelize.define('Match', {
|
||||
comment: 'Array of member IDs who actually played',
|
||||
field: 'players_played'
|
||||
},
|
||||
fixtureType: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'league',
|
||||
field: 'fixture_type'
|
||||
},
|
||||
notes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
field: 'notes'
|
||||
},
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'match',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import express from 'express';
|
||||
import { uploadCSV, getLeaguesForCurrentSeason, getMatchesForLeagues, getMatchesForLeague, getLeagueTable, fetchLeagueTableFromMyTischtennis, updateMatchPlayers, getPlayerMatchStats, getMatchPlayers } from '../controllers/matchController.js';
|
||||
import { uploadCSV, createManualMatch, getLeaguesForCurrentSeason, getMatchesForLeagues, getMatchesForLeague, getLeagueTable, fetchLeagueTableFromMyTischtennis, updateMatchPlayers, getPlayerMatchStats, getMatchPlayers } from '../controllers/matchController.js';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import { authorize } from '../middleware/authorizationMiddleware.js';
|
||||
import multer from 'multer';
|
||||
@@ -9,6 +9,7 @@ const router = express.Router();
|
||||
const upload = multer({ dest: 'uploads/' });
|
||||
|
||||
router.post('/import', authenticate, authorize('schedule', 'write'), upload.single('file'), uploadCSV);
|
||||
router.post('/:clubId', authenticate, authorize('schedule', 'write'), createManualMatch);
|
||||
router.get('/leagues/current/:clubId', authenticate, authorize('schedule', 'read'), getLeaguesForCurrentSeason);
|
||||
router.get('/leagues/:clubId/matches/:leagueId', authenticate, authorize('schedule', 'read'), getMatchesForLeague);
|
||||
router.get('/leagues/:clubId/matches', authenticate, authorize('schedule', 'read'), getMatchesForLeagues);
|
||||
|
||||
@@ -17,6 +17,72 @@ import HttpError from '../exceptions/HttpError.js';
|
||||
import { devLog } from '../utils/logger.js';
|
||||
class MatchService {
|
||||
|
||||
async enrichMatch(match) {
|
||||
const enriched = {
|
||||
id: match.id, date: match.date, time: match.time,
|
||||
homeTeamId: match.homeTeamId, guestTeamId: match.guestTeamId,
|
||||
locationId: match.locationId, leagueId: match.leagueId,
|
||||
code: match.code, homePin: match.homePin, guestPin: match.guestPin,
|
||||
homeMatchPoints: match.homeMatchPoints || 0, guestMatchPoints: match.guestMatchPoints || 0,
|
||||
isCompleted: match.isCompleted || false, pdfUrl: match.pdfUrl,
|
||||
playersReady: match.playersReady || [], playersPlanned: match.playersPlanned || [], playersPlayed: match.playersPlayed || [],
|
||||
fixtureType: match.fixtureType || 'league', notes: match.notes || null,
|
||||
homeTeam: { name: 'Unbekannt' }, guestTeam: { name: 'Unbekannt' },
|
||||
location: { name: 'Unbekannt', address: '', city: '', zip: '' }, leagueDetails: { name: 'Unbekannt' }
|
||||
};
|
||||
const [homeTeam, guestTeam, location, league] = await Promise.all([
|
||||
match.homeTeamId ? Team.findByPk(match.homeTeamId, { attributes: ['name'] }) : null,
|
||||
match.guestTeamId ? Team.findByPk(match.guestTeamId, { attributes: ['name'] }) : null,
|
||||
match.locationId ? Location.findByPk(match.locationId, { attributes: ['name', 'address', 'city', 'zip'] }) : null,
|
||||
match.leagueId ? League.findByPk(match.leagueId, { attributes: ['name'] }) : null
|
||||
]);
|
||||
if (homeTeam) enriched.homeTeam = homeTeam;
|
||||
if (guestTeam) enriched.guestTeam = guestTeam;
|
||||
if (location) enriched.location = location;
|
||||
if (league) enriched.leagueDetails = league;
|
||||
return enriched;
|
||||
}
|
||||
|
||||
async createManualMatch(userToken, clubId, payload = {}) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const parsedClubId = Number(clubId);
|
||||
const clubTeamId = Number(payload.clubTeamId);
|
||||
const date = String(payload.date || '').trim();
|
||||
const time = String(payload.time || '').trim();
|
||||
const opponentName = String(payload.opponentName || '').trim();
|
||||
const homeAway = payload.homeAway === 'away' ? 'away' : 'home';
|
||||
if (!Number.isInteger(clubTeamId) || !/^\d{4}-\d{2}-\d{2}$/.test(date) || !/^\d{2}:\d{2}$/.test(time) || !opponentName) {
|
||||
throw new HttpError('Mannschaft, Gegner, Datum und Uhrzeit sind erforderlich', 400);
|
||||
}
|
||||
const clubTeam = await ClubTeam.findOne({ where: { id: clubTeamId, clubId: parsedClubId } });
|
||||
if (!clubTeam?.leagueId || !clubTeam?.seasonId) throw new HttpError('Die ausgewählte Mannschaft ist keiner Liga der Saison zugeordnet', 400);
|
||||
const league = await League.findOne({ where: { id: clubTeam.leagueId, clubId: parsedClubId, seasonId: clubTeam.seasonId } });
|
||||
if (!league) throw new HttpError('Ungültige Liga für die ausgewählte Mannschaft', 400);
|
||||
const [ownTeam] = await Team.findOrCreate({
|
||||
where: { name: clubTeam.name, clubId: parsedClubId, leagueId: league.id, seasonId: clubTeam.seasonId },
|
||||
defaults: { name: clubTeam.name, clubId: parsedClubId, leagueId: league.id, seasonId: clubTeam.seasonId }
|
||||
});
|
||||
const [opponentTeam] = await Team.findOrCreate({
|
||||
where: { name: opponentName, clubId: parsedClubId, leagueId: league.id, seasonId: clubTeam.seasonId },
|
||||
defaults: { name: opponentName, clubId: parsedClubId, leagueId: league.id, seasonId: clubTeam.seasonId }
|
||||
});
|
||||
if (ownTeam.id === opponentTeam.id) throw new HttpError('Gegner darf nicht mit der eigenen Mannschaft identisch sein', 400);
|
||||
let locationId = null;
|
||||
const locationName = String(payload.locationName || '').trim();
|
||||
if (locationName) {
|
||||
const locationData = { name: locationName, address: String(payload.locationAddress || '').trim() || null, city: String(payload.locationCity || '').trim() || '', zip: String(payload.locationZip || '').trim() || '' };
|
||||
const [location] = await Location.findOrCreate({ where: locationData, defaults: locationData });
|
||||
locationId = location.id;
|
||||
}
|
||||
const match = await Match.create({
|
||||
date: new Date(`${date}T${time}:00`), time, clubId: parsedClubId, leagueId: league.id, locationId,
|
||||
homeTeamId: homeAway === 'home' ? ownTeam.id : opponentTeam.id,
|
||||
guestTeamId: homeAway === 'home' ? opponentTeam.id : ownTeam.id,
|
||||
fixtureType: 'cup', notes: String(payload.notes || '').trim() || null
|
||||
});
|
||||
return { id: match.id, clubId: match.clubId, match: await this.enrichMatch(match) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Format team name with age class suffix
|
||||
* @param {string} teamName - Base team name (e.g. "Harheimer TC")
|
||||
@@ -362,6 +428,20 @@ class MatchService {
|
||||
);
|
||||
ownTeamIdSet = new Set(ownTeams.map((t) => t.id));
|
||||
}
|
||||
const configuredClubTeams = await ClubTeam.findAll({
|
||||
where: { clubId, seasonId: season.id },
|
||||
attributes: ['name', 'leagueId']
|
||||
});
|
||||
const configuredTeamNames = new Set(configuredClubTeams.map((team) => this.normalizeTeamNameForMatch(team.name)));
|
||||
if (configuredTeamNames.size && leagueIdList.length) {
|
||||
const configuredTeams = await Team.findAll({
|
||||
where: { leagueId: { [Op.in]: leagueIdList } },
|
||||
attributes: ['id', 'name']
|
||||
});
|
||||
configuredTeams
|
||||
.filter((team) => configuredTeamNames.has(this.normalizeTeamNameForMatch(team.name)))
|
||||
.forEach((team) => ownTeamIdSet.add(team.id));
|
||||
}
|
||||
|
||||
const matches = await Match.findAll({
|
||||
where: {
|
||||
@@ -403,6 +483,8 @@ class MatchService {
|
||||
playersReady: match.playersReady || [],
|
||||
playersPlanned: match.playersPlanned || [],
|
||||
playersPlayed: match.playersPlayed || [],
|
||||
fixtureType: match.fixtureType || 'league',
|
||||
notes: match.notes || null,
|
||||
homeTeam: { name: 'Unbekannt' },
|
||||
guestTeam: { name: 'Unbekannt' },
|
||||
location: { name: 'Unbekannt', address: '', city: '', zip: '' },
|
||||
@@ -464,7 +546,14 @@ class MatchService {
|
||||
this.isTeamNameForClub(team.name, club.name)
|
||||
);
|
||||
|
||||
const ownTeamIds = ownTeams.map(t => t.id);
|
||||
const configuredClubTeams = await ClubTeam.findAll({
|
||||
where: { clubId, leagueId },
|
||||
attributes: ['name']
|
||||
});
|
||||
const configuredTeamNames = new Set(configuredClubTeams.map((team) => this.normalizeTeamNameForMatch(team.name)));
|
||||
ownTeams.push(...teamsInLeague.filter((team) => configuredTeamNames.has(this.normalizeTeamNameForMatch(team.name))));
|
||||
|
||||
const ownTeamIds = [...new Set(ownTeams.map(t => t.id))];
|
||||
|
||||
if (ownTeamIds.length > 0) {
|
||||
matches = await Match.findAll({
|
||||
@@ -502,6 +591,8 @@ class MatchService {
|
||||
playersReady: match.playersReady || [],
|
||||
playersPlanned: match.playersPlanned || [],
|
||||
playersPlayed: match.playersPlayed || [],
|
||||
fixtureType: match.fixtureType || 'league',
|
||||
notes: match.notes || null,
|
||||
homeTeam: { name: 'Unbekannt' },
|
||||
guestTeam: { name: 'Unbekannt' },
|
||||
location: { name: 'Unbekannt', address: '', city: '', zip: '' },
|
||||
@@ -641,6 +732,8 @@ class MatchService {
|
||||
playersReady: updated.playersReady || [],
|
||||
playersPlanned: updated.playersPlanned || [],
|
||||
playersPlayed: updated.playersPlayed || [],
|
||||
fixtureType: updated.fixtureType || 'league',
|
||||
notes: updated.notes || null,
|
||||
homeTeam: { name: 'Unbekannt' },
|
||||
guestTeam: { name: 'Unbekannt' },
|
||||
location: { name: 'Unbekannt', address: '', city: '', zip: '' },
|
||||
|
||||
@@ -22,6 +22,14 @@
|
||||
>
|
||||
{{ $t('schedule.importSchedule') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="showScheduleActions && canCreateManualMatch"
|
||||
type="button"
|
||||
class="btn-secondary schedule-add-match-button"
|
||||
@click="$emit('open-manual-match-modal')"
|
||||
>
|
||||
{{ $t('schedule.addMatch') }}
|
||||
</button>
|
||||
<button v-if="showFriendlyActions" @click="$emit('open-friendly-match-modal')" class="btn-secondary">Freundschaftsspiel</button>
|
||||
<button
|
||||
v-if="showGalleryButton"
|
||||
@@ -206,6 +214,7 @@ export default {
|
||||
fetchingTeamData: { type: Boolean, required: true },
|
||||
fetchingTable: { type: Boolean, required: true },
|
||||
showFriendlyActions: { type: Boolean, default: false },
|
||||
canCreateManualMatch: { type: Boolean, default: false },
|
||||
showScheduleActions: { type: Boolean, default: true },
|
||||
showSidebar: { type: Boolean, default: true },
|
||||
showTableTab: { type: Boolean, default: true }
|
||||
@@ -214,6 +223,7 @@ export default {
|
||||
'update:selected-season-id',
|
||||
'season-change',
|
||||
'open-import-modal',
|
||||
'open-manual-match-modal',
|
||||
'open-friendly-match-modal',
|
||||
'open-gallery-dialog',
|
||||
'update:team-search-query',
|
||||
@@ -321,6 +331,12 @@ export default {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.schedule-add-match-button {
|
||||
min-height: 2.5rem;
|
||||
align-self: flex-end;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.schedule-summary-bar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
|
||||
@@ -1479,6 +1479,24 @@
|
||||
"title": "Spielpläne",
|
||||
"subtitle": "Teams auswählen, Spieltage prüfen und Ligatabellen im Blick behalten.",
|
||||
"importSchedule": "Spielplanimport",
|
||||
"addMatch": "Spiel hinzufügen",
|
||||
"manualMatchDescription": "Manuell gepflegte Spiele erscheinen neben den importierten Click-TT-Terminen.",
|
||||
"fixtureType": "Spieltyp:",
|
||||
"cupMatch": "Pokalspiel",
|
||||
"ownTeam": "Eigene Mannschaft",
|
||||
"selectTeam": "Mannschaft wählen",
|
||||
"opponent": "Gegner",
|
||||
"opponentPlaceholder": "Name der gegnerischen Mannschaft",
|
||||
"venue": "Spielstätte",
|
||||
"address": "Adresse",
|
||||
"zip": "PLZ",
|
||||
"city": "Ort",
|
||||
"location": "Spielort",
|
||||
"notes": "Notizen",
|
||||
"saving": "Speichert…",
|
||||
"manualMatchRequired": "Bitte Mannschaft, Gegner, Datum und Uhrzeit ausfüllen.",
|
||||
"manualMatchSaved": "Pokalspiel wurde hinzugefügt.",
|
||||
"manualMatchSaveFailed": "Pokalspiel konnte nicht gespeichert werden.",
|
||||
"galleryLoading": "Galerie wird geladen…",
|
||||
"gallery": "Mitglieder-Galerie",
|
||||
"overallSchedule": "Gesamtspielplan",
|
||||
|
||||
@@ -1456,6 +1456,24 @@
|
||||
"title": "Schedules",
|
||||
"subtitle": "Select teams, review fixtures and keep league tables in view.",
|
||||
"importSchedule": "Import schedule",
|
||||
"addMatch": "Add match",
|
||||
"manualMatchDescription": "Manually maintained matches appear alongside imported Click-TT fixtures.",
|
||||
"fixtureType": "Fixture type:",
|
||||
"cupMatch": "Cup match",
|
||||
"ownTeam": "Own team",
|
||||
"selectTeam": "Select team",
|
||||
"opponent": "Opponent",
|
||||
"opponentPlaceholder": "Opponent team name",
|
||||
"venue": "Venue",
|
||||
"address": "Address",
|
||||
"zip": "Postal code",
|
||||
"city": "City",
|
||||
"location": "Location",
|
||||
"notes": "Notes",
|
||||
"saving": "Saving…",
|
||||
"manualMatchRequired": "Please fill in the team, opponent, date and time.",
|
||||
"manualMatchSaved": "Cup match added.",
|
||||
"manualMatchSaveFailed": "Cup match could not be saved.",
|
||||
"galleryLoading": "Loading gallery…",
|
||||
"gallery": "Member gallery",
|
||||
"overallSchedule": "Overall schedule",
|
||||
|
||||
@@ -24,11 +24,13 @@
|
||||
:fetching-table="fetchingTable"
|
||||
:show-friendly-actions="friendlyOnly"
|
||||
:show-schedule-actions="!friendlyOnly"
|
||||
:can-create-manual-match="canCreateManualMatch"
|
||||
:show-sidebar="!friendlyOnly"
|
||||
:show-table-tab="!friendlyOnly"
|
||||
@update:selected-season-id="selectedSeasonId = $event"
|
||||
@season-change="onSeasonChange"
|
||||
@open-import-modal="openImportModal"
|
||||
@open-manual-match-modal="openManualMatchDialog"
|
||||
@open-friendly-match-modal="openFriendlyMatchDialog"
|
||||
@open-gallery-dialog="openGalleryDialog"
|
||||
@update:team-search-query="teamSearchQuery = $event"
|
||||
@@ -153,7 +155,7 @@
|
||||
<td>{{ formatDate(match.date) }}</td>
|
||||
<td>{{ match.time ? match.time.toString().slice(0, 5) + ' ' + $t('common.time') : 'N/A' }}</td>
|
||||
<td :class="{ 'highlighted-club': isClubHighlighted(match.homeTeam?.name) }">
|
||||
{{ match.homeTeam?.name || 'N/A' }}
|
||||
{{ match.homeTeam?.name || 'N/A' }} <span v-if="match.fixtureType === 'cup'" class="manual-match-badge">{{ $t('schedule.cupMatch') }}</span>
|
||||
</td>
|
||||
<td :class="{ 'highlighted-club': isClubHighlighted(match.guestTeam?.name) }">
|
||||
{{ match.guestTeam?.name || 'N/A' }}
|
||||
@@ -269,6 +271,27 @@
|
||||
|
||||
<!-- Import Modal -->
|
||||
<CsvImportDialog v-model="showImportModal" @import="handleCsvImport" @close="closeImportModal" />
|
||||
|
||||
<BaseDialog v-model="manualMatchDialog.isOpen" :title="$t('schedule.addMatch')" :max-width="620" @close="closeManualMatchDialog">
|
||||
<form class="manual-match-form" @submit.prevent="saveManualMatch">
|
||||
<p class="manual-match-intro">{{ $t('schedule.manualMatchDescription') }}</p>
|
||||
<div class="manual-match-type">{{ $t('schedule.fixtureType') }} <strong>{{ $t('schedule.cupMatch') }}</strong></div>
|
||||
<p v-if="manualMatchDialog.error" class="manual-match-error" role="alert">{{ manualMatchDialog.error }}</p>
|
||||
<div class="manual-match-grid">
|
||||
<label><span>{{ $t('schedule.ownTeam') }} *</span><select v-model="manualMatchDialog.form.clubTeamId" required><option value="">{{ $t('schedule.selectTeam') }}</option><option v-for="team in teams" :key="team.id" :value="String(team.id)">{{ team.name }} <template v-if="team.league?.name">({{ team.league.name }})</template></option></select></label>
|
||||
<label><span>{{ $t('schedule.opponent') }} *</span><input v-model.trim="manualMatchDialog.form.opponentName" required :placeholder="$t('schedule.opponentPlaceholder')"></label>
|
||||
<label><span>{{ $t('schedule.date') }} *</span><input v-model="manualMatchDialog.form.date" type="date" required></label>
|
||||
<label><span>{{ $t('schedule.time') }} *</span><input v-model="manualMatchDialog.form.time" type="time" required></label>
|
||||
<label><span>{{ $t('schedule.venue') }}</span><input v-model.trim="manualMatchDialog.form.locationName"></label>
|
||||
<label><span>{{ $t('schedule.address') }}</span><input v-model.trim="manualMatchDialog.form.locationAddress"></label>
|
||||
<label><span>{{ $t('schedule.zip') }}</span><input v-model.trim="manualMatchDialog.form.locationZip"></label>
|
||||
<label><span>{{ $t('schedule.city') }}</span><input v-model.trim="manualMatchDialog.form.locationCity"></label>
|
||||
</div>
|
||||
<fieldset class="manual-match-home-away"><legend>{{ $t('schedule.location') }}</legend><label><input v-model="manualMatchDialog.form.homeAway" type="radio" value="home"> {{ $t('schedule.homeGame') }}</label><label><input v-model="manualMatchDialog.form.homeAway" type="radio" value="away"> {{ $t('schedule.away') }}</label></fieldset>
|
||||
<label class="manual-match-notes"><span>{{ $t('schedule.notes') }}</span><textarea v-model.trim="manualMatchDialog.form.notes" rows="3"></textarea></label>
|
||||
<div class="dialog-actions"><button type="button" class="btn-cancel" :disabled="manualMatchDialog.saving" @click="closeManualMatchDialog">{{ $t('schedule.cancel') }}</button><button type="submit" class="btn-save" :disabled="manualMatchDialog.saving">{{ manualMatchDialog.saving ? $t('schedule.saving') : $t('schedule.save') }}</button></div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -716,7 +739,10 @@ export default {
|
||||
MemberNotesDialog
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isAuthenticated', 'currentClub', 'clubs', 'currentClubName']),
|
||||
...mapGetters(['isAuthenticated', 'currentClub', 'clubs', 'currentClubName', 'hasPermission']),
|
||||
canCreateManualMatch() {
|
||||
return !this.friendlyOnly && this.hasPermission('schedule', 'write');
|
||||
},
|
||||
filteredScheduleTeams() {
|
||||
const query = this.teamSearchQuery.trim().toLowerCase();
|
||||
if (!query) {
|
||||
@@ -860,6 +886,10 @@ export default {
|
||||
resolveCallback: null
|
||||
},
|
||||
showImportModal: false,
|
||||
manualMatchDialog: {
|
||||
isOpen: false, saving: false, error: '',
|
||||
form: { clubTeamId: '', opponentName: '', date: new Date().toISOString().slice(0, 10), time: '', homeAway: 'home', locationName: '', locationAddress: '', locationZip: '', locationCity: '', notes: '' }
|
||||
},
|
||||
selectedFile: null,
|
||||
teams: [],
|
||||
matches: [],
|
||||
@@ -953,6 +983,39 @@ export default {
|
||||
filterRegularScheduleMatches(matches) {
|
||||
return (Array.isArray(matches) ? matches : []).filter((match) => !match?.isFriendly);
|
||||
},
|
||||
resetManualMatchDialogForm() {
|
||||
this.manualMatchDialog.form = { clubTeamId: this.selectedTeam?.id ? String(this.selectedTeam.id) : '', opponentName: '', date: new Date().toISOString().slice(0, 10), time: '', homeAway: 'home', locationName: '', locationAddress: '', locationZip: '', locationCity: '', notes: '' };
|
||||
this.manualMatchDialog.error = '';
|
||||
},
|
||||
openManualMatchDialog() {
|
||||
this.resetManualMatchDialogForm();
|
||||
this.manualMatchDialog.isOpen = true;
|
||||
},
|
||||
closeManualMatchDialog() {
|
||||
if (this.manualMatchDialog.saving) return;
|
||||
this.manualMatchDialog.isOpen = false;
|
||||
this.resetManualMatchDialogForm();
|
||||
},
|
||||
async saveManualMatch() {
|
||||
const form = this.manualMatchDialog.form;
|
||||
if (!form.clubTeamId || !form.opponentName || !form.date || !form.time) {
|
||||
this.manualMatchDialog.error = this.$t('schedule.manualMatchRequired');
|
||||
return;
|
||||
}
|
||||
this.manualMatchDialog.saving = true;
|
||||
this.manualMatchDialog.error = '';
|
||||
try {
|
||||
await apiClient.post(`/matches/${this.currentClub}`, { ...form, clubTeamId: Number(form.clubTeamId) });
|
||||
this.manualMatchDialog.isOpen = false;
|
||||
this.resetManualMatchDialogForm();
|
||||
await this.refreshScheduleData();
|
||||
this.showInfo(this.$t('messages.success'), this.$t('schedule.manualMatchSaved'), '', 'success');
|
||||
} catch (error) {
|
||||
this.manualMatchDialog.error = getSafeErrorMessage(error, this.$t('schedule.manualMatchSaveFailed'));
|
||||
} finally {
|
||||
this.manualMatchDialog.saving = false;
|
||||
}
|
||||
},
|
||||
getClubNameById(clubId) {
|
||||
const club = (this.clubs || []).find((item) => Number(item.id) === Number(clubId));
|
||||
return club?.name || `Verein ${clubId}`;
|
||||
@@ -2842,16 +2905,16 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
refreshScheduleData() {
|
||||
async refreshScheduleData() {
|
||||
if (!this.selectedLeague) return;
|
||||
if (this.selectedTeam) {
|
||||
this.loadMatchesForSpecificTeam(this.selectedTeam);
|
||||
return this.loadMatchesForSpecificTeam(this.selectedTeam);
|
||||
} else if (this.selectedLeague === this.$t('schedule.overallSchedule')) {
|
||||
this.loadAllMatches();
|
||||
return this.loadAllMatches();
|
||||
} else if (this.selectedLeague === this.$t('schedule.adultSchedule')) {
|
||||
this.loadAdultMatches();
|
||||
return this.loadAdultMatches();
|
||||
} else if (this.selectedLeague === this.friendlyMatchesLabel) {
|
||||
this.loadFriendlyMatches();
|
||||
return this.loadFriendlyMatches();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3827,6 +3890,20 @@ li {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.manual-match-form { display: flex; flex-direction: column; gap: 1rem; }
|
||||
.manual-match-intro { margin: 0; color: var(--text-muted, #6c757d); }
|
||||
.manual-match-type { padding: .65rem .8rem; border-left: 3px solid var(--primary-color); background: var(--surface-muted, #f5f7f6); font-size: .92rem; }
|
||||
.manual-match-type strong { margin-left: .4rem; }
|
||||
.manual-match-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .85rem; }
|
||||
.manual-match-grid label, .manual-match-notes { display: grid; gap: .35rem; font-weight: 600; font-size: .9rem; }
|
||||
.manual-match-grid input, .manual-match-grid select, .manual-match-notes textarea { width: 100%; box-sizing: border-box; padding: .6rem .7rem; border: 1px solid var(--border-color); border-radius: 4px; background: var(--background-light, #fff); color: inherit; font: inherit; }
|
||||
.manual-match-grid input:focus, .manual-match-grid select:focus, .manual-match-notes textarea:focus { outline: 2px solid var(--primary-color); outline-offset: 1px; }
|
||||
.manual-match-home-away { display: flex; gap: 1rem; padding: .65rem .8rem; border: 1px solid var(--border-color); border-radius: 4px; }
|
||||
.manual-match-home-away legend { padding: 0 .3rem; font-size: .86rem; font-weight: 600; }
|
||||
.manual-match-home-away label { font-size: .9rem; }
|
||||
.manual-match-error { margin: 0; padding: .65rem .8rem; color: var(--error-color, #b42318); background: #fff1f0; border-left: 3px solid currentColor; }
|
||||
.manual-match-badge { display: inline-block; margin-left: .35rem; padding: .1rem .35rem; border-radius: 3px; color: var(--primary-strong, #1f5f49); background: rgba(47, 122, 95, .12); font-size: .72rem; font-weight: 700; white-space: nowrap; }
|
||||
|
||||
.btn-save,
|
||||
.btn-cancel {
|
||||
padding: 8px 20px;
|
||||
@@ -4180,6 +4257,8 @@ li {
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.manual-match-grid { grid-template-columns: 1fr; }
|
||||
.manual-match-home-away { flex-direction: column; gap: .45rem; }
|
||||
.player-list { border: 1px solid var(--border-color); border-radius: 8px; }
|
||||
.player-list-scroll-hint { display:block; position:sticky; left:0; margin:0; padding:7px 10px; background:var(--surface-muted); color:var(--text-muted); font-size:.78rem; }
|
||||
.player-selection-table {
|
||||
|
||||
Reference in New Issue
Block a user