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
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 54s
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user