Füge Geburtstags-Widget hinzu und implementiere Geburtstagsladefunktion; erweitere Sichtbarkeitseinstellungen für Geburtstage in Profil und API
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 49s

This commit is contained in:
Torsten Schulz (local)
2026-02-13 17:27:27 +01:00
parent 3d3e22bb1b
commit 6e297c682c
6 changed files with 152 additions and 2 deletions

View File

@@ -7,6 +7,26 @@
<div class="w-24 h-1 bg-primary-600 mb-8" />
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Geburtstage Widget -->
<div class="bg-white p-6 rounded-xl shadow-lg border border-gray-100">
<div class="flex items-center mb-4">
<div class="w-12 h-12 bg-pink-100 rounded-lg flex items-center justify-center">
<Calendar :size="20" class="text-pink-600" />
</div>
<h2 class="ml-4 text-xl font-semibold text-gray-900">Geburtstage (nächste 4 Wochen)</h2>
</div>
<div v-if="loadingBirthdays" class="text-sm text-gray-500">Lade...</div>
<ul v-else class="space-y-2">
<li v-for="b in birthdays" :key="b.name + b.dayMonth" class="flex items-center justify-between p-3 border border-gray-100 rounded-lg">
<div class="min-w-0">
<div class="font-medium text-gray-900 truncate">{{ b.name }}</div>
<div class="text-xs text-gray-600">{{ b.dayMonth }}</div>
</div>
<div class="text-sm text-gray-500">{{ b.inDays === 0 ? 'Heute' : (b.inDays === 1 ? 'Morgen' : 'in ' + b.inDays + ' Tagen') }}</div>
</li>
<li v-if="birthdays.length === 0" class="text-sm text-gray-600">Keine Geburtstage in den nächsten 4 Wochen.</li>
</ul>
</div>
<!-- Inhalte (gruppiert) -->
<NuxtLink
to="/cms/inhalte"
@@ -160,9 +180,30 @@
<script setup>
import { Newspaper, Calendar, Users, UserCog, Settings, Layout } from 'lucide-vue-next'
import { ref, onMounted } from 'vue'
const authStore = useAuthStore()
const birthdays = ref([])
const loadingBirthdays = ref(true)
const loadBirthdays = async () => {
loadingBirthdays.value = true
try {
const res = await $fetch('/api/birthdays')
birthdays.value = res.birthdays || []
} catch (e) {
console.error('Fehler beim Laden der Geburtstage', e)
birthdays.value = []
} finally {
loadingBirthdays.value = false
}
}
onMounted(() => {
loadBirthdays()
})
definePageMeta({
middleware: 'auth',
layout: 'default'

View File

@@ -93,6 +93,10 @@
<input type="checkbox" class="mr-2" v-model="visibility.showAddress" :disabled="isSaving" />
Adresse für alle eingeloggten Mitglieder sichtbar
</label>
<label class="inline-flex items-center">
<input type="checkbox" class="mr-2" v-model="visibility.showBirthday" :disabled="isSaving" />
Geburtstag für alle eingeloggten Mitglieder sichtbar
</label>
</div>
</div>

View File

@@ -0,0 +1,90 @@
import { readMembers, normalizeDate } from '../utils/members.js'
import { readUsers, migrateUserRoles, getUserFromToken, verifyToken } from '../utils/auth.js'
// Helper: returns array of upcoming birthdays within daysAhead (inclusive)
function getUpcomingBirthdays(entries, daysAhead = 28) {
const now = new Date()
const results = []
// iterate entries with geburtsdatum and name
for (const e of entries) {
const raw = e.geburtsdatum
if (!raw) continue
const parsed = new Date(raw)
if (isNaN(parsed.getTime())) continue
// Build next occurrence for this year
const thisYear = now.getFullYear()
const occ = new Date(thisYear, parsed.getMonth(), parsed.getDate())
// If already passed this year, consider next year
if (occ < now) {
occ.setFullYear(thisYear + 1)
}
const diffDays = Math.ceil((occ - now) / (1000 * 60 * 60 * 24))
if (diffDays >= 0 && diffDays <= daysAhead) {
results.push({
name: e.name || `${e.firstName || ''} ${e.lastName || ''}`.trim(),
dayMonth: `${String(occ.getDate()).padStart(2, '0')}.${String(occ.getMonth()+1).padStart(2, '0')}`,
date: occ,
diffDays
})
}
}
// Sort by upcoming date
results.sort((a, b) => a.date - b.date)
return results
}
export default defineEventHandler(async (event) => {
try {
// Determine viewer for visibility rules; token optional
const token = getCookie(event, 'auth_token')
let currentUser = null
if (token) {
const decoded = verifyToken(token)
if (decoded) {
currentUser = await getUserFromToken(token)
}
}
const manualMembers = await readMembers()
const registeredUsers = await readUsers()
// Build unified list of candidates with geburtsdatum and visibility
const candidates = []
for (const m of manualMembers) {
const isAccepted = m.active === true || (m.status && String(m.status).toLowerCase() === 'accepted') || m.accepted === true
if (!isAccepted) continue
const vis = m.visibility || {}
const showBirthday = vis.showBirthday === undefined ? true : Boolean(vis.showBirthday)
candidates.push({ name: `${m.firstName || ''} ${m.lastName || ''}`.trim(), geburtsdatum: m.geburtsdatum, visibility: { showBirthday }, source: 'manual' })
}
for (const u of registeredUsers) {
if (!u.active) continue
const vis = u.visibility || {}
const showBirthday = vis.showBirthday === undefined ? true : Boolean(vis.showBirthday)
candidates.push({ name: u.name, geburtsdatum: u.geburtsdatum, visibility: { showBirthday }, source: 'login' })
}
// Respect visibility: if viewer is vorstand they see all birthdays
const isPrivilegedViewer = currentUser ? (Array.isArray(currentUser.roles) ? currentUser.roles.includes('vorstand') : currentUser.role === 'vorstand') : false
const filtered = candidates.filter(c => c.geburtsdatum && (isPrivilegedViewer || c.visibility.showBirthday === true))
const upcoming = getUpcomingBirthdays(filtered, 28)
// Return only next 4 weeks entries with name and dayMonth
return {
success: true,
birthdays: upcoming.map(b => ({ name: b.name, dayMonth: b.dayMonth, inDays: b.diffDays }))
}
} catch (error) {
console.error('Fehler beim Abrufen der Geburtstage:', error)
throw error
}
})

View File

@@ -194,9 +194,11 @@ export default defineEventHandler(async (event) => {
const hadEmail = !!member.email
const hadPhone = !!member.phone
const hadAddress = !!member.address
const hadBirthday = !!member.geburtsdatum
const emailVisible = (isPrivilegedViewer || (isViewerAuthenticated && showEmail))
const phoneVisible = (isPrivilegedViewer || (isViewerAuthenticated && showPhone))
const addressVisible = (isPrivilegedViewer || (isViewerAuthenticated && showAddress))
const birthdayVisible = (isPrivilegedViewer || (isViewerAuthenticated && (member.visibility && member.visibility.showBirthday !== undefined ? Boolean(member.visibility.showBirthday) : true)))
const contactHidden = (!emailVisible && hadEmail) || (!phoneVisible && hadPhone) || (!addressVisible && hadAddress)
return {
@@ -214,6 +216,18 @@ export default defineEventHandler(async (event) => {
email: emailVisible ? member.email : undefined,
phone: phoneVisible ? member.phone : undefined,
address: addressVisible ? member.address : undefined,
// Birthday: expose only day + month and only if allowed; do not expose year or age
birthday: (birthdayVisible && hadBirthday) ? (function(){
try {
const d = new Date(member.geburtsdatum)
if (isNaN(d.getTime())) return undefined
const day = `${d.getDate()}`.padStart(2, '0')
const month = `${d.getMonth()+1}`.padStart(2, '0')
return `${day}.${month}`
} catch (_e) {
return undefined
}
})() : undefined,
// Flag for UI: data existed but is hidden to the current viewer
contactHidden
}

View File

@@ -26,7 +26,7 @@ export default defineEventHandler(async (event) => {
name: user.name,
email: user.email,
phone: user.phone || '',
visibility: user.visibility || {}
visibility: Object.assign({ showBirthday: true }, (user.visibility || {}))
}
}
} catch (error) {

View File

@@ -60,7 +60,7 @@ export default defineEventHandler(async (event) => {
user.phone = phone || ''
// Optional visibility preferences (what to show to other logged-in members)
// Expected shape: { showEmail: boolean, showPhone: boolean, showAddress: boolean }
// Expected shape: { showEmail: boolean, showPhone: boolean, showAddress: boolean, showBirthday: boolean }
const visibility = body.visibility || body.visibilityPreferences || null
if (visibility && typeof visibility === 'object') {
user.visibility = user.visibility || {}
@@ -68,6 +68,7 @@ export default defineEventHandler(async (event) => {
if (visibility.showEmail !== undefined) user.visibility.showEmail = Boolean(visibility.showEmail)
if (visibility.showPhone !== undefined) user.visibility.showPhone = Boolean(visibility.showPhone)
if (visibility.showAddress !== undefined) user.visibility.showAddress = Boolean(visibility.showAddress)
if (visibility.showBirthday !== undefined) user.visibility.showBirthday = Boolean(visibility.showBirthday)
}
// Handle password change