Verbessere die Benutzeroberfläche der CMS-Seiten durch Anpassungen an der Navigation und dem myTischtennis-Formular. Optimiere die Anzeige der Vereinsränge in der QTTR-Tabelle.
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 5m51s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 3m14s

This commit is contained in:
Torsten Schulz (local)
2026-09-24 11:43:18 +02:00
parent d3e6fbf49b
commit 1fc28a6438
4 changed files with 139 additions and 77 deletions

View File

@@ -292,69 +292,69 @@
<template v-if="currentSubmenu === 'intern'"> <template v-if="currentSubmenu === 'intern'">
<div class="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto whitespace-nowrap"> <div class="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto whitespace-nowrap">
<NuxtLink <NuxtLink
to="/mitgliederbereich" to="/mitgliederbereich"
class="px-2.5 py-1 text-xs font-semibold text-white hover:bg-primary-700/50 rounded transition-all" class="px-2.5 py-1 text-xs font-semibold text-white hover:bg-primary-700/50 rounded transition-all"
active-class="bg-primary-600" active-class="bg-primary-600"
> >
Übersicht Übersicht
</NuxtLink> </NuxtLink>
<div class="h-3 w-px bg-primary-700" />
<NuxtLink
to="/mitgliederbereich/mitglieder"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600"
>
Mitgliederliste
</NuxtLink>
<NuxtLink
to="/mitgliederbereich/qttr"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600"
>
TTR / QTTR
</NuxtLink>
<NuxtLink
to="/mitgliederbereich/news"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600"
>
News
</NuxtLink>
<NuxtLink
to="/mitgliederbereich/profil"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600"
>
Mein Profil
</NuxtLink>
<div class="h-3 w-px bg-primary-700" />
<NuxtLink
to="/mitgliederbereich/api"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600"
>
API-Dokumentation
</NuxtLink>
<template v-if="canAccessNewsletter">
<div class="h-3 w-px bg-primary-700" /> <div class="h-3 w-px bg-primary-700" />
<NuxtLink <NuxtLink
to="/cms/newsletter" to="/mitgliederbereich/mitglieder"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all" class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600" active-class="text-white bg-primary-600"
> >
Newsletter Mitgliederliste
</NuxtLink> </NuxtLink>
</template>
<template v-if="canAccessContactRequests">
<div class="h-3 w-px bg-primary-700" />
<NuxtLink <NuxtLink
to="/cms/kontaktanfragen" to="/mitgliederbereich/qttr"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all" class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600" active-class="text-white bg-primary-600"
> >
Kontaktanfragen TTR / QTTR
</NuxtLink> </NuxtLink>
</template> <NuxtLink
to="/mitgliederbereich/news"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600"
>
News
</NuxtLink>
<NuxtLink
to="/mitgliederbereich/profil"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600"
>
Mein Profil
</NuxtLink>
<div class="h-3 w-px bg-primary-700" />
<NuxtLink
to="/mitgliederbereich/api"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600"
>
API-Dokumentation
</NuxtLink>
<template v-if="canAccessNewsletter">
<div class="h-3 w-px bg-primary-700" />
<NuxtLink
to="/cms/newsletter"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600"
>
Newsletter
</NuxtLink>
</template>
<template v-if="canAccessContactRequests">
<div class="h-3 w-px bg-primary-700" />
<NuxtLink
to="/cms/kontaktanfragen"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600"
>
Kontaktanfragen
</NuxtLink>
</template>
</div> </div>
<template v-if="isAdmin"> <template v-if="isAdmin">
<div class="h-3 w-px shrink-0 bg-primary-700" /> <div class="h-3 w-px shrink-0 bg-primary-700" />

View File

