Add smart member list with manual+login merge and duplicate detection

This commit is contained in:
Torsten Schulz (local)
2025-10-21 14:35:21 +02:00
parent fa54e53820
commit b024bfe884
134 changed files with 15439 additions and 10 deletions

View File

@@ -0,0 +1,55 @@
import { verifyToken, getUserById } from '../utils/auth.js'
import { deleteMember } from '../utils/members.js'
export default defineEventHandler(async (event) => {
try {
const token = getCookie(event, 'auth_token')
if (!token) {
throw createError({
statusCode: 401,
message: 'Nicht authentifiziert.'
})
}
const decoded = verifyToken(token)
if (!decoded) {
throw createError({
statusCode: 401,
message: 'Ungültiges Token.'
})
}
const user = await getUserById(decoded.id)
// Only admin and vorstand can delete members
if (!user || (user.role !== 'admin' && user.role !== 'vorstand')) {
throw createError({
statusCode: 403,
message: 'Keine Berechtigung zum Löschen von Mitgliedern.'
})
}
const body = await readBody(event)
const { id } = body
if (!id) {
throw createError({
statusCode: 400,
message: 'Mitglieds-ID ist erforderlich.'
})
}
await deleteMember(id)
return {
success: true,
message: 'Mitglied erfolgreich gelöscht.'
}
} catch (error) {
console.error('Fehler beim Löschen des Mitglieds:', error)
throw error
}
})

113
server/api/members.get.js Normal file
View File

@@ -0,0 +1,113 @@
import { verifyToken } from '../utils/auth.js'
import { readMembers, readUsers } from '../utils/members.js'
export default defineEventHandler(async (event) => {
try {
const token = getCookie(event, 'auth_token')
if (!token) {
throw createError({
statusCode: 401,
message: 'Nicht authentifiziert.'
})
}
const decoded = verifyToken(token)
if (!decoded) {
throw createError({
statusCode: 401,
message: 'Ungültiges Token.'
})
}
// Get manual members and registered users
const manualMembers = await readMembers()
const registeredUsers = await readUsers()
// Merge members: combine manual + registered, detect duplicates
const mergedMembers = []
const processedEmails = new Set()
const processedNames = new Set()
// First, add all manual members
for (const member of manualMembers) {
const normalizedEmail = member.email?.toLowerCase().trim() || ''
const normalizedName = member.name?.toLowerCase().trim() || ''
mergedMembers.push({
...member,
source: 'manual',
editable: true,
hasLogin: false
})
if (normalizedEmail) processedEmails.add(normalizedEmail)
if (normalizedName) processedNames.add(normalizedName)
}
// Then add registered users (only active ones)
for (const user of registeredUsers) {
if (!user.active) continue
const normalizedEmail = user.email?.toLowerCase().trim() || ''
const normalizedName = user.name?.toLowerCase().trim() || ''
// Check if this user matches an existing manual member
let matchedManualIndex = -1
// Try to match by email first
if (normalizedEmail) {
matchedManualIndex = mergedMembers.findIndex(
m => m.source === 'manual' && m.email?.toLowerCase().trim() === normalizedEmail
)
}
// If no email match, try name
if (matchedManualIndex === -1 && normalizedName) {
matchedManualIndex = mergedMembers.findIndex(
m => m.source === 'manual' && m.name?.toLowerCase().trim() === normalizedName
)
}
if (matchedManualIndex !== -1) {
// Merge with existing manual member
mergedMembers[matchedManualIndex] = {
...mergedMembers[matchedManualIndex],
hasLogin: true,
loginEmail: user.email,
loginRole: user.role,
lastLogin: user.lastLogin
}
} else {
// Add as new member (from login system)
mergedMembers.push({
id: user.id,
name: user.name,
email: user.email,
phone: user.phone || '',
address: '',
notes: `Rolle: ${user.role}`,
source: 'login',
editable: false,
hasLogin: true,
loginEmail: user.email,
loginRole: user.role,
lastLogin: user.lastLogin
})
}
}
// Sort by name
mergedMembers.sort((a, b) => a.name.localeCompare(b.name))
return {
success: true,
members: mergedMembers
}
} catch (error) {
console.error('Fehler beim Abrufen der Mitgliederliste:', error)
throw error
}
})

