Implement member management enhancements; add bulk import functionality and duplicate checking based on geburtsdatum. Update API to support new fields and improve error handling for member data submissions. Refactor member-related components for better user experience and data validation.
This commit is contained in:
@@ -59,9 +59,10 @@ export default defineEventHandler(async (event) => {
|
||||
maxAge: 60 * 60 * 24 * 7 // 7 days
|
||||
})
|
||||
|
||||
// Return user data (without password)
|
||||
// Return user data (without password) and token for API usage
|
||||
return {
|
||||
success: true,
|
||||
token: token, // Token auch im Body für externe API-Clients
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
|
||||
@@ -3,12 +3,21 @@ import { saveMember } from '../utils/members.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const token = getCookie(event, 'auth_token')
|
||||
// Support both Cookie and Authorization Header
|
||||
let token = getCookie(event, 'auth_token')
|
||||
|
||||
// If no cookie token, try Authorization header (Bearer token)
|
||||
if (!token) {
|
||||
const authHeader = getHeader(event, 'authorization')
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
token = authHeader.substring(7)
|
||||
}
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Nicht authentifiziert.'
|
||||
message: 'Nicht authentifiziert. Bitte Token im Cookie oder Authorization-Header bereitstellen.'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,16 +32,23 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
const user = await getUserById(decoded.id)
|
||||
|
||||
// Only admin and vorstand can edit members
|
||||
if (!user || (user.role !== 'admin' && user.role !== 'vorstand')) {
|
||||
if (!user) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Benutzer nicht gefunden.'
|
||||
})
|
||||
}
|
||||
|
||||
// Only admin and vorstand can add/edit members
|
||||
if (user.role !== 'admin' && user.role !== 'vorstand') {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Keine Berechtigung zum Bearbeiten von Mitgliedern.'
|
||||
message: 'Keine Berechtigung zum Hinzufügen/Bearbeiten von Mitgliedern. Erforderlich: admin oder vorstand Rolle.'
|
||||
})
|
||||
}
|
||||
|
||||
const body = await readBody(event)
|
||||
const { id, firstName, lastName, email, phone, address, notes } = body
|
||||
const { id, firstName, lastName, geburtsdatum, email, phone, address, notes } = body
|
||||
|
||||
if (!firstName || !lastName) {
|
||||
throw createError({
|
||||
@@ -41,23 +57,51 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
}
|
||||
|
||||
await saveMember({
|
||||
id: id || undefined,
|
||||
firstName,
|
||||
lastName,
|
||||
email: email || '',
|
||||
phone: phone || '',
|
||||
address: address || '',
|
||||
notes: notes || ''
|
||||
})
|
||||
if (!geburtsdatum) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: 'Geburtsdatum ist erforderlich, um Duplikate zu vermeiden.'
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Mitglied erfolgreich gespeichert.'
|
||||
try {
|
||||
await saveMember({
|
||||
id: id || undefined,
|
||||
firstName,
|
||||
lastName,
|
||||
geburtsdatum: geburtsdatum || '',
|
||||
email: email || '',
|
||||
phone: phone || '',
|
||||
address: address || '',
|
||||
notes: notes || ''
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Mitglied erfolgreich gespeichert.'
|
||||
}
|
||||
} catch (memberError) {
|
||||
// Check if it's a duplicate error
|
||||
if (memberError.message && memberError.message.includes('existiert bereits')) {
|
||||
throw createError({
|
||||
statusCode: 409,
|
||||
message: memberError.message
|
||||
})
|
||||
}
|
||||
// Re-throw other errors
|
||||
throw memberError
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern des Mitglieds:', error)
|
||||
throw error
|
||||
// If it's already a createError, re-throw it
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
// Otherwise wrap it
|
||||
throw createError({
|
||||
statusCode: error.statusCode || 500,
|
||||
message: error.message || 'Fehler beim Speichern des Mitglieds.'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
192
server/api/members/bulk.post.js
Normal file
192
server/api/members/bulk.post.js
Normal file
@@ -0,0 +1,192 @@
|
||||
import { verifyToken, getUserById } from '../../utils/auth.js'
|
||||
import { readMembers, writeMembers, normalizeDate } from '../../utils/members.js'
|
||||
import { randomUUID } from 'crypto'
|
||||
|
||||
// Helper function to check for duplicates in a list (with optional exclude)
|
||||
function findDuplicateMemberInList(members, firstName, lastName, geburtsdatum, excludeId = null) {
|
||||
const normalizedFirstName = (firstName || '').trim().toLowerCase()
|
||||
const normalizedLastName = (lastName || '').trim().toLowerCase()
|
||||
const normalizedDate = normalizeDate(geburtsdatum)
|
||||
|
||||
return members.find(m => {
|
||||
if (excludeId && m.id === excludeId) return false
|
||||
const mFirstName = (m.firstName || '').trim().toLowerCase()
|
||||
const mLastName = (m.lastName || '').trim().toLowerCase()
|
||||
const mDate = normalizeDate(m.geburtsdatum)
|
||||
|
||||
return mFirstName === normalizedFirstName &&
|
||||
mLastName === normalizedLastName &&
|
||||
mDate === normalizedDate &&
|
||||
mDate !== ''
|
||||
})
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
// Support both Cookie and Authorization Header
|
||||
let token = getCookie(event, 'auth_token')
|
||||
|
||||
if (!token) {
|
||||
const authHeader = getHeader(event, 'authorization')
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
token = authHeader.substring(7)
|
||||
}
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Nicht authentifiziert. Bitte Token im Cookie oder Authorization-Header bereitstellen.'
|
||||
})
|
||||
}
|
||||
|
||||
const decoded = verifyToken(token)
|
||||
|
||||
if (!decoded) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Ungültiges Token.'
|
||||
})
|
||||
}
|
||||
|
||||
const user = await getUserById(decoded.id)
|
||||
|
||||
if (!user) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Benutzer nicht gefunden.'
|
||||
})
|
||||
}
|
||||
|
||||
// Only admin and vorstand can add members in bulk
|
||||
if (user.role !== 'admin' && user.role !== 'vorstand') {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Keine Berechtigung zum Bulk-Import von Mitgliedern. Erforderlich: admin oder vorstand Rolle.'
|
||||
})
|
||||
}
|
||||
|
||||
const body = await readBody(event)
|
||||
const { members: membersToImport } = body
|
||||
|
||||
if (!Array.isArray(membersToImport) || membersToImport.length === 0) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: 'Bitte senden Sie ein Array von Mitgliedern im Feld "members".'
|
||||
})
|
||||
}
|
||||
|
||||
// Validate all members before processing
|
||||
const validationErrors = []
|
||||
membersToImport.forEach((member, index) => {
|
||||
if (!member.firstName || !member.lastName) {
|
||||
validationErrors.push(`Zeile ${index + 1}: Vorname und Nachname sind erforderlich.`)
|
||||
}
|
||||
if (!member.geburtsdatum) {
|
||||
validationErrors.push(`Zeile ${index + 1}: Geburtsdatum ist erforderlich.`)
|
||||
}
|
||||
})
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: `Validierungsfehler:\n${validationErrors.join('\n')}`
|
||||
})
|
||||
}
|
||||
|
||||
// Read existing members
|
||||
const existingMembers = await readMembers()
|
||||
|
||||
const results = {
|
||||
success: [],
|
||||
duplicates: [],
|
||||
errors: []
|
||||
}
|
||||
|
||||
// Process each member
|
||||
for (let i = 0; i < membersToImport.length; i++) {
|
||||
const memberData = membersToImport[i]
|
||||
|
||||
try {
|
||||
// Check for duplicates in existing members
|
||||
const duplicateInExisting = findDuplicateMemberInList(
|
||||
existingMembers,
|
||||
memberData.firstName,
|
||||
memberData.lastName,
|
||||
memberData.geburtsdatum
|
||||
)
|
||||
|
||||
if (duplicateInExisting) {
|
||||
results.duplicates.push({
|
||||
index: i + 1,
|
||||
member: memberData,
|
||||
reason: `Existiert bereits (ID: ${duplicateInExisting.id})`
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for duplicates within the import batch
|
||||
const duplicateInBatch = findDuplicateMemberInList(
|
||||
membersToImport.slice(0, i),
|
||||
memberData.firstName,
|
||||
memberData.lastName,
|
||||
memberData.geburtsdatum
|
||||
)
|
||||
|
||||
if (duplicateInBatch) {
|
||||
results.duplicates.push({
|
||||
index: i + 1,
|
||||
member: memberData,
|
||||
reason: 'Duplikat innerhalb des Imports'
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Add new member
|
||||
const newMember = {
|
||||
...memberData,
|
||||
id: memberData.id || randomUUID()
|
||||
}
|
||||
|
||||
existingMembers.push(newMember)
|
||||
results.success.push({
|
||||
index: i + 1,
|
||||
member: newMember
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
results.errors.push({
|
||||
index: i + 1,
|
||||
member: memberData,
|
||||
error: error.message || 'Unbekannter Fehler'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Save all successfully imported members
|
||||
if (results.success.length > 0) {
|
||||
await writeMembers(existingMembers)
|
||||
}
|
||||
|
||||
return {
|
||||
success: results.success.length > 0,
|
||||
summary: {
|
||||
total: membersToImport.length,
|
||||
imported: results.success.length,
|
||||
duplicates: results.duplicates.length,
|
||||
errors: results.errors.length
|
||||
},
|
||||
results: results
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Bulk-Import von Mitgliedern:', error)
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
throw createError({
|
||||
statusCode: error.statusCode || 500,
|
||||
message: error.message || 'Fehler beim Bulk-Import von Mitgliedern.'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -106,6 +106,37 @@ export async function getMemberById(id) {
|
||||
return members.find(m => m.id === id)
|
||||
}
|
||||
|
||||
// Normalize date string for comparison (handles different date formats)
|
||||
export function normalizeDate(dateString) {
|
||||
if (!dateString) return ''
|
||||
// Try to parse and normalize to ISO format (YYYY-MM-DD)
|
||||
try {
|
||||
const date = new Date(dateString)
|
||||
if (isNaN(date.getTime())) return dateString.trim()
|
||||
return date.toISOString().split('T')[0]
|
||||
} catch (e) {
|
||||
return dateString.trim()
|
||||
}
|
||||
}
|
||||
|
||||
// Check for duplicate member based on firstName, lastName, and geburtsdatum
|
||||
function findDuplicateMember(members, firstName, lastName, geburtsdatum) {
|
||||
const normalizedFirstName = (firstName || '').trim().toLowerCase()
|
||||
const normalizedLastName = (lastName || '').trim().toLowerCase()
|
||||
const normalizedDate = normalizeDate(geburtsdatum)
|
||||
|
||||
return members.find(m => {
|
||||
const mFirstName = (m.firstName || '').trim().toLowerCase()
|
||||
const mLastName = (m.lastName || '').trim().toLowerCase()
|
||||
const mDate = normalizeDate(m.geburtsdatum)
|
||||
|
||||
return mFirstName === normalizedFirstName &&
|
||||
mLastName === normalizedLastName &&
|
||||
mDate === normalizedDate &&
|
||||
mDate !== '' // Only match if date is provided for both
|
||||
})
|
||||
}
|
||||
|
||||
// Add or update manual member
|
||||
export async function saveMember(memberData) {
|
||||
const members = await readMembers()
|
||||
@@ -114,11 +145,37 @@ export async function saveMember(memberData) {
|
||||
// Update existing
|
||||
const index = members.findIndex(m => m.id === memberData.id)
|
||||
if (index !== -1) {
|
||||
// Check for duplicate (excluding current member)
|
||||
const duplicate = findDuplicateMember(
|
||||
members.filter(m => m.id !== memberData.id),
|
||||
memberData.firstName,
|
||||
memberData.lastName,
|
||||
memberData.geburtsdatum
|
||||
)
|
||||
|
||||
if (duplicate) {
|
||||
throw new Error('Ein Mitglied mit diesem Namen und Geburtsdatum existiert bereits.')
|
||||
}
|
||||
|
||||
members[index] = { ...members[index], ...memberData }
|
||||
} else {
|
||||
throw new Error('Mitglied nicht gefunden')
|
||||
}
|
||||
} else {
|
||||
// Add new - check for duplicate first
|
||||
if (memberData.firstName && memberData.lastName && memberData.geburtsdatum) {
|
||||
const duplicate = findDuplicateMember(
|
||||
members,
|
||||
memberData.firstName,
|
||||
memberData.lastName,
|
||||
memberData.geburtsdatum
|
||||
)
|
||||
|
||||
if (duplicate) {
|
||||
throw new Error('Ein Mitglied mit diesem Namen und Geburtsdatum existiert bereits.')
|
||||
}
|
||||
}
|
||||
|
||||
// Add new - use UUID for guaranteed uniqueness
|
||||
const newMember = {
|
||||
...memberData,
|
||||
|
||||
Reference in New Issue
Block a user