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
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 49s
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
90
server/api/birthdays.get.js
Normal file
90
server/api/birthdays.get.js
Normal 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
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user