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">
Mannschaft
</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">
Status
</th>
@@ -177,6 +180,15 @@
{{ member.isMannschaftsspieler ? 'Ja' : 'Nein' }}
</span>
</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">
<div class="flex items-center space-x-2">
<span
@@ -250,6 +262,12 @@
<h3 class="text-xl font-semibold text-gray-900">
{{ member.name }}
</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
v-if="member.hasLogin"
class="ml-3 px-2 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-full"
@@ -504,6 +522,22 @@
</label>
</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
v-if="errorMessage"
class="flex items-center p-3 rounded-md bg-red-50 text-red-700 text-sm"
@@ -752,7 +786,8 @@ const formData = ref({
phone: '',
address: '',
notes: '',
isMannschaftsspieler: false
isMannschaftsspieler: false,
hasHallKey: false
})
const canEdit = computed(() => {
@@ -777,7 +812,7 @@ const loadMembers = async () => {
const openAddModal = () => {
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
errorMessage.value = ''
}
@@ -792,7 +827,8 @@ const openEditModal = (member) => {
phone: member.phone || '',
address: member.address || '',
notes: member.notes || '',
isMannschaftsspieler: member.isMannschaftsspieler === true
isMannschaftsspieler: member.isMannschaftsspieler === true,
hasHallKey: member.hasHallKey === true
}
showModal.value = true
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">
Mannschaft
</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">
Status
</th>
@@ -184,6 +187,15 @@
{{ member.isMannschaftsspieler ? 'Ja' : 'Nein' }}
</span>
</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">
<div class="flex items-center space-x-2">
<span
@@ -265,6 +277,12 @@
🎂 {{ formatBirthday(member.birthday) }}
</span>
</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
v-if="member.hasLogin"
class="ml-3 px-2 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-full"
@@ -508,6 +526,22 @@
</label>
</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
v-if="errorMessage"
class="flex items-center p-3 rounded-md bg-red-50 text-red-700 text-sm"
@@ -876,7 +910,8 @@ const formData = ref({
phone: '',
address: '',
notes: '',
isMannschaftsspieler: false
isMannschaftsspieler: false,
hasHallKey: false
})
const canEdit = computed(() => {
@@ -910,7 +945,8 @@ const openAddModal = () => {
phone: '',
address: '',
notes: '',
isMannschaftsspieler: false
isMannschaftsspieler: false,
hasHallKey: false
}
showModal.value = true
errorMessage.value = ''
@@ -926,7 +962,8 @@ const openEditModal = (member) => {
phone: member.phone || '',
address: member.address || '',
notes: member.notes || '',
isMannschaftsspieler: member.isMannschaftsspieler === true
isMannschaftsspieler: member.isMannschaftsspieler === true,
hasHallKey: member.hasHallKey === true
}
showModal.value = true
errorMessage.value = ''

View File

@@ -242,6 +242,7 @@ export default defineEventHandler(async (event) => {
loginRole: member.loginRole,
lastLogin: member.lastLogin,
isMannschaftsspieler: member.isMannschaftsspieler,
hasHallKey: member.hasHallKey === true || member.hasHallenschluessel === true,
notes: member.notes || '',
// Sichtbarkeits-Flags explizit mitgeben
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 { 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) {
throw createError({
@@ -75,6 +75,7 @@ export default defineEventHandler(async (event) => {
address: address || '',
notes: notes || '',
isMannschaftsspieler: isMannschaftsspieler === true || isMannschaftsspieler === 'true',
hasHallKey: hasHallKey === true || hasHallKey === 'true' || hasHallenschluessel === true || hasHallenschluessel === 'true',
active: typeof active === 'boolean' ? active : true
})

View File

@@ -4,6 +4,7 @@ import { createEvent, mockSuccessReadBody } from './setup'
vi.mock('../server/utils/auth.js', () => ({
verifyToken: vi.fn(),
getUserById: vi.fn(),
getUserFromToken: vi.fn(),
readUsers: vi.fn(),
readMembers: vi.fn(),
writeUsers: vi.fn(),
@@ -24,6 +25,11 @@ vi.mock('../server/utils/auth.js', () => ({
if (!user) return false
const userRoles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : [])
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' } })
authUtils.verifyToken.mockReturnValue({ id: '1' })
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([
{ 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)
expect(response.success).toBe(true)
expect(response.members).toHaveLength(2)
expect(response.members[0]).toHaveProperty('hasHallKey', true)
})
})
@@ -76,7 +84,8 @@ describe('Members API Endpoints', () => {
firstName: 'Lisa',
lastName: 'Beispiel',
geburtsdatum: '2000-01-01',
email: 'lisa@example.com'
email: 'lisa@example.com',
hasHallKey: true
}
it('verweigert Zugriff ohne Token', async () => {
@@ -113,7 +122,9 @@ describe('Members API Endpoints', () => {
const response = await membersPostHandler(event)
expect(response.success).toBe(true)
expect(memberUtils.saveMember).toHaveBeenCalled()
expect(memberUtils.saveMember).toHaveBeenCalledWith(expect.objectContaining({
hasHallKey: true
}))
})
})