Update package-lock.json and package.json to include 'globals' dependency and improve code formatting in various components for better readability.
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 54s

This commit is contained in:
Torsten Schulz (local)
2025-12-20 10:17:16 +01:00
parent 861802b716
commit b20b89d333
72 changed files with 5338 additions and 2008 deletions

View File

@@ -13,19 +13,27 @@
<!-- Authentication Info -->
<div class="bg-blue-50 border-l-4 border-blue-500 p-6 rounded-lg mb-8">
<h2 class="text-xl font-semibold text-blue-900 mb-2">Authentifizierung</h2>
<h2 class="text-xl font-semibold text-blue-900 mb-2">
Authentifizierung
</h2>
<p class="text-blue-800 mb-4">
Alle API-Endpoints erfordern Authentifizierung (außer Login). Es werden zwei Methoden unterstützt:
</p>
<div class="space-y-3">
<div>
<strong class="text-blue-900">1. Cookie-basiert:</strong>
<p class="text-blue-700 text-sm mt-1">Nach dem Login über <code>/api/auth/login</code> wird automatisch ein Cookie gesetzt.</p>
<p class="text-blue-700 text-sm mt-1">
Nach dem Login über <code>/api/auth/login</code> wird automatisch ein Cookie gesetzt.
</p>
</div>
<div>
<strong class="text-blue-900">2. Authorization Header:</strong>
<p class="text-blue-700 text-sm mt-1">Header: <code>Authorization: Bearer &lt;token&gt;</code></p>
<p class="text-blue-700 text-sm">Der Token wird im Login-Response im Feld <code>token</code> zurückgegeben.</p>
<p class="text-blue-700 text-sm mt-1">
Header: <code>Authorization: Bearer &lt;token&gt;</code>
</p>
<p class="text-blue-700 text-sm">
Der Token wird im Login-Response im Feld <code>token</code> zurückgegeben.
</p>
</div>
</div>
</div>
@@ -34,19 +42,27 @@
<div class="space-y-8">
<!-- Authentication Endpoints -->
<section class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6">Authentifizierung</h2>
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6">
Authentifizierung
</h2>
<div class="space-y-6">
<!-- Login -->
<div class="border-l-4 border-primary-600 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">POST /api/auth/login</h3>
<h3 class="text-lg font-semibold text-gray-900">
POST /api/auth/login
</h3>
<span class="px-2 py-1 text-xs font-medium bg-green-100 text-green-800 rounded">Öffentlich</span>
</div>
<p class="text-gray-600 mb-3">Benutzer einloggen und Token erhalten</p>
<p class="text-gray-600 mb-3">
Benutzer einloggen und Token erhalten
</p>
<div class="bg-gray-50 rounded-lg p-4 mb-3">
<p class="text-sm font-medium text-gray-700 mb-2">Request Body:</p>
<p class="text-sm font-medium text-gray-700 mb-2">
Request Body:
</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"email": "benutzer@example.com",
"password": "passwort"
@@ -54,7 +70,9 @@
</div>
<div class="bg-gray-50 rounded-lg p-4">
<p class="text-sm font-medium text-gray-700 mb-2">Response:</p>
<p class="text-sm font-medium text-gray-700 mb-2">
Response:
</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
@@ -71,13 +89,19 @@
<!-- Logout -->
<div class="border-l-4 border-primary-600 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">POST /api/auth/logout</h3>
<h3 class="text-lg font-semibold text-gray-900">
POST /api/auth/logout
</h3>
<span class="px-2 py-1 text-xs font-medium bg-yellow-100 text-yellow-800 rounded">Auth erforderlich</span>
</div>
<p class="text-gray-600 mb-3">Benutzer ausloggen</p>
<p class="text-gray-600 mb-3">
Benutzer ausloggen
</p>
<div class="bg-gray-50 rounded-lg p-4">
<p class="text-sm font-medium text-gray-700 mb-2">Response:</p>
<p class="text-sm font-medium text-gray-700 mb-2">
Response:
</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"success": true,
"message": "Erfolgreich ausgeloggt"
@@ -88,13 +112,19 @@
<!-- Auth Status -->
<div class="border-l-4 border-primary-600 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">GET /api/auth/status</h3>
<h3 class="text-lg font-semibold text-gray-900">
GET /api/auth/status
</h3>
<span class="px-2 py-1 text-xs font-medium bg-yellow-100 text-yellow-800 rounded">Auth erforderlich</span>
</div>
<p class="text-gray-600 mb-3">Aktuellen Authentifizierungsstatus abrufen</p>
<p class="text-gray-600 mb-3">
Aktuellen Authentifizierungsstatus abrufen
</p>
<div class="bg-gray-50 rounded-lg p-4">
<p class="text-sm font-medium text-gray-700 mb-2">Response:</p>
<p class="text-sm font-medium text-gray-700 mb-2">
Response:
</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"isLoggedIn": true,
"user": {
@@ -112,19 +142,27 @@
<!-- Members Endpoints -->
<section class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6">Mitglieder</h2>
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6">
Mitglieder
</h2>
<div class="space-y-6">
<!-- Get Members -->
<div class="border-l-4 border-primary-600 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">GET /api/members</h3>
<h3 class="text-lg font-semibold text-gray-900">
GET /api/members
</h3>
<span class="px-2 py-1 text-xs font-medium bg-yellow-100 text-yellow-800 rounded">Auth erforderlich</span>
</div>
<p class="text-gray-600 mb-3">Alle Mitglieder abrufen (mit Merge aus registrierten Benutzern)</p>
<p class="text-gray-600 mb-3">
Alle Mitglieder abrufen (mit Merge aus registrierten Benutzern)
</p>
<div class="bg-gray-50 rounded-lg p-4">
<p class="text-sm font-medium text-gray-700 mb-2">Response:</p>
<p class="text-sm font-medium text-gray-700 mb-2">
Response:
</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"success": true,
"members": [
@@ -148,13 +186,19 @@
<!-- Post Members -->
<div class="border-l-4 border-red-500 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">POST /api/members</h3>
<h3 class="text-lg font-semibold text-gray-900">
POST /api/members
</h3>
<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded">admin/vorstand</span>
</div>
<p class="text-gray-600 mb-3">Neues Mitglied hinzufügen oder bestehendes bearbeiten</p>
<p class="text-gray-600 mb-3">
Neues Mitglied hinzufügen oder bestehendes bearbeiten
</p>
<div class="bg-gray-50 rounded-lg p-4 mb-3">
<p class="text-sm font-medium text-gray-700 mb-2">Request Body:</p>
<p class="text-sm font-medium text-gray-700 mb-2">
Request Body:
</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"id": "optional-für-update",
"firstName": "Max",
@@ -168,7 +212,9 @@
</div>
<div class="bg-gray-50 rounded-lg p-4 mb-3">
<p class="text-sm font-medium text-gray-700 mb-2">Response:</p>
<p class="text-sm font-medium text-gray-700 mb-2">
Response:
</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"success": true,
"message": "Mitglied erfolgreich gespeichert."
@@ -185,13 +231,19 @@
<!-- Bulk Import Members -->
<div class="border-l-4 border-red-500 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">POST /api/members/bulk</h3>
<h3 class="text-lg font-semibold text-gray-900">
POST /api/members/bulk
</h3>
<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded">admin/vorstand</span>
</div>
<p class="text-gray-600 mb-3">Mehrere Mitglieder auf einmal importieren (Bulk-Import)</p>
<p class="text-gray-600 mb-3">
Mehrere Mitglieder auf einmal importieren (Bulk-Import)
</p>
<div class="bg-gray-50 rounded-lg p-4 mb-3">
<p class="text-sm font-medium text-gray-700 mb-2">Request Body:</p>
<p class="text-sm font-medium text-gray-700 mb-2">
Request Body:
</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"members": [
{
@@ -214,7 +266,9 @@
</div>
<div class="bg-gray-50 rounded-lg p-4 mb-3">
<p class="text-sm font-medium text-gray-700 mb-2">Response:</p>
<p class="text-sm font-medium text-gray-700 mb-2">
Response:
</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"success": true,
"summary": {
@@ -253,20 +307,28 @@
<!-- Delete Members -->
<div class="border-l-4 border-red-500 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">DELETE /api/members</h3>
<h3 class="text-lg font-semibold text-gray-900">
DELETE /api/members
</h3>
<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded">admin/vorstand</span>
</div>
<p class="text-gray-600 mb-3">Mitglied löschen</p>
<p class="text-gray-600 mb-3">
Mitglied löschen
</p>
<div class="bg-gray-50 rounded-lg p-4 mb-3">
<p class="text-sm font-medium text-gray-700 mb-2">Request Body:</p>
<p class="text-sm font-medium text-gray-700 mb-2">
Request Body:
</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"id": "member-id"
}</code></pre>
</div>
<div class="bg-gray-50 rounded-lg p-4">
<p class="text-sm font-medium text-gray-700 mb-2">Response:</p>
<p class="text-sm font-medium text-gray-700 mb-2">
Response:
</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"success": true,
"message": "Mitglied erfolgreich gelöscht."
@@ -278,28 +340,40 @@
<!-- News Endpoints -->
<section class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6">News</h2>
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6">
News
</h2>
<div class="space-y-6">
<!-- Get News -->
<div class="border-l-4 border-primary-600 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">GET /api/news</h3>
<h3 class="text-lg font-semibold text-gray-900">
GET /api/news
</h3>
<span class="px-2 py-1 text-xs font-medium bg-yellow-100 text-yellow-800 rounded">Auth erforderlich</span>
</div>
<p class="text-gray-600 mb-3">Alle News abrufen (inkl. interner News)</p>
<p class="text-gray-600 mb-3">
Alle News abrufen (inkl. interner News)
</p>
</div>
<!-- Post News -->
<div class="border-l-4 border-red-500 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">POST /api/news</h3>
<h3 class="text-lg font-semibold text-gray-900">
POST /api/news
</h3>
<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded">admin/vorstand</span>
</div>
<p class="text-gray-600 mb-3">Neue News erstellen oder bestehende bearbeiten</p>
<p class="text-gray-600 mb-3">
Neue News erstellen oder bestehende bearbeiten
</p>
<div class="bg-gray-50 rounded-lg p-4 mb-3">
<p class="text-sm font-medium text-gray-700 mb-2">Request Body:</p>
<p class="text-sm font-medium text-gray-700 mb-2">
Request Body:
</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"id": "optional-für-update",
"title": "Titel der News",
@@ -314,13 +388,19 @@
<!-- Delete News -->
<div class="border-l-4 border-red-500 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">DELETE /api/news</h3>
<h3 class="text-lg font-semibold text-gray-900">
DELETE /api/news
</h3>
<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded">admin/vorstand</span>
</div>
<p class="text-gray-600 mb-3">News löschen</p>
<p class="text-gray-600 mb-3">
News löschen
</p>
<div class="bg-gray-50 rounded-lg p-4 mb-3">
<p class="text-sm font-medium text-gray-700 mb-2">Request Body:</p>
<p class="text-sm font-medium text-gray-700 mb-2">
Request Body:
</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"id": "news-id"
}</code></pre>
@@ -331,28 +411,40 @@
<!-- Termine Endpoints -->
<section class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6">Termine</h2>
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6">
Termine
</h2>
<div class="space-y-6">
<!-- Get Termine -->
<div class="border-l-4 border-red-500 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">GET /api/termine-manage</h3>
<h3 class="text-lg font-semibold text-gray-900">
GET /api/termine-manage
</h3>
<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded">admin/vorstand</span>
</div>
<p class="text-gray-600 mb-3">Alle Termine abrufen (für Verwaltung)</p>
<p class="text-gray-600 mb-3">
Alle Termine abrufen (für Verwaltung)
</p>
</div>
<!-- Post Termine -->
<div class="border-l-4 border-red-500 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">POST /api/termine-manage</h3>
<h3 class="text-lg font-semibold text-gray-900">
POST /api/termine-manage
</h3>
<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded">admin/vorstand</span>
</div>
<p class="text-gray-600 mb-3">Neuen Termin erstellen</p>
<p class="text-gray-600 mb-3">
Neuen Termin erstellen
</p>
<div class="bg-gray-50 rounded-lg p-4 mb-3">
<p class="text-sm font-medium text-gray-700 mb-2">Request Body:</p>
<p class="text-sm font-medium text-gray-700 mb-2">
Request Body:
</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"datum": "2025-12-25",
"uhrzeit": "19:00",
@@ -366,13 +458,19 @@
<!-- Delete Termine -->
<div class="border-l-4 border-red-500 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">DELETE /api/termine-manage</h3>
<h3 class="text-lg font-semibold text-gray-900">
DELETE /api/termine-manage
</h3>
<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded">admin/vorstand</span>
</div>
<p class="text-gray-600 mb-3">Termin löschen</p>
<p class="text-gray-600 mb-3">
Termin löschen
</p>
<div class="bg-gray-50 rounded-lg p-4 mb-3">
<p class="text-sm font-medium text-gray-700 mb-2">Query Parameters:</p>
<p class="text-sm font-medium text-gray-700 mb-2">
Query Parameters:
</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>?datum=2025-12-25&uhrzeit=19:00&titel=Weihnachtsfeier&beschreibung=...&kategorie=...</code></pre>
</div>
</div>
@@ -381,29 +479,43 @@
<!-- Config Endpoints -->
<section class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6">Konfiguration</h2>
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6">
Konfiguration
</h2>
<div class="space-y-6">
<!-- Get Config -->
<div class="border-l-4 border-red-500 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">GET /api/config</h3>
<h3 class="text-lg font-semibold text-gray-900">
GET /api/config
</h3>
<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded">admin/vorstand</span>
</div>
<p class="text-gray-600 mb-3">Vereinskonfiguration abrufen</p>
<p class="text-gray-600 mb-3">
Vereinskonfiguration abrufen
</p>
</div>
<!-- Put Config -->
<div class="border-l-4 border-red-500 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">PUT /api/config</h3>
<h3 class="text-lg font-semibold text-gray-900">
PUT /api/config
</h3>
<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded">admin/vorstand</span>
</div>
<p class="text-gray-600 mb-3">Vereinskonfiguration aktualisieren</p>
<p class="text-gray-600 mb-3">
Vereinskonfiguration aktualisieren
</p>
<div class="bg-gray-50 rounded-lg p-4 mb-3">
<p class="text-sm font-medium text-gray-700 mb-2">Request Body:</p>
<p class="text-xs text-gray-600">Komplettes Config-Objekt mit allen Einstellungen</p>
<p class="text-sm font-medium text-gray-700 mb-2">
Request Body:
</p>
<p class="text-xs text-gray-600">
Komplettes Config-Objekt mit allen Einstellungen
</p>
</div>
</div>
</div>
@@ -412,11 +524,15 @@
<!-- Example Usage -->
<div class="mt-12 bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6">Beispiel-Usage</h2>
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6">
Beispiel-Usage
</h2>
<div class="space-y-4">
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">cURL Beispiel:</h3>
<h3 class="text-lg font-semibold text-gray-900 mb-2">
cURL Beispiel:
</h3>
<pre class="text-xs bg-gray-900 text-gray-100 p-4 rounded overflow-x-auto"><code># Login und Token erhalten
curl -X POST http://localhost:3100/api/auth/login \
-H "Content-Type: application/json" \
@@ -457,7 +573,9 @@ curl -X POST http://localhost:3100/api/members/bulk \
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">JavaScript/Fetch Beispiel:</h3>
<h3 class="text-lg font-semibold text-gray-900 mb-2">
JavaScript/Fetch Beispiel:
</h3>
<pre class="text-xs bg-gray-900 text-gray-100 p-4 rounded overflow-x-auto"><code>// Login
const loginResponse = await fetch('/api/auth/login', {
method: 'POST',
@@ -516,7 +634,9 @@ const result = await bulkResponse.json()
<!-- Role Legend -->
<div class="mt-8 bg-gray-50 rounded-xl p-6">
<h2 class="text-xl font-semibold text-gray-900 mb-4">Legende</h2>
<h2 class="text-xl font-semibold text-gray-900 mb-4">
Legende
</h2>
<div class="grid md:grid-cols-3 gap-4">
<div class="flex items-center space-x-2">
<span class="px-2 py-1 text-xs font-medium bg-green-100 text-green-800 rounded">Öffentlich</span>

View File

@@ -14,9 +14,14 @@
>
<div class="flex items-center mb-4">
<div class="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center group-hover:bg-primary-600 transition-colors">
<User :size="24" class="text-primary-600 group-hover:text-white" />
<User
:size="24"
class="text-primary-600 group-hover:text-white"
/>
</div>
<h2 class="ml-4 text-xl font-semibold text-gray-900">Mein Profil</h2>
<h2 class="ml-4 text-xl font-semibold text-gray-900">
Mein Profil
</h2>
</div>
<p class="text-gray-600">
Persönliche Daten und Passwort verwalten
@@ -30,9 +35,14 @@
>
<div class="flex items-center mb-4">
<div class="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center group-hover:bg-primary-600 transition-colors">
<Users :size="24" class="text-primary-600 group-hover:text-white" />
<Users
:size="24"
class="text-primary-600 group-hover:text-white"
/>
</div>
<h2 class="ml-4 text-xl font-semibold text-gray-900">Mitglieder</h2>
<h2 class="ml-4 text-xl font-semibold text-gray-900">
Mitglieder
</h2>
</div>
<p class="text-gray-600">
Kontaktdaten der Vereinsmitglieder
@@ -46,9 +56,14 @@
>
<div class="flex items-center mb-4">
<div class="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center group-hover:bg-primary-600 transition-colors">
<Newspaper :size="24" class="text-primary-600 group-hover:text-white" />
<Newspaper
:size="24"
class="text-primary-600 group-hover:text-white"
/>
</div>
<h2 class="ml-4 text-xl font-semibold text-gray-900">News</h2>
<h2 class="ml-4 text-xl font-semibold text-gray-900">
News
</h2>
</div>
<p class="text-gray-600">
Neuigkeiten und Ankündigungen
@@ -66,19 +81,31 @@
</p>
<div class="grid sm:grid-cols-2 gap-4">
<div class="flex items-start">
<Check :size="20" class="text-primary-600 mr-2 mt-0.5" />
<Check
:size="20"
class="text-primary-600 mr-2 mt-0.5"
/>
<span class="text-gray-700">Zugriff auf Mitgliederliste mit Kontaktdaten</span>
</div>
<div class="flex items-start">
<Check :size="20" class="text-primary-600 mr-2 mt-0.5" />
<Check
:size="20"
class="text-primary-600 mr-2 mt-0.5"
/>
<span class="text-gray-700">Vereinsnews und Ankündigungen</span>
</div>
<div class="flex items-start">
<Check :size="20" class="text-primary-600 mr-2 mt-0.5" />
<Check
:size="20"
class="text-primary-600 mr-2 mt-0.5"
/>
<span class="text-gray-700">Profilverwaltung und Passwort ändern</span>
</div>
<div class="flex items-start">
<Check :size="20" class="text-primary-600 mr-2 mt-0.5" />
<Check
:size="20"
class="text-primary-600 mr-2 mt-0.5"
/>
<span class="text-gray-700">Weitere Funktionen folgen in Kürze</span>
</div>
</div>

View File

@@ -10,80 +10,151 @@
</div>
<div class="flex items-center space-x-3">
<button
@click="viewMode = viewMode === 'cards' ? 'table' : 'cards'"
class="flex items-center px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 font-semibold rounded-lg transition-colors"
@click="viewMode = viewMode === 'cards' ? 'table' : 'cards'"
>
<component :is="viewMode === 'cards' ? Table2 : Grid3x3" :size="20" class="mr-2" />
<component
:is="viewMode === 'cards' ? Table2 : Grid3x3"
:size="20"
class="mr-2"
/>
{{ viewMode === 'cards' ? 'Tabelle' : 'Karten' }}
</button>
<button
v-if="canEdit"
@click="showBulkImportModal = true"
class="flex items-center px-4 py-2 bg-green-600 hover:bg-green-700 text-white font-semibold rounded-lg transition-colors"
@click="showBulkImportModal = true"
>
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
<svg
class="w-5 h-5 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
Bulk-Import
</button>
<button
v-if="canEdit"
@click="openAddModal"
class="flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors"
@click="openAddModal"
>
<UserPlus :size="20" class="mr-2" />
<UserPlus
:size="20"
class="mr-2"
/>
Mitglied hinzufügen
</button>
</div>
</div>
<!-- Loading State -->
<div v-if="isLoading" class="flex items-center justify-center py-12">
<Loader2 :size="40" class="animate-spin text-primary-600" />
<div
v-if="isLoading"
class="flex items-center justify-center py-12"
>
<Loader2
:size="40"
class="animate-spin text-primary-600"
/>
</div>
<!-- Table View -->
<div v-else-if="viewMode === 'table'" class="bg-white rounded-xl shadow-lg overflow-hidden">
<div
v-else-if="viewMode === 'table'"
class="bg-white rounded-xl shadow-lg overflow-hidden"
>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">E-Mail</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Telefon</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Mannschaft</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th v-if="canEdit" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Aktionen</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Name
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
E-Mail
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Telefon
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Mannschaft
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th
v-if="canEdit"
class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Aktionen
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="member in members" :key="member.id" class="hover:bg-gray-50">
<tr
v-for="member in members"
:key="member.id"
class="hover:bg-gray-50"
>
<td class="px-4 py-3 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900">{{ member.name }}</div>
<div v-if="member.notes" class="text-xs text-gray-500">{{ member.notes }}</div>
<div class="text-sm font-medium text-gray-900">
{{ member.name }}
</div>
<div
v-if="member.notes"
class="text-xs text-gray-500"
>
{{ member.notes }}
</div>
</td>
<td class="px-4 py-3 whitespace-nowrap">
<template v-if="canViewContactData">
<a v-if="member.email" :href="`mailto:${member.email}`" class="text-sm text-primary-600 hover:text-primary-800">
<a
v-if="member.email"
:href="`mailto:${member.email}`"
class="text-sm text-primary-600 hover:text-primary-800"
>
{{ member.email }}
</a>
<span v-else class="text-sm text-gray-400">-</span>
<span
v-else
class="text-sm text-gray-400"
>-</span>
</template>
<span v-else class="text-sm text-gray-400">Nur für Vorstand</span>
<span
v-else
class="text-sm text-gray-400"
>Nur für Vorstand</span>
</td>
<td class="px-4 py-3 whitespace-nowrap">
<template v-if="canViewContactData">
<a v-if="member.phone" :href="`tel:${member.phone}`" class="text-sm text-primary-600 hover:text-primary-800">
<a
v-if="member.phone"
:href="`tel:${member.phone}`"
class="text-sm text-primary-600 hover:text-primary-800"
>
{{ member.phone }}
</a>
<span v-else class="text-sm text-gray-400">-</span>
<span
v-else
class="text-sm text-gray-400"
>-</span>
</template>
<span v-else class="text-sm text-gray-400">Nur für Vorstand</span>
<span
v-else
class="text-sm text-gray-400"
>Nur für Vorstand</span>
</td>
<td class="px-4 py-3 whitespace-nowrap">
<button
v-if="canEdit"
@click="toggleMannschaftsspieler(member)"
:class="[
'px-2 py-1 text-xs font-medium rounded-full transition-colors',
member.isMannschaftsspieler
@@ -91,6 +162,7 @@
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
]"
title="Klicken zum Umschalten"
@click="toggleMannschaftsspieler(member)"
>
{{ member.isMannschaftsspieler ? 'Ja' : 'Nein' }}
</button>
@@ -122,37 +194,52 @@
</span>
</div>
</td>
<td v-if="canEdit" class="px-4 py-3 whitespace-nowrap text-right text-sm font-medium">
<div v-if="member.editable" class="flex justify-end space-x-2">
<td
v-if="canEdit"
class="px-4 py-3 whitespace-nowrap text-right text-sm font-medium"
>
<div
v-if="member.editable"
class="flex justify-end space-x-2"
>
<button
@click="openEditModal(member)"
class="text-blue-600 hover:text-blue-900"
title="Bearbeiten"
@click="openEditModal(member)"
>
<Edit :size="18" />
</button>
<button
@click="confirmDelete(member)"
class="text-red-600 hover:text-red-900"
title="Löschen"
@click="confirmDelete(member)"
>
<Trash2 :size="18" />
</button>
</div>
<span v-else class="text-gray-400 text-xs">Nicht editierbar</span>
<span
v-else
class="text-gray-400 text-xs"
>Nicht editierbar</span>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="members.length === 0" class="text-center py-12 text-gray-500">
<div
v-if="members.length === 0"
class="text-center py-12 text-gray-500"
>
Keine Mitglieder gefunden.
</div>
</div>
<!-- Cards View -->
<div v-else class="space-y-4">
<div
v-else
class="space-y-4"
>
<div
v-for="member in members"
:key="member.id"
@@ -161,7 +248,9 @@
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="flex items-center mb-2">
<h3 class="text-xl font-semibold text-gray-900">{{ member.name }}</h3>
<h3 class="text-xl font-semibold text-gray-900">
{{ member.name }}
</h3>
<span
v-if="member.hasLogin"
class="ml-3 px-2 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-full"
@@ -182,7 +271,6 @@
</span>
<button
v-if="canEdit"
@click="toggleMannschaftsspieler(member)"
:class="[
'ml-2 px-2 py-1 text-xs font-medium rounded-full transition-colors',
member.isMannschaftsspieler
@@ -190,6 +278,7 @@
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
]"
title="Klicken zum Umschalten"
@click="toggleMannschaftsspieler(member)"
>
Mannschaftsspieler: {{ member.isMannschaftsspieler ? 'Ja' : 'Nein' }}
</button>
@@ -208,46 +297,91 @@
<div class="grid sm:grid-cols-2 gap-3 text-gray-600">
<template v-if="canViewContactData">
<div v-if="member.email" class="flex items-center">
<Mail :size="16" class="mr-2 text-primary-600" />
<a :href="`mailto:${member.email}`" class="hover:text-primary-600">{{ member.email }}</a>
<div
v-if="member.email"
class="flex items-center"
>
<Mail
:size="16"
class="mr-2 text-primary-600"
/>
<a
:href="`mailto:${member.email}`"
class="hover:text-primary-600"
>{{ member.email }}</a>
</div>
<div v-if="member.phone" class="flex items-center">
<Phone :size="16" class="mr-2 text-primary-600" />
<a :href="`tel:${member.phone}`" class="hover:text-primary-600">{{ member.phone }}</a>
<div
v-if="member.phone"
class="flex items-center"
>
<Phone
:size="16"
class="mr-2 text-primary-600"
/>
<a
:href="`tel:${member.phone}`"
class="hover:text-primary-600"
>{{ member.phone }}</a>
</div>
</template>
<div v-else class="col-span-2 flex items-center text-gray-500 text-sm italic">
<Mail :size="16" class="mr-2" />
<div
v-else
class="col-span-2 flex items-center text-gray-500 text-sm italic"
>
<Mail
:size="16"
class="mr-2"
/>
Kontaktdaten nur für Vorstand sichtbar
</div>
<div v-if="member.address" class="flex items-start col-span-2">
<MapPin :size="16" class="mr-2 text-primary-600 mt-0.5" />
<div
v-if="member.address"
class="flex items-start col-span-2"
>
<MapPin
:size="16"
class="mr-2 text-primary-600 mt-0.5"
/>
<span>{{ member.address }}</span>
</div>
<div v-if="member.notes" class="flex items-start col-span-2">
<FileText :size="16" class="mr-2 text-primary-600 mt-0.5" />
<div
v-if="member.notes"
class="flex items-start col-span-2"
>
<FileText
:size="16"
class="mr-2 text-primary-600 mt-0.5"
/>
<span>{{ member.notes }}</span>
</div>
<div v-if="member.lastLogin" class="flex items-center col-span-2 text-sm text-gray-500">
<Clock :size="16" class="mr-2" />
<div
v-if="member.lastLogin"
class="flex items-center col-span-2 text-sm text-gray-500"
>
<Clock
:size="16"
class="mr-2"
/>
Letzter Login: {{ formatDate(member.lastLogin) }}
</div>
</div>
</div>
<div v-if="canEdit && member.editable" class="flex space-x-2 ml-4">
<div
v-if="canEdit && member.editable"
class="flex space-x-2 ml-4"
>
<button
@click="openEditModal(member)"
class="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
title="Bearbeiten"
@click="openEditModal(member)"
>
<Edit :size="20" />
</button>
<button
@click="confirmDelete(member)"
class="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="Löschen"
@click="confirmDelete(member)"
>
<Trash2 :size="20" />
</button>
@@ -255,7 +389,10 @@
</div>
</div>
<div v-if="members.length === 0" class="text-center py-12 text-gray-500">
<div
v-if="members.length === 0"
class="text-center py-12 text-gray-500"
>
Keine Mitglieder gefunden.
</div>
</div>
@@ -271,7 +408,10 @@
{{ editingMember ? 'Mitglied bearbeiten' : 'Mitglied hinzufügen' }}
</h2>
<form @submit.prevent="saveMember" class="space-y-4">
<form
class="space-y-4"
@submit.prevent="saveMember"
>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Vorname *</label>
@@ -281,7 +421,7 @@
required
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
:disabled="isSaving"
/>
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Nachname *</label>
@@ -291,7 +431,7 @@
required
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
:disabled="isSaving"
/>
>
</div>
</div>
@@ -303,8 +443,10 @@
required
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
:disabled="isSaving"
/>
<p class="text-xs text-gray-500 mt-1">Wird zur eindeutigen Identifizierung benötigt</p>
>
<p class="text-xs text-gray-500 mt-1">
Wird zur eindeutigen Identifizierung benötigt
</p>
</div>
<div>
@@ -314,7 +456,7 @@
type="email"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
:disabled="isSaving"
/>
>
</div>
<div>
@@ -324,7 +466,7 @@
type="tel"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
:disabled="isSaving"
/>
>
</div>
<div>
@@ -334,7 +476,7 @@
type="text"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
:disabled="isSaving"
/>
>
</div>
<div>
@@ -349,28 +491,37 @@
<div class="flex items-center">
<input
id="isMannschaftsspieler"
v-model="formData.isMannschaftsspieler"
type="checkbox"
id="isMannschaftsspieler"
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
:disabled="isSaving"
/>
<label for="isMannschaftsspieler" class="ml-2 block text-sm font-medium text-gray-700">
>
<label
for="isMannschaftsspieler"
class="ml-2 block text-sm font-medium text-gray-700"
>
Mannschaftsspieler
</label>
</div>
<div v-if="errorMessage" class="flex items-center p-3 rounded-md bg-red-50 text-red-700 text-sm">
<AlertCircle :size="20" class="mr-2" />
<div
v-if="errorMessage"
class="flex items-center p-3 rounded-md bg-red-50 text-red-700 text-sm"
>
<AlertCircle
:size="20"
class="mr-2"
/>
{{ errorMessage }}
</div>
<div class="flex justify-end space-x-4 pt-4">
<button
type="button"
@click="closeModal"
class="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
:disabled="isSaving"
@click="closeModal"
>
Abbrechen
</button>
@@ -379,7 +530,11 @@
class="px-6 py-2 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors flex items-center"
:disabled="isSaving"
>
<Loader2 v-if="isSaving" :size="20" class="animate-spin mr-2" />
<Loader2
v-if="isSaving"
:size="20"
class="animate-spin mr-2"
/>
<span>{{ isSaving ? 'Speichert...' : 'Speichern' }}</span>
</button>
</div>
@@ -402,33 +557,54 @@
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">CSV-Datei hochladen</label>
<div
class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-primary-400 hover:bg-primary-50 transition-colors cursor-pointer"
:class="{ 'border-primary-400 bg-primary-50': isDragOver }"
@click="triggerBulkFileInput"
@dragover.prevent
@dragenter.prevent="isDragOver = true"
@dragleave.prevent="isDragOver = false"
@drop.prevent="handleBulkFileDrop"
class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-primary-400 hover:bg-primary-50 transition-colors cursor-pointer"
:class="{ 'border-primary-400 bg-primary-50': isDragOver }"
>
<svg class="w-12 h-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
<svg
class="w-12 h-12 text-gray-400 mx-auto mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
<p class="text-lg font-medium text-gray-900 mb-2">CSV-Datei hochladen</p>
<p class="text-sm text-gray-600 mb-4">Klicken Sie hier oder ziehen Sie eine CSV-Datei hierher</p>
<p v-if="bulkSelectedFile" class="text-sm text-primary-600 font-medium">{{ bulkSelectedFile.name }}</p>
<p class="text-lg font-medium text-gray-900 mb-2">
CSV-Datei hochladen
</p>
<p class="text-sm text-gray-600 mb-4">
Klicken Sie hier oder ziehen Sie eine CSV-Datei hierher
</p>
<p
v-if="bulkSelectedFile"
class="text-sm text-primary-600 font-medium"
>
{{ bulkSelectedFile.name }}
</p>
</div>
<input
ref="bulkFileInput"
type="file"
accept=".csv"
@change="handleBulkFileSelect"
class="hidden"
/>
@change="handleBulkFileSelect"
>
</div>
<!-- CSV Format Info -->
<div class="bg-blue-50 border-l-4 border-blue-500 p-4 rounded-lg mb-6">
<h4 class="text-sm font-medium text-blue-800 mb-2">Erwartetes CSV-Format:</h4>
<h4 class="text-sm font-medium text-blue-800 mb-2">
Erwartetes CSV-Format:
</h4>
<div class="text-xs text-blue-700 space-y-1">
<p> Erste Zeile: Spaltenüberschriften (firstName, lastName, geburtsdatum, email, phone, address, notes)</p>
<p> <strong>Pflichtfelder:</strong> firstName, lastName, geburtsdatum</p>
@@ -438,65 +614,126 @@
</div>
<!-- Preview Section -->
<div v-if="bulkPreviewData.length > 0" class="mb-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Vorschau ({{ bulkPreviewData.length }} Einträge)</h3>
<div
v-if="bulkPreviewData.length > 0"
class="mb-6"
>
<h3 class="text-lg font-semibold text-gray-900 mb-4">
Vorschau ({{ bulkPreviewData.length }} Einträge)
</h3>
<div class="max-h-64 overflow-y-auto border border-gray-200 rounded-lg">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-50 sticky top-0">
<tr>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Vorname</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Nachname</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Geburtsdatum</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">E-Mail</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">
Vorname
</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">
Nachname
</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">
Geburtsdatum
</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">
E-Mail
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="(row, index) in bulkPreviewData.slice(0, 10)" :key="index" class="hover:bg-gray-50">
<td class="px-3 py-2">{{ row.firstName || '-' }}</td>
<td class="px-3 py-2">{{ row.lastName || '-' }}</td>
<td class="px-3 py-2">{{ row.geburtsdatum || '-' }}</td>
<td class="px-3 py-2">{{ row.email || '-' }}</td>
<tr
v-for="(row, index) in bulkPreviewData.slice(0, 10)"
:key="index"
class="hover:bg-gray-50"
>
<td class="px-3 py-2">
{{ row.firstName || '-' }}
</td>
<td class="px-3 py-2">
{{ row.lastName || '-' }}
</td>
<td class="px-3 py-2">
{{ row.geburtsdatum || '-' }}
</td>
<td class="px-3 py-2">
{{ row.email || '-' }}
</td>
</tr>
</tbody>
</table>
<div v-if="bulkPreviewData.length > 10" class="px-3 py-2 text-xs text-gray-500 bg-gray-50 text-center">
<div
v-if="bulkPreviewData.length > 10"
class="px-3 py-2 text-xs text-gray-500 bg-gray-50 text-center"
>
... und {{ bulkPreviewData.length - 10 }} weitere
</div>
</div>
</div>
<!-- Import Results -->
<div v-if="bulkImportResults" class="mb-6">
<div
v-if="bulkImportResults"
class="mb-6"
>
<div class="bg-gray-50 rounded-lg p-4">
<h3 class="text-lg font-semibold text-gray-900 mb-3">Import-Ergebnisse</h3>
<h3 class="text-lg font-semibold text-gray-900 mb-3">
Import-Ergebnisse
</h3>
<div class="grid grid-cols-3 gap-4 mb-4">
<div class="text-center">
<div class="text-2xl font-bold text-green-600">{{ bulkImportResults.summary.imported }}</div>
<div class="text-sm text-gray-600">Importiert</div>
<div class="text-2xl font-bold text-green-600">
{{ bulkImportResults.summary.imported }}
</div>
<div class="text-sm text-gray-600">
Importiert
</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-yellow-600">{{ bulkImportResults.summary.duplicates }}</div>
<div class="text-sm text-gray-600">Duplikate</div>
<div class="text-2xl font-bold text-yellow-600">
{{ bulkImportResults.summary.duplicates }}
</div>
<div class="text-sm text-gray-600">
Duplikate
</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-red-600">{{ bulkImportResults.summary.errors }}</div>
<div class="text-sm text-gray-600">Fehler</div>
<div class="text-2xl font-bold text-red-600">
{{ bulkImportResults.summary.errors }}
</div>
<div class="text-sm text-gray-600">
Fehler
</div>
</div>
</div>
<div v-if="bulkImportResults.results.duplicates.length > 0" class="mt-4">
<h4 class="text-sm font-medium text-gray-700 mb-2">Duplikate:</h4>
<div
v-if="bulkImportResults.results.duplicates.length > 0"
class="mt-4"
>
<h4 class="text-sm font-medium text-gray-700 mb-2">
Duplikate:
</h4>
<div class="text-xs text-gray-600 space-y-1 max-h-32 overflow-y-auto">
<div v-for="dup in bulkImportResults.results.duplicates" :key="dup.index">
<div
v-for="dup in bulkImportResults.results.duplicates"
:key="dup.index"
>
Zeile {{ dup.index }}: {{ dup.member.firstName }} {{ dup.member.lastName }} - {{ dup.reason }}
</div>
</div>
</div>
<div v-if="bulkImportResults.results.errors.length > 0" class="mt-4">
<h4 class="text-sm font-medium text-gray-700 mb-2">Fehler:</h4>
<div
v-if="bulkImportResults.results.errors.length > 0"
class="mt-4"
>
<h4 class="text-sm font-medium text-gray-700 mb-2">
Fehler:
</h4>
<div class="text-xs text-red-600 space-y-1 max-h-32 overflow-y-auto">
<div v-for="err in bulkImportResults.results.errors" :key="err.index">
<div
v-for="err in bulkImportResults.results.errors"
:key="err.index"
>
Zeile {{ err.index }}: {{ err.error }}
</div>
</div>
@@ -507,18 +744,22 @@
<div class="flex justify-end space-x-4 pt-4">
<button
type="button"
@click="closeBulkImportModal"
class="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
:disabled="isBulkImporting"
@click="closeBulkImportModal"
>
Schließen
</button>
<button
@click="processBulkImport"
:disabled="!bulkPreviewData.length || isBulkImporting"
class="px-6 py-2 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors flex items-center disabled:bg-gray-400"
@click="processBulkImport"
>
<Loader2 v-if="isBulkImporting" :size="20" class="animate-spin mr-2" />
<Loader2
v-if="isBulkImporting"
:size="20"
class="animate-spin mr-2"
/>
<span>{{ isBulkImporting ? 'Importiert...' : 'Importieren' }}</span>
</button>
</div>

View File

@@ -10,21 +10,33 @@
</div>
<button
v-if="canWrite"
@click="openAddModal"
class="flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors"
@click="openAddModal"
>
<Plus :size="20" class="mr-2" />
<Plus
:size="20"
class="mr-2"
/>
News erstellen
</button>
</div>
<!-- Loading State -->
<div v-if="isLoading" class="flex items-center justify-center py-12">
<Loader2 :size="40" class="animate-spin text-primary-600" />
<div
v-if="isLoading"
class="flex items-center justify-center py-12"
>
<Loader2
:size="40"
class="animate-spin text-primary-600"
/>
</div>
<!-- News List -->
<div v-else class="space-y-6">
<div
v-else
class="space-y-6"
>
<article
v-for="item in news"
:key="item.id"
@@ -40,52 +52,76 @@
v-if="item.isPublic"
class="px-3 py-1 bg-blue-100 text-blue-800 text-xs font-semibold rounded-full flex items-center"
>
<Globe :size="14" class="mr-1" />
<Globe
:size="14"
class="mr-1"
/>
Öffentlich
</span>
<span
v-if="item.isHidden"
class="px-3 py-1 bg-yellow-100 text-yellow-800 text-xs font-semibold rounded-full flex items-center"
>
<EyeOff :size="14" class="mr-1" />
<EyeOff
:size="14"
class="mr-1"
/>
Ausgeblendet
</span>
<span
v-if="item.expiresAt && isExpired(item.expiresAt)"
class="px-3 py-1 bg-red-100 text-red-800 text-xs font-semibold rounded-full flex items-center"
>
<Calendar :size="14" class="mr-1" />
<Calendar
:size="14"
class="mr-1"
/>
Abgelaufen
</span>
</div>
<div class="flex items-center text-sm text-gray-500 space-x-4">
<div class="flex items-center">
<User :size="16" class="mr-1" />
<User
:size="16"
class="mr-1"
/>
{{ item.author }}
</div>
<div class="flex items-center">
<Calendar :size="16" class="mr-1" />
<Calendar
:size="16"
class="mr-1"
/>
{{ formatDate(item.created) }}
</div>
<div v-if="item.updated !== item.created" class="flex items-center">
<Edit :size="16" class="mr-1" />
<div
v-if="item.updated !== item.created"
class="flex items-center"
>
<Edit
:size="16"
class="mr-1"
/>
Aktualisiert: {{ formatDate(item.updated) }}
</div>
</div>
</div>
<div v-if="canWrite" class="flex space-x-2 ml-4">
<div
v-if="canWrite"
class="flex space-x-2 ml-4"
>
<button
@click="openEditModal(item)"
class="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
title="Bearbeiten"
@click="openEditModal(item)"
>
<Edit :size="20" />
</button>
<button
@click="confirmDelete(item)"
class="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="Löschen"
@click="confirmDelete(item)"
>
<Trash2 :size="20" />
</button>
@@ -97,10 +133,21 @@
</div>
</article>
<div v-if="news.length === 0" class="text-center py-12">
<Newspaper :size="48" class="mx-auto text-gray-400 mb-4" />
<p class="text-gray-500 text-lg">Noch keine News vorhanden.</p>
<p v-if="canWrite" class="text-gray-400 text-sm mt-2">
<div
v-if="news.length === 0"
class="text-center py-12"
>
<Newspaper
:size="48"
class="mx-auto text-gray-400 mb-4"
/>
<p class="text-gray-500 text-lg">
Noch keine News vorhanden.
</p>
<p
v-if="canWrite"
class="text-gray-400 text-sm mt-2"
>
Klicken Sie auf "News erstellen", um die erste News zu veröffentlichen.
</p>
</div>
@@ -117,7 +164,10 @@
{{ editingNews ? 'News bearbeiten' : 'News erstellen' }}
</h2>
<form @submit.prevent="saveNews" class="space-y-4">
<form
class="space-y-4"
@submit.prevent="saveNews"
>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Titel *</label>
<input
@@ -126,7 +176,7 @@
required
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
:disabled="isSaving"
/>
>
</div>
<div>
@@ -147,10 +197,16 @@
type="checkbox"
class="w-5 h-5 text-primary-600 border-gray-300 rounded focus:ring-primary-500"
:disabled="isSaving"
/>
<label for="isPublic" class="text-sm font-medium text-gray-900 cursor-pointer flex-1">
>
<label
for="isPublic"
class="text-sm font-medium text-gray-900 cursor-pointer flex-1"
>
<div class="flex items-center">
<Globe :size="18" class="mr-2 text-blue-600" />
<Globe
:size="18"
class="mr-2 text-blue-600"
/>
<span>Öffentliche News (auf Startseite anzeigen)</span>
</div>
<p class="text-xs text-gray-600 mt-1 ml-6">
@@ -159,7 +215,10 @@
</label>
</div>
<div v-if="formData.isPublic" class="space-y-4">
<div
v-if="formData.isPublic"
class="space-y-4"
>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Ablaufdatum (optional)</label>
<input
@@ -167,7 +226,7 @@
type="datetime-local"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
:disabled="isSaving"
/>
>
<p class="text-xs text-gray-600 mt-1">
Nach diesem Datum wird die News automatisch nicht mehr auf der Startseite angezeigt.
</p>
@@ -180,10 +239,16 @@
type="checkbox"
class="w-5 h-5 text-primary-600 border-gray-300 rounded focus:ring-primary-500"
:disabled="isSaving"
/>
<label for="isHidden" class="text-sm font-medium text-gray-900 cursor-pointer flex-1">
>
<label
for="isHidden"
class="text-sm font-medium text-gray-900 cursor-pointer flex-1"
>
<div class="flex items-center">
<EyeOff :size="18" class="mr-2 text-yellow-600" />
<EyeOff
:size="18"
class="mr-2 text-yellow-600"
/>
<span>News ausblenden</span>
</div>
<p class="text-xs text-gray-600 mt-1 ml-6">
@@ -193,17 +258,23 @@
</div>
</div>
<div v-if="errorMessage" class="flex items-center p-3 rounded-md bg-red-50 text-red-700 text-sm">
<AlertCircle :size="20" class="mr-2" />
<div
v-if="errorMessage"
class="flex items-center p-3 rounded-md bg-red-50 text-red-700 text-sm"
>
<AlertCircle
:size="20"
class="mr-2"
/>
{{ errorMessage }}
</div>
<div class="flex justify-end space-x-4 pt-4">
<button
type="button"
@click="closeModal"
class="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
:disabled="isSaving"
@click="closeModal"
>
Abbrechen
</button>
@@ -212,7 +283,11 @@
class="px-6 py-2 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors flex items-center"
:disabled="isSaving"
>
<Loader2 v-if="isSaving" :size="20" class="animate-spin mr-2" />
<Loader2
v-if="isSaving"
:size="20"
class="animate-spin mr-2"
/>
<span>{{ isSaving ? 'Speichert...' : 'Speichern' }}</span>
</button>
</div>

View File

@@ -8,15 +8,28 @@
<div class="bg-white rounded-xl shadow-lg p-8 border border-gray-100">
<!-- Loading State -->
<div v-if="isLoading" class="flex items-center justify-center py-12">
<Loader2 :size="40" class="animate-spin text-primary-600" />
<div
v-if="isLoading"
class="flex items-center justify-center py-12"
>
<Loader2
:size="40"
class="animate-spin text-primary-600"
/>
</div>
<!-- Profile Form -->
<form v-else @submit.prevent="handleSave" class="space-y-6">
<form
v-else
class="space-y-6"
@submit.prevent="handleSave"
>
<!-- Name -->
<div>
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">
<label
for="name"
class="block text-sm font-medium text-gray-700 mb-2"
>
Name
</label>
<input
@@ -26,12 +39,15 @@
required
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
:disabled="isSaving"
/>
>
</div>
<!-- E-Mail -->
<div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">
<label
for="email"
class="block text-sm font-medium text-gray-700 mb-2"
>
E-Mail-Adresse
</label>
<input
@@ -41,12 +57,15 @@
required
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
:disabled="isSaving"
/>
>
</div>
<!-- Telefon -->
<div>
<label for="phone" class="block text-sm font-medium text-gray-700 mb-2">
<label
for="phone"
class="block text-sm font-medium text-gray-700 mb-2"
>
Telefonnummer
</label>
<input
@@ -55,16 +74,21 @@
type="tel"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
:disabled="isSaving"
/>
>
</div>
<!-- Passwort ändern -->
<div class="border-t border-gray-200 pt-6 mt-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Passwort ändern</h3>
<h3 class="text-lg font-semibold text-gray-900 mb-4">
Passwort ändern
</h3>
<div class="space-y-4">
<div>
<label for="currentPassword" class="block text-sm font-medium text-gray-700 mb-2">
<label
for="currentPassword"
class="block text-sm font-medium text-gray-700 mb-2"
>
Aktuelles Passwort
</label>
<input
@@ -73,11 +97,14 @@
type="password"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
:disabled="isSaving"
/>
>
</div>
<div>
<label for="newPassword" class="block text-sm font-medium text-gray-700 mb-2">
<label
for="newPassword"
class="block text-sm font-medium text-gray-700 mb-2"
>
Neues Passwort
</label>
<input
@@ -86,11 +113,14 @@
type="password"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
:disabled="isSaving"
/>
>
</div>
<div>
<label for="confirmPassword" class="block text-sm font-medium text-gray-700 mb-2">
<label
for="confirmPassword"
class="block text-sm font-medium text-gray-700 mb-2"
>
Passwort bestätigen
</label>
<input
@@ -99,19 +129,31 @@
type="password"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
:disabled="isSaving"
/>
>
</div>
</div>
</div>
<!-- Error/Success Messages -->
<div v-if="errorMessage" class="flex items-center p-3 rounded-md bg-red-50 text-red-700 text-sm">
<AlertCircle :size="20" class="mr-2" />
<div
v-if="errorMessage"
class="flex items-center p-3 rounded-md bg-red-50 text-red-700 text-sm"
>
<AlertCircle
:size="20"
class="mr-2"
/>
{{ errorMessage }}
</div>
<div v-if="successMessage" class="flex items-center p-3 rounded-md bg-green-50 text-green-700 text-sm">
<Check :size="20" class="mr-2" />
<div
v-if="successMessage"
class="flex items-center p-3 rounded-md bg-green-50 text-green-700 text-sm"
>
<Check
:size="20"
class="mr-2"
/>
{{ successMessage }}
</div>
@@ -119,9 +161,9 @@
<div class="flex justify-end space-x-4">
<button
type="button"
@click="loadProfile"
class="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
:disabled="isSaving"
@click="loadProfile"
>
Zurücksetzen
</button>
@@ -130,7 +172,11 @@
class="px-6 py-2 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors flex items-center"
:disabled="isSaving"
>
<Loader2 v-if="isSaving" :size="20" class="animate-spin mr-2" />
<Loader2
v-if="isSaving"
:size="20"
class="animate-spin mr-2"
/>
<span>{{ isSaving ? 'Speichert...' : 'Speichern' }}</span>
</button>
</div>