@@ -208,11 +208,18 @@
> >
<div class="flex items-center mb-4"> <div class="flex items-center mb-4">
<div class="w-12 h-12 bg-teal-100 rounded-lg flex items-center justify-center group-hover:bg-teal-600 transition-colors"> <div class="w-12 h-12 bg-teal-100 rounded-lg flex items-center justify-center group-hover:bg-teal-600 transition-colors">
<RefreshCw :size="24" class="text-teal-600 group-hover:text-white" /> <RefreshCw
:size="24"
class="text-teal-600 group-hover:text-white"
/>
</div> </div>
<h2 class="ml-4 text-xl font-semibold text-gray-900">myTischtennis</h2> <h2 class="ml-4 text-xl font-semibold text-gray-900">
myTischtennis
</h2>
</div> </div>
<p class="text-gray-600">Vereinszugang und TTR-Synchronisierung</p> <p class="text-gray-600">
Vereinszugang und TTR-Synchronisierung
</p>
</NuxtLink> </NuxtLink>
<!-- Benutzerverwaltung (Admin ODER Vorstand) --> <!-- Benutzerverwaltung (Admin ODER Vorstand) -->

View File

@@ -1,33 +1,94 @@
<template> <template>
<div class="min-h-screen bg-gray-50 py-10"> <div class="min-h-screen bg-gray-50 py-10">
<div class="max-w-2xl mx-auto px-4"> <div class="max-w-2xl mx-auto px-4">
<h1 class="text-3xl font-display font-bold text-gray-900">myTischtennis-Verbindung</h1> <h1 class="text-3xl font-display font-bold text-gray-900">
<p class="mt-2 text-gray-600">Vereinszugang für den geschützten Import aktueller TTR- und QTTR-Werte.</p> myTischtennis-Verbindung
<form class="mt-8 bg-white rounded-xl shadow-lg p-6 space-y-5" @submit.prevent="save"> </h1>
<p class="mt-2 text-gray-600">
Vereinszugang für den geschützten Import aktueller TTR- und QTTR-Werte.
</p>
<form
class="mt-8 bg-white rounded-xl shadow-lg p-6 space-y-5"
@submit.prevent="save"
>
<div class="rounded-lg bg-amber-50 border border-amber-200 p-4 text-sm text-amber-900"> <div class="rounded-lg bg-amber-50 border border-amber-200 p-4 text-sm text-amber-900">
Das Passwort wird verschlüsselt auf dem Server gespeichert und nie wieder angezeigt. Zugriff haben ausschließlich Admin und Vorstand. Das Passwort wird verschlüsselt auf dem Server gespeichert und nie wieder angezeigt. Zugriff haben ausschließlich Admin und Vorstand.
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">myTischtennis-E-Mail</label> <label class="block text-sm font-medium text-gray-700 mb-1">myTischtennis-E-Mail</label>
<input v-model.trim="form.email" type="email" required autocomplete="username" class="w-full rounded-lg border border-gray-300 px-3 py-2"> <input
v-model.trim="form.email"
type="email"
required
autocomplete="username"
class="w-full rounded-lg border border-gray-300 px-3 py-2"
>
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">Passwort</label> <label class="block text-sm font-medium text-gray-700 mb-1">Passwort</label>
<input v-model="form.password" type="password" required autocomplete="current-password" class="w-full rounded-lg border border-gray-300 px-3 py-2"> <input
v-model="form.password"
type="password"
required
autocomplete="current-password"
class="w-full rounded-lg border border-gray-300 px-3 py-2"
>
</div> </div>
<div class="grid sm:grid-cols-2 gap-4"> <div class="grid sm:grid-cols-2 gap-4">
<label class="block text-sm font-medium text-gray-700">Verband<input v-model.trim="form.association" required class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2"></label> <label class="block text-sm font-medium text-gray-700">Verband<input
<label class="block text-sm font-medium text-gray-700">Vereinsnummer<input v-model.trim="form.clubId" required class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2"></label> v-model.trim="form.association"
required
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2"
></label>
<label class="block text-sm font-medium text-gray-700">Vereinsnummer<input
v-model.trim="form.clubId"
required
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2"
></label>
</div> </div>
<p v-if="status.configured" class="text-sm text-gray-600">Hinterlegt: {{ status.email }} · letzter erfolgreicher Abruf: {{ formattedLastImport }}</p> <p
<p v-if="status.lastImportError" class="rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-800"> v-if="status.configured"
class="text-sm text-gray-600"
>
Hinterlegt: {{ status.email }} · letzter erfolgreicher Abruf: {{ formattedLastImport }}
</p>
<p
v-if="status.lastImportError"
class="rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-800"
>
Letzter Abruf fehlgeschlagen: {{ status.lastImportError }} Letzter Abruf fehlgeschlagen: {{ status.lastImportError }}
</p> </p>
<p v-if="message" class="text-sm" :class="error ? 'text-red-700' : 'text-green-700'">{{ message }}</p> <p
v-if="message"
class="text-sm"
:class="error ? 'text-red-700' : 'text-green-700'"
>
{{ message }}
</p>
<div class="flex gap-3 flex-wrap"> <div class="flex gap-3 flex-wrap">
<button :disabled="busy" class="rounded-lg bg-primary-600 px-4 py-2 font-semibold text-white disabled:opacity-60">Zugang speichern</button> <button
<button type="button" :disabled="busy || !status.configured" class="rounded-lg border border-primary-600 px-4 py-2 font-semibold text-primary-700 disabled:opacity-60" @click="runImport">Jetzt abrufen</button> :disabled="busy"
<button v-if="authStore.hasRole('admin')" type="button" :disabled="busy" class="rounded-lg border border-teal-600 px-4 py-2 font-semibold text-teal-700 disabled:opacity-60" @click="sendTestPush">Push-Test an mich senden</button> class="rounded-lg bg-primary-600 px-4 py-2 font-semibold text-white disabled:opacity-60"
>
Zugang speichern
</button>
<button
type="button"
:disabled="busy || !status.configured"
class="rounded-lg border border-primary-600 px-4 py-2 font-semibold text-primary-700 disabled:opacity-60"
@click="runImport"
>
Jetzt abrufen
</button>
<button
v-if="authStore.hasRole('admin')"
type="button"
:disabled="busy"
class="rounded-lg border border-teal-600 px-4 py-2 font-semibold text-teal-700 disabled:opacity-60"
@click="sendTestPush"
>
Push-Test an mich senden
</button>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -72,7 +72,7 @@
<thead class="bg-gray-50"> <thead class="bg-gray-50">
<tr> <tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500"> <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500">
Vereinsrang<br>{{ sortBy === 'qttr' ? '(QTTR / TTR)' : '(TTR / QTTR)' }} Vereinsrang<br>(QTTR / TTR)
</th> </th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500"> <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500">
Spieler Spieler
@@ -190,17 +190,11 @@ function isOwnRow(playerName) {
} }
function formatClubRank(row) { function formatClubRank(row) {
const activeRank = sortBy.value === 'qttr' ? row.clubQttrRank : row.clubTtrRank return `${row.clubQttrRank ?? '–'} (${row.clubTtrRank ?? '–'})`
const otherRank = sortBy.value === 'qttr' ? row.clubTtrRank : row.clubQttrRank
return `${activeRank ?? '–'} (${otherRank ?? '–'})`
} }
function clubRankTitle(row) { function clubRankTitle(row) {
const activeLabel = sortBy.value === 'qttr' ? 'QTTR' : 'TTR' return `QTTR-Platzierung im Verein: ${row.clubQttrRank ?? 'nicht verfügbar'}; TTR-Platzierung im Verein: ${row.clubTtrRank ?? 'nicht verfügbar'}`
const otherLabel = sortBy.value === 'qttr' ? 'TTR' : 'QTTR'
const activeRank = sortBy.value === 'qttr' ? row.clubQttrRank : row.clubTtrRank
const otherRank = sortBy.value === 'qttr' ? row.clubTtrRank : row.clubQttrRank
return `${activeLabel}-Platzierung im Verein: ${activeRank ?? 'nicht verfügbar'}; ${otherLabel}-Platzierung im Verein: ${otherRank ?? 'nicht verfügbar'}`
} }
function getPlayerNameClass(row) { function getPlayerNameClass(row) {