View File

@@ -0,0 +1,62 @@
import { verifyToken, getUserById } from '../utils/auth.js'
import { saveMember } from '../utils/members.js'
export default defineEventHandler(async (event) => {
try {
const token = getCookie(event, 'auth_token')
if (!token) {
throw createError({
statusCode: 401,
message: 'Nicht authentifiziert.'
})
}
const decoded = verifyToken(token)
if (!decoded) {
throw createError({
statusCode: 401,
message: 'Ungültiges Token.'
})
}
const user = await getUserById(decoded.id)
// Only admin and vorstand can edit members
if (!user || (user.role !== 'admin' && user.role !== 'vorstand')) {
throw createError({
statusCode: 403,
message: 'Keine Berechtigung zum Bearbeiten von Mitgliedern.'
})
}
const body = await readBody(event)
const { id, name, email, phone, address, notes } = body
if (!name) {
throw createError({
statusCode: 400,
message: 'Name ist erforderlich.'
})
}
await saveMember({
id: id || undefined,
name,
email: email || '',
phone: phone || '',
address: address || '',
notes: notes || ''
})
return {
success: true,
message: 'Mitglied erfolgreich gespeichert.'
}
} catch (error) {
console.error('Fehler beim Speichern des Mitglieds:', error)
throw error
}
})

View File

@@ -1,12 +1,18 @@
[
{
"id": "1",
"name": "Admin",
"email": "admin@harheimertc.de",
"phone": "069-12345678",
"role": "admin",
"memberSince": "2020-01-01",
"active": true
"id": "m1",
"name": "Max Mustermann",
"email": "max@example.com",
"phone": "069 123456",
"address": "Musterstraße 1, 60437 Frankfurt",
"notes": "Herren 1"
},
{
"id": "m2",
"name": "Anna Schmidt",
"email": "",
"phone": "069 234567",
"address": "Hauptstraße 5, 60437 Frankfurt",
"notes": "Damen"
}
]

97
server/utils/members.js Normal file
View File

@@ -0,0 +1,97 @@
import { promises as fs } from 'fs'
import path from 'path'
// Handle both dev and production paths
const getDataPath = (filename) => {
const cwd = process.cwd()
// In production (.output/server), working dir is .output
if (cwd.endsWith('.output')) {
return path.join(cwd, '../server/data', filename)
}
// In development, working dir is project root
return path.join(cwd, 'server/data', filename)
}
const MEMBERS_FILE = getDataPath('members.json')
const USERS_FILE = getDataPath('users.json')
// Read manual members from file
export async function readMembers() {
try {
const data = await fs.readFile(MEMBERS_FILE, 'utf-8')
return JSON.parse(data)
} catch (error) {
if (error.code === 'ENOENT') {
return []
}
console.error('Fehler beim Lesen der Mitgliederdaten:', error)
return []
}
}
// Write manual members to file
export async function writeMembers(members) {
try {
await fs.writeFile(MEMBERS_FILE, JSON.stringify(members, null, 2), 'utf-8')
return true
} catch (error) {
console.error('Fehler beim Schreiben der Mitgliederdaten:', error)
return false
}
}
// Read registered users from file
export async function readUsers() {
try {
const data = await fs.readFile(USERS_FILE, 'utf-8')
return JSON.parse(data)
} catch (error) {
if (error.code === 'ENOENT') {
return []
}
console.error('Fehler beim Lesen der Benutzerdaten:', error)
return []
}
}
// Get member by ID
export async function getMemberById(id) {
const members = await readMembers()
return members.find(m => m.id === id)
}
// Add or update manual member
export async function saveMember(memberData) {
const members = await readMembers()
if (memberData.id) {
// Update existing
const index = members.findIndex(m => m.id === memberData.id)
if (index !== -1) {
members[index] = { ...members[index], ...memberData }
} else {
throw new Error('Mitglied nicht gefunden')
}
} else {
// Add new
const newMember = {
id: `m${Date.now()}`,
...memberData
}
members.push(newMember)
}
await writeMembers(members)
return true
}
// Delete manual member
export async function deleteMember(id) {
const members = await readMembers()
const filtered = members.filter(m => m.id !== id)
await writeMembers(filtered)
return true
}