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...
+
    +
  • +
    +
    {{ b.name }}
    +
    {{ b.dayMonth }}
    +
    +
    {{ b.inDays === 0 ? 'Heute' : (b.inDays === 1 ? 'Morgen' : 'in ' + b.inDays + ' Tagen') }}
    +
  • +
  • Keine Geburtstage in den nächsten 4 Wochen.
  • +
+
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