diff --git a/pages/cms/index.vue b/pages/cms/index.vue
index 06641dc..a9197c3 100644
--- a/pages/cms/index.vue
+++ b/pages/cms/index.vue
@@ -7,6 +7,26 @@
+
+
+
+
+
+
+
Geburtstage (nächste 4 Wochen)
+
+
Lade...
+
+
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'
diff --git a/pages/mitgliederbereich/profil.vue b/pages/mitgliederbereich/profil.vue
index 239e3a0..f499ef8 100644
--- a/pages/mitgliederbereich/profil.vue
+++ b/pages/mitgliederbereich/profil.vue
@@ -93,6 +93,10 @@
Adresse für alle eingeloggten Mitglieder sichtbar
+
diff --git a/server/api/birthdays.get.js b/server/api/birthdays.get.js
new file mode 100644
index 0000000..129f12a
--- /dev/null
+++ b/server/api/birthdays.get.js
@@ -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
+ }
+})
diff --git a/server/api/members.get.js b/server/api/members.get.js
index 85b25d5..ce41c30 100644
--- a/server/api/members.get.js
+++ b/server/api/members.get.js
@@ -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
}
diff --git a/server/api/profile.get.js b/server/api/profile.get.js
index 1755fed..47d8fde 100644
--- a/server/api/profile.get.js
+++ b/server/api/profile.get.js
@@ -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) {
diff --git a/server/api/profile.put.js b/server/api/profile.put.js
index 74a4289..113fa4c 100644
--- a/server/api/profile.put.js
+++ b/server/api/profile.put.js
@@ -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