Add hall key feature to member management, including UI updates for displaying and editing hall key status. Update API to handle hall key data in member records.
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 1m6s

This commit is contained in:
Torsten Schulz (local)
2026-03-29 14:37:49 +02:00
parent 49e7255062
commit f7701d698f
5 changed files with 96 additions and 10 deletions

View File

@@ -85,6 +85,9 @@
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Mannschaft Mannschaft
</th> </th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
🔑
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status Status
</th> </th>
@@ -177,6 +180,15 @@
{{ member.isMannschaftsspieler ? 'Ja' : 'Nein' }} {{ member.isMannschaftsspieler ? 'Ja' : 'Nein' }}
</span> </span>
</td> </td>
<td class="px-4 py-3 whitespace-nowrap">
<span
:class="member.hasHallKey ? 'text-amber-600' : 'text-gray-300'"
:title="member.hasHallKey ? 'Hat Hallenschlüssel' : 'Hat keinen Hallenschlüssel'"
class="text-lg"
>
🔑
</span>
</td>
<td class="px-4 py-3 whitespace-nowrap"> <td class="px-4 py-3 whitespace-nowrap">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<span <span
@@ -250,6 +262,12 @@
<h3 class="text-xl font-semibold text-gray-900"> <h3 class="text-xl font-semibold text-gray-900">
{{ member.name }} {{ member.name }}
</h3> </h3>
<span
:class="member.hasHallKey ? 'ml-2 text-amber-600' : 'ml-2 text-gray-300'"
:title="member.hasHallKey ? 'Hat Hallenschlüssel' : 'Hat keinen Hallenschlüssel'"
>
🔑
</span>
<span <span
v-if="member.hasLogin" v-if="member.hasLogin"
class="ml-3 px-2 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-full" class="ml-3 px-2 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-full"
@@ -504,6 +522,22 @@
</label> </label>
</div> </div>
<div class="flex items-center">
<input
id="hasHallKey"
v-model="formData.hasHallKey"
type="checkbox"
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
:disabled="isSaving"
>
<label
for="hasHallKey"
class="ml-2 block text-sm font-medium text-gray-700"
>
Hat Hallenschlüssel
</label>
</div>
<div <div
v-if="errorMessage" v-if="errorMessage"
class="flex items-center p-3 rounded-md bg-red-50 text-red-700 text-sm" class="flex items-center p-3 rounded-md bg-red-50 text-red-700 text-sm"
@@ -752,7 +786,8 @@ const formData = ref({
phone: '', phone: '',
address: '', address: '',
notes: '', notes: '',
isMannschaftsspieler: false isMannschaftsspieler: false,
hasHallKey: false
}) })
const canEdit = computed(() => { const canEdit = computed(() => {
@@ -777,7 +812,7 @@ const loadMembers = async () => {
const openAddModal = () => { const openAddModal = () => {
editingMember.value = null editingMember.value = null
formData.value = { firstName: '', lastName: '', geburtsdatum: '', email: '', phone: '', address: '', notes: '', isMannschaftsspieler: false } formData.value = { firstName: '', lastName: '', geburtsdatum: '', email: '', phone: '', address: '', notes: '', isMannschaftsspieler: false, hasHallKey: false }
showModal.value = true showModal.value = true
errorMessage.value = '' errorMessage.value = ''
} }
@@ -792,7 +827,8 @@ const openEditModal = (member) => {
phone: member.phone || '', phone: member.phone || '',
address: member.address || '', address: member.address || '',
notes: member.notes || '', notes: member.notes || '',
isMannschaftsspieler: member.isMannschaftsspieler === true isMannschaftsspieler: member.isMannschaftsspieler === true,
hasHallKey: member.hasHallKey === true
} }
showModal.value = true showModal.value = true
errorMessage.value = '' errorMessage.value = ''

View File

@@ -96,6 +96,9 @@
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Mannschaft Mannschaft
</th> </th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
🔑
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status Status
</th> </th>
@@ -184,6 +187,15 @@
{{ member.isMannschaftsspieler ? 'Ja' : 'Nein' }} {{ member.isMannschaftsspieler ? 'Ja' : 'Nein' }}
</span> </span>
</td> </td>
<td class="px-4 py-3 whitespace-nowrap">
<span
:class="member.hasHallKey ? 'text-amber-600' : 'text-gray-300'"
:title="member.hasHallKey ? 'Hat Hallenschlüssel' : 'Hat keinen Hallenschlüssel'"
class="text-lg"
>
🔑
</span>
</td>
<td class="px-4 py-3 whitespace-nowrap"> <td class="px-4 py-3 whitespace-nowrap">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<span <span
@@ -265,6 +277,12 @@
🎂 {{ formatBirthday(member.birthday) }} 🎂 {{ formatBirthday(member.birthday) }}
</span> </span>
</h3> </h3>
<span
:class="member.hasHallKey ? 'ml-2 text-amber-600' : 'ml-2 text-gray-300'"
:title="member.hasHallKey ? 'Hat Hallenschlüssel' : 'Hat keinen Hallenschlüssel'"
>
🔑
</span>
<span <span
v-if="member.hasLogin" v-if="member.hasLogin"
class="ml-3 px-2 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-full" class="ml-3 px-2 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-full"
@@ -508,6 +526,22 @@
</label> </label>
</div> </div>
<div class="flex items-center">
<input
id="hasHallKey"
v-model="formData.hasHallKey"
type="checkbox"
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
:disabled="isSaving"
>
<label
for="hasHallKey"
class="ml-2 block text-sm font-medium text-gray-700"
>
Hat Hallenschlüssel
</label>
</div>
<div <div
v-if="errorMessage" v-if="errorMessage"
class="flex items-center p-3 rounded-md bg-red-50 text-red-700 text-sm" class="flex items-center p-3 rounded-md bg-red-50 text-red-700 text-sm"
@@ -876,7 +910,8 @@ const formData = ref({
phone: '', phone: '',
address: '', address: '',
notes: '', notes: '',
isMannschaftsspieler: false isMannschaftsspieler: false,
hasHallKey: false
}) })
const canEdit = computed(() => { const canEdit = computed(() => {
@@ -910,7 +945,8 @@ const openAddModal = () => {
phone: '', phone: '',
address: '', address: '',
notes: '', notes: '',
isMannschaftsspieler: false isMannschaftsspieler: false,
hasHallKey: false
} }
showModal.value = true showModal.value = true
errorMessage.value = '' errorMessage.value = ''
@@ -926,7 +962,8 @@ const openEditModal = (member) => {
phone: member.phone || '', phone: member.phone || '',
address: member.address || '', address: member.address || '',
notes: member.notes || '', notes: member.notes || '',
isMannschaftsspieler: member.isMannschaftsspieler === true isMannschaftsspieler: member.isMannschaftsspieler === true,
hasHallKey: member.hasHallKey === true
} }
showModal.value = true showModal.value = true
errorMessage.value = '' errorMessage.value = ''

View File

@@ -242,6 +242,7 @@ export default defineEventHandler(async (event) => {
loginRole: member.loginRole, loginRole: member.loginRole,
lastLogin: member.lastLogin, lastLogin: member.lastLogin,
isMannschaftsspieler: member.isMannschaftsspieler, isMannschaftsspieler: member.isMannschaftsspieler,
hasHallKey: member.hasHallKey === true || member.hasHallenschluessel === true,
notes: member.notes || '', notes: member.notes || '',
// Sichtbarkeits-Flags explizit mitgeben // Sichtbarkeits-Flags explizit mitgeben
showEmail: visibility.showEmail === undefined ? true : Boolean(visibility.showEmail), showEmail: visibility.showEmail === undefined ? true : Boolean(visibility.showEmail),

View File

@@ -48,7 +48,7 @@ export default defineEventHandler(async (event) => {
} }
const body = await readBody(event) const body = await readBody(event)
const { id, firstName, lastName, geburtsdatum, email, phone, address, notes, isMannschaftsspieler, active } = body const { id, firstName, lastName, geburtsdatum, email, phone, address, notes, isMannschaftsspieler, hasHallKey, hasHallenschluessel, active } = body
if (!firstName || !lastName) { if (!firstName || !lastName) {
throw createError({ throw createError({
@@ -75,6 +75,7 @@ export default defineEventHandler(async (event) => {
address: address || '', address: address || '',
notes: notes || '', notes: notes || '',
isMannschaftsspieler: isMannschaftsspieler === true || isMannschaftsspieler === 'true', isMannschaftsspieler: isMannschaftsspieler === true || isMannschaftsspieler === 'true',
hasHallKey: hasHallKey === true || hasHallKey === 'true' || hasHallenschluessel === true || hasHallenschluessel === 'true',
active: typeof active === 'boolean' ? active : true active: typeof active === 'boolean' ? active : true
}) })

View File

@@ -4,6 +4,7 @@ import { createEvent, mockSuccessReadBody } from './setup'
vi.mock('../server/utils/auth.js', () => ({ vi.mock('../server/utils/auth.js', () => ({
verifyToken: vi.fn(), verifyToken: vi.fn(),
getUserById: vi.fn(), getUserById: vi.fn(),
getUserFromToken: vi.fn(),
readUsers: vi.fn(), readUsers: vi.fn(),
readMembers: vi.fn(), readMembers: vi.fn(),
writeUsers: vi.fn(), writeUsers: vi.fn(),
@@ -24,6 +25,11 @@ vi.mock('../server/utils/auth.js', () => ({
if (!user) return false if (!user) return false
const userRoles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : []) const userRoles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : [])
return roles.some(r => userRoles.includes(r)) return roles.some(r => userRoles.includes(r))
}),
hasRole: vi.fn((user, role) => {
if (!user) return false
const userRoles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : [])
return userRoles.includes(role)
}) })
})) }))
@@ -58,16 +64,18 @@ describe('Members API Endpoints', () => {
const event = createEvent({ cookies: { auth_token: 'token' } }) const event = createEvent({ cookies: { auth_token: 'token' } })
authUtils.verifyToken.mockReturnValue({ id: '1' }) authUtils.verifyToken.mockReturnValue({ id: '1' })
memberUtils.readMembers.mockResolvedValue([ memberUtils.readMembers.mockResolvedValue([
{ id: 'm1', firstName: 'Anna', lastName: 'Muster', email: 'anna@club.de' } { id: 'm1', firstName: 'Anna', lastName: 'Muster', email: 'anna@club.de', hasHallKey: true, active: true }
]) ])
authUtils.readUsers.mockResolvedValue([ authUtils.readUsers.mockResolvedValue([
{ id: 'u1', name: 'Ben Nutzer', email: 'ben@club.de', role: 'mitglied', active: true } { id: 'u1', name: 'Ben Nutzer', email: 'ben@club.de', role: 'mitglied', active: true }
]) ])
authUtils.getUserFromToken.mockResolvedValue({ id: '1', role: 'mitglied' })
const response = await membersGetHandler(event) const response = await membersGetHandler(event)
expect(response.success).toBe(true) expect(response.success).toBe(true)
expect(response.members).toHaveLength(2) expect(response.members).toHaveLength(2)
expect(response.members[0]).toHaveProperty('hasHallKey', true)
}) })
}) })
@@ -76,7 +84,8 @@ describe('Members API Endpoints', () => {
firstName: 'Lisa', firstName: 'Lisa',
lastName: 'Beispiel', lastName: 'Beispiel',
geburtsdatum: '2000-01-01', geburtsdatum: '2000-01-01',
email: 'lisa@example.com' email: 'lisa@example.com',
hasHallKey: true
} }
it('verweigert Zugriff ohne Token', async () => { it('verweigert Zugriff ohne Token', async () => {
@@ -113,7 +122,9 @@ describe('Members API Endpoints', () => {
const response = await membersPostHandler(event) const response = await membersPostHandler(event)
expect(response.success).toBe(true) expect(response.success).toBe(true)
expect(memberUtils.saveMember).toHaveBeenCalled() expect(memberUtils.saveMember).toHaveBeenCalledWith(expect.objectContaining({
hasHallKey: true
}))
}) })
}) })