Files
harheimertc/components/cms/CmsMitglieder.vue
Torsten Schulz (local) 905e02debf Update CMS navigation links and remove membership application page
This commit modifies the Navigation component and the CMS index page to replace the "Mitglieder" link with "Mitgliederverwaltung" and updates the corresponding route. Additionally, it removes the outdated "mitgliedschaftsantraege" page, streamlining the CMS structure and improving user navigation.
2026-02-09 09:58:46 +01:00

937 lines
34 KiB
Vue

<template>
<div>
<div class="flex justify-between items-center mb-6">
<div>
<h2 class="text-2xl sm:text-3xl font-display font-bold text-gray-900 mb-2">
Mitgliederliste
</h2>
<div class="w-24 h-1 bg-primary-600 mb-4" />
</div>
<div class="flex items-center space-x-3">
<button
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"
/>
{{ viewMode === 'cards' ? 'Tabelle' : 'Karten' }}
</button>
<button
v-if="canEdit"
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"
/>
</svg>
Bulk-Import
</button>
<button
v-if="canEdit"
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"
/>
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>
<!-- Table View -->
<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>
</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"
>
<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>
</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"
>
{{ member.email }}
</a>
<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>
</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"
>
{{ member.phone }}
</a>
<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>
</td>
<td class="px-4 py-3 whitespace-nowrap">
<button
v-if="canEdit"
:class="[
'px-2 py-1 text-xs font-medium rounded-full transition-colors',
member.isMannschaftsspieler
? 'bg-blue-100 text-blue-800 hover:bg-blue-200'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
]"
title="Klicken zum Umschalten"
@click="toggleMannschaftsspieler(member)"
>
{{ member.isMannschaftsspieler ? 'Ja' : 'Nein' }}
</button>
<span
v-else
:class="[
'px-2 py-1 text-xs font-medium rounded-full',
member.isMannschaftsspieler
? 'bg-blue-100 text-blue-800'
: 'bg-gray-100 text-gray-600'
]"
>
{{ member.isMannschaftsspieler ? 'Ja' : 'Nein' }}
</span>
</td>
<td class="px-4 py-3 whitespace-nowrap">
<div class="flex items-center space-x-2">
<span
v-if="member.hasLogin"
class="px-2 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-full"
>
Login
</span>
<span
:class="member.source === 'manual' ? 'bg-blue-100 text-blue-800' : 'bg-purple-100 text-purple-800'"
class="px-2 py-1 text-xs font-medium rounded-full"
>
{{ member.source === 'manual' ? 'Manuell' : 'System' }}
</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"
>
<button
class="text-blue-600 hover:text-blue-900"
title="Bearbeiten"
@click="openEditModal(member)"
>
<Edit :size="18" />
</button>
<button
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>
</td>
</tr>
</tbody>
</table>
</div>
<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-for="member in members"
:key="member.id"
class="bg-white p-6 rounded-xl shadow-lg border border-gray-100"
>
<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>
<span
v-if="member.hasLogin"
class="ml-3 px-2 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-full"
>
Hat Login
</span>
<span
v-if="member.source === 'manual'"
class="ml-2 px-2 py-1 bg-blue-100 text-blue-800 text-xs font-medium rounded-full"
>
Manuell
</span>
<span
v-else
class="ml-2 px-2 py-1 bg-purple-100 text-purple-800 text-xs font-medium rounded-full"
>
Aus Login-System
</span>
<button
v-if="canEdit"
:class="[
'ml-2 px-2 py-1 text-xs font-medium rounded-full transition-colors',
member.isMannschaftsspieler
? 'bg-blue-100 text-blue-800 hover:bg-blue-200'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
]"
title="Klicken zum Umschalten"
@click="toggleMannschaftsspieler(member)"
>
Mannschaftsspieler: {{ member.isMannschaftsspieler ? 'Ja' : 'Nein' }}
</button>
<span
v-else
:class="[
'ml-2 px-2 py-1 text-xs font-medium rounded-full',
member.isMannschaftsspieler
? 'bg-blue-100 text-blue-800'
: 'bg-gray-100 text-gray-600'
]"
>
Mannschaftsspieler: {{ member.isMannschaftsspieler ? 'Ja' : 'Nein' }}
</span>
</div>
<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>
<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"
/>
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"
/>
<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"
/>
<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"
/>
Letzter Login: {{ formatDate(member.lastLogin) }}
</div>
</div>
</div>
<div
v-if="canEdit && member.editable"
class="flex space-x-2 ml-4"
>
<button
class="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
title="Bearbeiten"
@click="openEditModal(member)"
>
<Edit :size="20" />
</button>
<button
class="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="Löschen"
@click="confirmDelete(member)"
>
<Trash2 :size="20" />
</button>
</div>
</div>
</div>
<div
v-if="members.length === 0"
class="text-center py-12 text-gray-500"
>
Keine Mitglieder gefunden.
</div>
</div>
<!-- Edit/Add Modal -->
<div
v-if="showModal"
class="fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4"
@click.self="closeModal"
>
<div class="bg-white rounded-xl shadow-2xl max-w-2xl w-full p-8">
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6">
{{ editingMember ? 'Mitglied bearbeiten' : 'Mitglied hinzufügen' }}
</h2>
<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>
<input
v-model="formData.firstName"
type="text"
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>
<input
v-model="formData.lastName"
type="text"
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>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Geburtsdatum *</label>
<input
v-model="formData.geburtsdatum"
type="date"
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>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">E-Mail</label>
<input
v-model="formData.email"
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>
<label class="block text-sm font-medium text-gray-700 mb-2">Telefon</label>
<input
v-model="formData.phone"
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>
<label class="block text-sm font-medium text-gray-700 mb-2">Adresse</label>
<input
v-model="formData.address"
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>
<label class="block text-sm font-medium text-gray-700 mb-2">Notizen</label>
<textarea
v-model="formData.notes"
rows="3"
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 class="flex items-center">
<input
id="isMannschaftsspieler"
v-model="formData.isMannschaftsspieler"
type="checkbox"
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"
>
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"
/>
{{ errorMessage }}
</div>
<div class="flex justify-end space-x-4 pt-4">
<button
type="button"
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>
<button
type="submit"
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"
/>
<span>{{ isSaving ? 'Speichert...' : 'Speichern' }}</span>
</button>
</div>
</form>
</div>
</div>
<!-- Bulk Import Modal -->
<div
v-if="showBulkImportModal"
class="fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4"
@click.self="closeBulkImportModal"
>
<div class="bg-white rounded-xl shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-y-auto p-8">
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6">
Bulk-Import von Mitgliedern
</h2>
<!-- CSV Upload Section -->
<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"
>
<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>
</div>
<input
ref="bulkFileInput"
type="file"
accept=".csv"
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>
<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>
<p> <strong>Geburtsdatum:</strong> Format YYYY-MM-DD (z.B. 1990-01-15)</p>
<p> Trennzeichen: Komma (,) oder Semikolon (;)</p>
</div>
</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 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>
</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>
</tbody>
</table>
<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 class="bg-gray-50 rounded-lg p-4">
<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>
<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>
<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>
</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 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">
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 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">
Zeile {{ err.index }}: {{ err.error }}
</div>
</div>
</div>
</div>
</div>
<div class="flex justify-end space-x-4 pt-4">
<button
type="button"
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
: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"
/>
<span>{{ isBulkImporting ? 'Importiert...' : 'Importieren' }}</span>
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { UserPlus, Mail, Phone, MapPin, FileText, Clock, Edit, Trash2, Loader2, AlertCircle, Table2, Grid3x3 } from 'lucide-vue-next'
const authStore = useAuthStore()
const isLoading = ref(true)
const isSaving = ref(false)
const members = ref([])
const showModal = ref(false)
const editingMember = ref(null)
const errorMessage = ref('')
const viewMode = ref('cards')
// Bulk import state
const showBulkImportModal = ref(false)
const bulkFileInput = ref(null)
const bulkSelectedFile = ref(null)
const bulkPreviewData = ref([])
const isBulkImporting = ref(false)
const bulkImportResults = ref(null)
const isDragOver = ref(false)
const formData = ref({
firstName: '',
lastName: '',
geburtsdatum: '',
email: '',
phone: '',
address: '',
notes: '',
isMannschaftsspieler: false
})
const canEdit = computed(() => {
return authStore.hasAnyRole('admin', 'vorstand')
})
const canViewContactData = computed(() => {
return authStore.hasRole('vorstand')
})
const loadMembers = async () => {
isLoading.value = true
try {
const response = await $fetch('/api/members')
members.value = response.members
} catch (error) {
console.error('Fehler beim Laden der Mitglieder:', error)
} finally {
isLoading.value = false
}
}
const openAddModal = () => {
editingMember.value = null
formData.value = { firstName: '', lastName: '', geburtsdatum: '', email: '', phone: '', address: '', notes: '', isMannschaftsspieler: false }
showModal.value = true
errorMessage.value = ''
}
const openEditModal = (member) => {
editingMember.value = member
formData.value = {
firstName: member.firstName || '',
lastName: member.lastName || '',
geburtsdatum: member.geburtsdatum || '',
email: member.email || '',
phone: member.phone || '',
address: member.address || '',
notes: member.notes || '',
isMannschaftsspieler: member.isMannschaftsspieler === true
}
showModal.value = true
errorMessage.value = ''
}
const closeModal = () => {
showModal.value = false
editingMember.value = null
errorMessage.value = ''
}
const saveMember = async () => {
isSaving.value = true
errorMessage.value = ''
try {
await $fetch('/api/members', {
method: 'POST',
body: { id: editingMember.value?.id, ...formData.value }
})
closeModal()
await loadMembers()
if (window.showSuccessModal) window.showSuccessModal('Erfolg', 'Mitglied erfolgreich gespeichert.')
} catch (error) {
console.error('Fehler beim Speichern:', error)
const errorMsg = error.data?.message || error.message || 'Fehler beim Speichern des Mitglieds.'
errorMessage.value = errorMsg
if ((error.statusCode === 409 || error.status === 409) && window.showErrorModal) {
window.showErrorModal('Duplikat gefunden', errorMsg)
}
} finally {
isSaving.value = false
}
}
const toggleMannschaftsspieler = async (member) => {
try {
const response = await $fetch('/api/members/toggle-mannschaftsspieler', {
method: 'POST',
body: { memberId: member.id }
})
member.isMannschaftsspieler = response.isMannschaftsspieler
await loadMembers()
} catch (error) {
console.error('Fehler beim Umschalten des Mannschaftsspieler-Status:', error)
if (window.showErrorModal) window.showErrorModal('Fehler', error.data?.message || 'Fehler beim Umschalten des Status.')
}
}
const confirmDelete = async (member) => {
window.showConfirmModal('Mitglied löschen', `Möchten Sie "${member.name}" wirklich löschen?`, async () => {
try {
await $fetch('/api/members', { method: 'DELETE', body: { id: member.id } })
await loadMembers()
window.showSuccessModal('Erfolg', 'Mitglied wurde erfolgreich gelöscht')
} catch (error) {
console.error('Fehler beim Löschen:', error)
window.showErrorModal('Fehler', 'Fehler beim Löschen des Mitglieds')
}
})
}
const formatDate = (dateString) => {
if (!dateString) return ''
return new Date(dateString).toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
}
// Bulk import functions
const triggerBulkFileInput = () => { bulkFileInput.value?.click() }
const handleBulkFileSelect = (event) => { const file = event.target.files?.[0]; if (file) processBulkCSV(file) }
const handleBulkFileDrop = (event) => { isDragOver.value = false; const file = event.dataTransfer?.files?.[0]; if (file && file.type === 'text/csv') processBulkCSV(file) }
const processBulkCSV = async (file) => {
bulkSelectedFile.value = file
bulkImportResults.value = null
try {
const text = await file.text()
const lines = text.split('\n').filter(line => line.trim() !== '')
if (lines.length < 2) { window.showErrorModal('Fehler', 'CSV-Datei muss mindestens eine Kopfzeile und eine Datenzeile enthalten'); return }
const parseCSVLine = (line) => {
const tabCount = (line.match(/\t/g) || []).length
const semicolonCount = (line.match(/;/g) || []).length
const delimiter = tabCount > semicolonCount ? '\t' : (semicolonCount > 0 ? ';' : ',')
return line.split(delimiter).map(value => value.trim().replace(/^"|"$/g, ''))
}
const headers = parseCSVLine(lines[0]).map(h => h.toLowerCase())
const firstNameIdx = headers.findIndex(h => h.includes('firstname') || h.includes('vorname'))
const lastNameIdx = headers.findIndex(h => h.includes('lastname') || h.includes('nachname'))
const geburtsdatumIdx = headers.findIndex(h => h.includes('geburtsdatum') || h.includes('birthdate') || h.includes('geburt'))
const emailIdx = headers.findIndex(h => h.includes('email') || h.includes('e-mail'))
const phoneIdx = headers.findIndex(h => h.includes('phone') || h.includes('telefon') || h.includes('tel'))
const addressIdx = headers.findIndex(h => h.includes('address') || h.includes('adresse'))
const notesIdx = headers.findIndex(h => h.includes('note') || h.includes('notiz') || h.includes('bemerkung'))
if (firstNameIdx === -1 || lastNameIdx === -1 || geburtsdatumIdx === -1) { window.showErrorModal('Fehler', 'CSV muss Spalten für firstName, lastName und geburtsdatum enthalten'); return }
bulkPreviewData.value = lines.slice(1).map((line) => {
const values = parseCSVLine(line)
return {
firstName: values[firstNameIdx] || '',
lastName: values[lastNameIdx] || '',
geburtsdatum: values[geburtsdatumIdx] || '',
email: emailIdx !== -1 ? (values[emailIdx] || '') : '',
phone: phoneIdx !== -1 ? (values[phoneIdx] || '') : '',
address: addressIdx !== -1 ? (values[addressIdx] || '') : '',
notes: notesIdx !== -1 ? (values[notesIdx] || '') : ''
}
}).filter(row => row.firstName && row.lastName && row.geburtsdatum)
} catch (error) {
console.error('Fehler beim Parsen der CSV:', error)
window.showErrorModal('Fehler', 'Fehler beim Lesen der CSV-Datei: ' + error.message)
}
}
const processBulkImport = async () => {
if (!bulkPreviewData.value.length) return
isBulkImporting.value = true
bulkImportResults.value = null
try {
const response = await $fetch('/api/members/bulk', { method: 'POST', body: { members: bulkPreviewData.value } })
bulkImportResults.value = response
if (response.summary.imported > 0) {
await loadMembers()
window.showSuccessModal('Import erfolgreich', `${response.summary.imported} Mitglieder wurden erfolgreich importiert.`)
}
} catch (error) {
console.error('Fehler beim Bulk-Import:', error)
window.showErrorModal('Import-Fehler', error.data?.message || error.message || 'Fehler beim Import')
} finally {
isBulkImporting.value = false
}
}
const closeBulkImportModal = () => {
showBulkImportModal.value = false
bulkSelectedFile.value = null
bulkPreviewData.value = []
bulkImportResults.value = null
isDragOver.value = false
if (bulkFileInput.value) bulkFileInput.value.value = ''
}
onMounted(() => { loadMembers() })
</script>