Merge pull request 'dev' (#8) from dev into main
All checks were successful
Code Analysis and Production Deploy / analyze (push) Has been skipped
Code Analysis and Production Deploy / deploy-production (push) Successful in 2m8s
Code Analysis and Production Deploy / deploy-test (push) Has been skipped

Reviewed-on: #8
This commit was merged in pull request #8.
This commit is contained in:
2026-04-27 16:46:55 +02:00
4 changed files with 108 additions and 14 deletions

12
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "harheimertc-website",
"version": "1.1.0",
"version": "1.1.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "harheimertc-website",
"version": "1.1.0",
"version": "1.1.3",
"hasInstallScript": true,
"dependencies": {
"@pinia/nuxt": "^0.11.2",
@@ -36,7 +36,7 @@
"eslint-plugin-vue": "^10.6.2",
"globals": "^16.5.0",
"lucide-vue-next": "^0.344.0",
"postcss": "^8.4.0",
"postcss": "^8.5.12",
"supertest": "^7.1.0",
"tailwindcss": "^3.4.0",
"vitest": "^4.0.16",
@@ -11366,9 +11366,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"version": "8.5.12",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
"integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
"funding": [
{
"type": "opencollective",

View File

@@ -1,6 +1,6 @@
{
"name": "harheimertc-website",
"version": "1.1.2",
"version": "1.1.3",
"description": "Moderne Webseite für den Harheimer Tischtennis Club",
"private": true,
"type": "module",
@@ -50,7 +50,7 @@
"eslint-plugin-vue": "^10.6.2",
"globals": "^16.5.0",
"lucide-vue-next": "^0.344.0",
"postcss": "^8.4.0",
"postcss": "^8.5.12",
"supertest": "^7.1.0",
"tailwindcss": "^3.4.0",
"vitest": "^4.0.16",

View File

@@ -106,9 +106,31 @@
<!-- Active Users -->
<div>
<h2 class="text-2xl font-display font-bold text-gray-900 mb-4">
Aktive Benutzer ({{ activeUsers.length }})
</h2>
<div class="flex flex-col gap-3 mb-4 sm:flex-row sm:items-end sm:justify-between">
<h2 class="text-2xl font-display font-bold text-gray-900">
Aktive Benutzer ({{ sortedActiveUsers.length }})
</h2>
<div class="flex items-center gap-2">
<label
for="user-sort-order"
class="text-sm font-medium text-gray-700"
>
Sortierung
</label>
<select
id="user-sort-order"
v-model="nameSortMode"
class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary-600"
>
<option value="firstLast">
Vorname Nachname
</option>
<option value="lastFirst">
Nachname, Vorname
</option>
</select>
</div>
</div>
<div class="bg-white rounded-xl shadow-lg overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
@@ -135,13 +157,13 @@
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr
v-for="user in activeUsers"
v-for="user in sortedActiveUsers"
:key="user.id"
class="hover:bg-gray-50"
>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900">
{{ user.name }}
{{ getDisplayName(user) }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
@@ -253,7 +275,7 @@
>
<div class="bg-white rounded-xl shadow-2xl max-w-md w-full p-6">
<h2 class="text-2xl font-display font-bold text-gray-900 mb-4">
Rollen bearbeiten: {{ editingUser.name }}
Rollen bearbeiten: {{ getDisplayName(editingUser) }}
</h2>
<div class="space-y-3 mb-6">
@@ -350,6 +372,7 @@ const errorMessage = ref('')
const showRoleModal = ref(false)
const editingUser = ref(null)
const selectedRoles = ref([])
const nameSortMode = ref('firstLast')
const pendingUsers = computed(() => {
return allUsers.value
@@ -364,6 +387,61 @@ const activeUsers = computed(() => {
return allUsers.value.filter(u => u.active === true)
})
const splitNameParts = (name = '') => {
const trimmed = (name || '').trim()
if (!trimmed) {
return { firstName: '', lastName: '' }
}
if (trimmed.includes(',')) {
const [lastNameRaw, ...firstNameRaw] = trimmed.split(',')
return {
firstName: firstNameRaw.join(',').trim(),
lastName: (lastNameRaw || '').trim()
}
}
const parts = trimmed.split(/\s+/).filter(Boolean)
if (parts.length <= 1) {
return { firstName: parts[0] || '', lastName: '' }
}
return {
firstName: parts[0],
lastName: parts.slice(1).join(' ')
}
}
const getDisplayName = (user) => {
const { firstName, lastName } = splitNameParts(user?.name || '')
if (nameSortMode.value === 'lastFirst') {
if (!lastName) {
return firstName
}
return `${lastName}, ${firstName}`.trim()
}
return `${firstName} ${lastName}`.trim()
}
const sortedActiveUsers = computed(() => {
return [...activeUsers.value].sort((a, b) => {
const nameA = splitNameParts(a.name)
const nameB = splitNameParts(b.name)
if (nameSortMode.value === 'lastFirst') {
const lastNameCompare = nameA.lastName.localeCompare(nameB.lastName, 'de', { sensitivity: 'base' })
if (lastNameCompare !== 0) return lastNameCompare
return nameA.firstName.localeCompare(nameB.firstName, 'de', { sensitivity: 'base' })
}
const firstNameCompare = nameA.firstName.localeCompare(nameB.firstName, 'de', { sensitivity: 'base' })
if (firstNameCompare !== 0) return firstNameCompare
return nameA.lastName.localeCompare(nameB.lastName, 'de', { sensitivity: 'base' })
})
})
const formatDate = (dateString) => {
return new Date(dateString).toLocaleString('de-DE', {
year: 'numeric',

View File

@@ -236,6 +236,22 @@ export async function getRecipientsByGroup(targetGroup) {
email: m.email,
name: `${m.firstName || ''} ${m.lastName || ''}`.trim() || m.name || ''
}))
// Zusätzlich aktive Trainer aus users.json anschreiben
users
.filter(u => {
if (!u.active || !u.email || !u.email.trim()) return false
const roles = Array.isArray(u.roles) ? u.roles : (u.role ? [u.role] : [])
return roles.includes('trainer')
})
.forEach(u => {
if (!recipients.find(r => r.email.toLowerCase().trim() === u.email.toLowerCase().trim())) {
recipients.push({
email: u.email.trim(),
name: u.name || ''
})
}
})
break
case 'mannschaftsspieler':