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