Files
harheimertc/pages/cms/mytischtennis.vue
Torsten Schulz (local) 1fc28a6438
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
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.
2026-09-24 11:43:18 +02:00

114 lines
6.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="min-h-screen bg-gray-50 py-10">
<div class="max-w-2xl mx-auto px-4">
<h1 class="text-3xl font-display font-bold text-gray-900">
myTischtennis-Verbindung
</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">
Das Passwort wird verschlüsselt auf dem Server gespeichert und nie wieder angezeigt. Zugriff haben ausschließlich Admin und Vorstand.
</div>
<div>
<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"
>
</div>
<div>
<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"
>
</div>
<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">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>
<p
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 }}
</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">
<button
:disabled="busy"
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>
</form>
</div>
</div>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { useAuthStore } from '~/stores/auth'
definePageMeta({ middleware: 'auth' })
useHead({ title: 'myTischtennis-Verbindung – CMS' })
const form = reactive({ email: '', password: '', association: 'HeTTV', clubId: '43030' })
const authStore = useAuthStore()
const status = ref({ configured: false })
const busy = ref(false); const message = ref(''); const error = ref(false)
const formattedLastImport = computed(() => status.value.lastSuccessfulImportAt ? new Intl.DateTimeFormat('de-DE', { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(status.value.lastSuccessfulImportAt)) : 'noch nie')
async function load() { status.value = await $fetch('/api/cms/mytischtennis') }
async function save() { busy.value = true; message.value = ''; error.value = false; try { status.value = await $fetch('/api/cms/mytischtennis', { method: 'PUT', body: form }); form.password = ''; message.value = 'Zugang verschlüsselt gespeichert.' } catch { error.value = true; message.value = 'Zugang konnte nicht gespeichert werden.' } finally { busy.value = false } }
async function runImport() { busy.value = true; message.value = ''; error.value = false; try { const result = await $fetch('/api/cms/mytischtennis/import', { method: 'POST' }); message.value = `${result.rowCount} Werte wurden aktualisiert.`; await load() } catch (caught) { error.value = true; message.value = caught?.data?.message || 'Abruf fehlgeschlagen. Details stehen im Serverlog.'; await load().catch(() => {}) } finally { busy.value = false } }
async function sendTestPush() { busy.value = true; message.value = ''; error.value = false; try { const result = await $fetch('/api/cms/notifications/test', { method: 'POST' }); if (result.registeredTokenCount === 0) { message.value = 'Kein Android-Token für dieses Konto gespeichert.'; error.value = true } else if (result.skipped) { message.value = `${result.registeredTokenCount} Android-Gerät(e) registriert, aber der FCM-Push-Dienst ist auf dem Server nicht konfiguriert.`; error.value = true } else if (result.deliveryError) { message.value = `${result.registeredTokenCount} Android-Gerät(e) registriert, aber: ${result.deliveryError}`; error.value = true } else if (result.sent > 0) { message.value = `Push-Test wurde an ${result.sent} registrierte(s) Android-Gerät(e) gesendet.` } else { message.value = `${result.registeredTokenCount} Android-Gerät(e) registriert, Zustellung fehlgeschlagen. Bitte Serverlog prüfen.`; error.value = true } } catch (caught) { error.value = true; message.value = caught?.data?.statusMessage || caught?.data?.message || 'Push-Test konnte nicht gesendet werden.' } finally { busy.value = false } }
onMounted(load)
</script>