document.addEventListener('DOMContentLoaded', () => { // --- APPLICATION STATE --- let state = { friends: [], meetings: [], selectedFriend: null, activeTab: 'dashboard' }; // --- SELECT DOM ELEMENTS --- const DOM = { // Navigation & Tabs navItems: document.querySelectorAll('.nav-item'), tabPanes: document.querySelectorAll('.tab-pane'), pageTitle: document.getElementById('page-title'), // Dashboard Stats statTotalFriends: document.getElementById('stat-total-friends'), statMeetingsMonth: document.getElementById('stat-meetings-month'), statAvgHonor: document.getElementById('stat-avg-honor'), statUpcomingBirthdays: document.getElementById('stat-upcoming-birthdays'), // Dashboard Panels urgentContactsList: document.getElementById('urgent-contacts-list'), upcomingBirthdaysList: document.getElementById('upcoming-birthdays-list'), quickLogForm: document.getElementById('quick-log-form'), quickFriendSelect: document.getElementById('quick-friend-select'), quickDate: document.getElementById('quick-date'), quickActivity: document.getElementById('quick-activity'), quickMood: document.getElementById('quick-mood'), quickDetails: document.getElementById('quick-details'), // Friends Directory friendsSearch: document.getElementById('friends-search'), btnOpenAddModal: document.getElementById('btn-add-friend-modal'), friendsGrid: document.getElementById('friends-grid'), // Add Friend Modal addFriendModal: document.getElementById('add-friend-modal'), addFriendForm: document.getElementById('add-friend-form'), btnCloseAddModal: document.getElementById('btn-close-add-modal'), btnCancelAddModal: document.getElementById('btn-cancel-add-modal'), // Friend Detail Modal detailModal: document.getElementById('friend-detail-modal'), btnCloseDetailModal: document.getElementById('btn-close-detail-modal'), btnDeleteFriend: document.getElementById('btn-delete-friend'), btnEditFriendTrigger: document.getElementById('btn-edit-friend-trigger'), detailAvatar: document.getElementById('detail-avatar'), detailName: document.getElementById('detail-name'), detailRelationshipBadge: document.getElementById('detail-badge-relationship'), detailHonorValue: document.getElementById('detail-honor-value'), btnHonorPlus: document.getElementById('btn-honor-plus'), btnHonorMinus: document.getElementById('btn-honor-minus'), detailBirthday: document.getElementById('detail-birthday'), detailContact: document.getElementById('detail-contact'), detailAddress: document.getElementById('detail-address'), detailFamily: document.getElementById('detail-family'), detailLife: document.getElementById('detail-life'), detailHobbies: document.getElementById('detail-hobbies'), detailMilestones: document.getElementById('detail-milestones'), detailFood: document.getElementById('detail-food'), detailNotes: document.getElementById('detail-notes'), detailTopicsList: document.getElementById('detail-topics-list'), detailMeetingsTimeline: document.getElementById('detail-meetings-timeline'), addTopicForm: document.getElementById('add-topic-form'), newTopicInput: document.getElementById('new-topic-input'), profileLogMeetingForm: document.getElementById('profile-log-meeting-form'), profileMeetingDate: document.getElementById('profile-meeting-date'), profileMeetingMood: document.getElementById('profile-meeting-mood'), profileMeetingActivity: document.getElementById('profile-meeting-activity'), profileMeetingDetails: document.getElementById('profile-meeting-details'), // Edit Friend Modal editFriendModal: document.getElementById('edit-friend-modal'), editFriendForm: document.getElementById('edit-friend-form'), btnCloseEditModal: document.getElementById('btn-close-edit-modal'), btnCancelEditModal: document.getElementById('btn-cancel-edit-modal'), editId: document.getElementById('edit-id'), editName: document.getElementById('edit-name'), editBirthday: document.getElementById('edit-birthday'), editContact: document.getElementById('edit-contact'), editRelationship: document.getElementById('edit-relationship'), editFamily: document.getElementById('edit-family'), editAddress: document.getElementById('edit-address'), editHonor: document.getElementById('edit-honor'), editJob: document.getElementById('edit-job'), editLife: document.getElementById('edit-life'), editHobbies: document.getElementById('edit-hobbies'), editMilestones: document.getElementById('edit-milestones'), editFood: document.getElementById('edit-food'), editNotes: document.getElementById('edit-notes'), // Obsidian Import Tab obsidianImportForm: document.getElementById('obsidian-import-form'), importFilename: document.getElementById('import-filename'), importContent: document.getElementById('import-content'), importResult: document.getElementById('import-result') }; // --- INITIALIZATION --- async function init() { setupTabNavigation(); setupEventListeners(); setFormDefaultDates(); // Initial fetch of data await refreshAllData(); } // --- DATA FETCHING & SYNC --- async function refreshAllData() { showGlobalLoaders(); await Promise.all([ fetchFriends(), fetchMeetings() ]); renderDashboard(); renderFriendsDirectory(); // If a friend modal is open, refresh it as well if (state.selectedFriend) { await refreshFriendDetails(state.selectedFriend.id); } } async function fetchFriends() { try { const res = await fetch('/api/friends'); if (!res.ok) throw new Error('Fehler beim Laden der Freunde'); state.friends = await res.json(); } catch (err) { console.error(err); alert('Konnte Freunde nicht laden: ' + err.message); } } async function fetchMeetings() { try { const res = await fetch('/api/meetings'); if (!res.ok) throw new Error('Fehler beim Laden der Treffen'); state.meetings = await res.json(); } catch (err) { console.error(err); // We fall back to empty meetings list gracefully if route doesn't work yet state.meetings = []; } } async function refreshFriendDetails(friendId) { try { const res = await fetch(`/api/friends/${friendId}`); if (!res.ok) throw new Error('Konnte Details nicht laden'); state.selectedFriend = await res.json(); populateFriendDetails(state.selectedFriend); } catch (err) { console.error(err); alert('Fehler beim Aktualisieren der Details: ' + err.message); } } // --- SPA ROUTING / TAB NAVIGATION --- function setupTabNavigation() { // URL Hash handling window.addEventListener('hashchange', handleHashRoute); // Sidebar clicks DOM.navItems.forEach(item => { item.addEventListener('click', (e) => { e.preventDefault(); const tab = item.getAttribute('data-tab'); window.location.hash = tab; }); }); // Handle initial load route handleHashRoute(); } function handleHashRoute() { let tab = window.location.hash.replace('#', '') || 'dashboard'; // Validate tab const validTabs = ['dashboard', 'friends', 'import']; if (!validTabs.includes(tab)) tab = 'dashboard'; state.activeTab = tab; // Toggle nav items DOM.navItems.forEach(item => { if (item.getAttribute('data-tab') === tab) { item.classList.add('active'); } else { item.classList.remove('active'); } }); // Toggle panes DOM.tabPanes.forEach(pane => { if (pane.id === `tab-${tab}`) { pane.classList.add('active'); } else { pane.classList.remove('active'); } }); // Update Header Title const titles = { dashboard: 'Dashboard', friends: 'Freunde-Verzeichnis', import: 'Obsidian Markdown Import' }; DOM.pageTitle.textContent = titles[tab] || 'Dashboard'; } // --- EVENT LISTENERS --- function setupEventListeners() { // --- Modals Toggle --- DOM.btnOpenAddModal.addEventListener('click', () => openModal(DOM.addFriendModal)); DOM.btnCloseAddModal.addEventListener('click', () => closeModal(DOM.addFriendModal)); DOM.btnCancelAddModal.addEventListener('click', () => closeModal(DOM.addFriendModal)); DOM.btnCloseDetailModal.addEventListener('click', () => { closeModal(DOM.detailModal); state.selectedFriend = null; }); DOM.btnCloseEditModal.addEventListener('click', () => closeModal(DOM.editFriendModal)); DOM.btnCancelEditModal.addEventListener('click', () => closeModal(DOM.editFriendModal)); // Close modals on clicking outside container window.addEventListener('click', (e) => { if (e.target.classList.contains('modal-backdrop')) { closeModal(e.target); if (e.target.id === 'friend-detail-modal') { state.selectedFriend = null; } } }); // --- Search Filter --- DOM.friendsSearch.addEventListener('input', (e) => { renderFriendsDirectory(e.target.value); }); // --- Add Friend Form Submit --- DOM.addFriendForm.addEventListener('submit', async (e) => { e.preventDefault(); const friendData = { name: document.getElementById('add-name').value.trim(), birthday: document.getElementById('add-birthday').value, contact: document.getElementById('add-contact').value.trim(), relationship_status: document.getElementById('add-relationship').value.trim(), family: document.getElementById('add-family').value.trim(), address: document.getElementById('add-address').value.trim(), honor: parseInt(document.getElementById('add-honor').value, 10) || 0, job: document.getElementById('add-job').value.trim(), life_situation: document.getElementById('add-life').value.trim(), hobbies: document.getElementById('add-hobbies').value.trim(), milestones: document.getElementById('add-milestones').value.trim(), food_preferences: document.getElementById('add-food').value.trim(), random_notes: document.getElementById('add-notes').value.trim() }; try { const res = await fetch('/api/friends', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(friendData) }); if (!res.ok) throw new Error('Fehler beim Anlegen'); DOM.addFriendForm.reset(); setFormDefaultDates(); closeModal(DOM.addFriendModal); await refreshAllData(); } catch (err) { alert('Konnte Freund nicht speichern: ' + err.message); } }); // --- Edit Friend Form Submit --- DOM.editFriendForm.addEventListener('submit', async (e) => { e.preventDefault(); const friendId = DOM.editId.value; const friendData = { name: DOM.editName.value.trim(), birthday: DOM.editBirthday.value, contact: DOM.editContact.value.trim(), relationship_status: DOM.editRelationship.value.trim(), family: DOM.editFamily.value.trim(), address: DOM.editAddress.value.trim(), honor: parseInt(DOM.editHonor.value, 10) || 0, job: DOM.editJob.value.trim(), life_situation: DOM.editLife.value.trim(), hobbies: DOM.editHobbies.value.trim(), milestones: DOM.editMilestones.value.trim(), food_preferences: DOM.editFood.value.trim(), random_notes: DOM.editNotes.value.trim() }; try { const res = await fetch(`/api/friends/${friendId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(friendData) }); if (!res.ok) throw new Error('Fehler beim Aktualisieren'); closeModal(DOM.editFriendModal); await refreshAllData(); if (state.selectedFriend) { await refreshFriendDetails(friendId); } } catch (err) { alert('Konnte Profildaten nicht aktualisieren: ' + err.message); } }); // --- Delete Friend Trigger --- DOM.btnDeleteFriend.addEventListener('click', async () => { if (!state.selectedFriend) return; const confirmDelete = confirm(`Bist du sicher, dass du ${state.selectedFriend.name} aus dem CRM löschen willst? Alle Treffen und Notizen gehen verloren.`); if (!confirmDelete) return; try { const res = await fetch(`/api/friends/${state.selectedFriend.id}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Löschen fehlgeschlagen'); closeModal(DOM.detailModal); state.selectedFriend = null; await refreshAllData(); } catch (err) { alert('Fehler beim Löschen: ' + err.message); } }); // --- Edit Modal Prefill & Trigger --- DOM.btnEditFriendTrigger.addEventListener('click', () => { if (!state.selectedFriend) return; const f = state.selectedFriend; DOM.editId.value = f.id; DOM.editName.value = f.name || ''; DOM.editBirthday.value = f.birthday || ''; DOM.editContact.value = f.contact || ''; DOM.editRelationship.value = f.relationship_status || ''; DOM.editFamily.value = f.family || ''; DOM.editAddress.value = f.address || ''; DOM.editHonor.value = f.honor || 0; DOM.editJob.value = f.job || ''; DOM.editLife.value = f.life_situation || ''; DOM.editHobbies.value = f.hobbies || ''; DOM.editMilestones.value = f.milestones || ''; DOM.editFood.value = f.food_preferences || ''; DOM.editNotes.value = f.random_notes || ''; openModal(DOM.editFriendModal); }); // --- Honor Score +/- Clickers --- DOM.btnHonorPlus.addEventListener('click', () => adjustHonor(1)); DOM.btnHonorMinus.addEventListener('click', () => adjustHonor(-1)); // --- Log Meeting Form (Dashboard) Submit --- DOM.quickLogForm.addEventListener('submit', async (e) => { e.preventDefault(); const meetingData = { friend_id: parseInt(DOM.quickFriendSelect.value, 10), date: DOM.quickDate.value, activity: DOM.quickActivity.value.trim(), mood: DOM.quickMood.value.trim(), details: DOM.quickDetails.value.trim() }; try { const res = await fetch('/api/meetings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(meetingData) }); if (!res.ok) throw new Error('Eintrag fehlgeschlagen'); DOM.quickLogForm.reset(); setFormDefaultDates(); await refreshAllData(); } catch (err) { alert('Konnte Treffen nicht eintragen: ' + err.message); } }); // --- Log Meeting Form (Profile Modal) Submit --- DOM.profileLogMeetingForm.addEventListener('submit', async (e) => { e.preventDefault(); if (!state.selectedFriend) return; const meetingData = { friend_id: state.selectedFriend.id, date: DOM.profileMeetingDate.value, activity: DOM.profileMeetingActivity.value.trim(), mood: DOM.profileMeetingMood.value.trim(), details: DOM.profileMeetingDetails.value.trim() }; try { const res = await fetch('/api/meetings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(meetingData) }); if (!res.ok) throw new Error('Eintrag fehlgeschlagen'); DOM.profileLogMeetingForm.reset(); setFormDefaultDates(); await refreshFriendDetails(state.selectedFriend.id); await refreshAllData(); } catch (err) { alert('Konnte Treffen nicht eintragen: ' + err.message); } }); // --- Add Topic Form Submit --- DOM.addTopicForm.addEventListener('submit', async (e) => { e.preventDefault(); if (!state.selectedFriend) return; const topicData = { friend_id: state.selectedFriend.id, topic: DOM.newTopicInput.value.trim() }; try { const res = await fetch('/api/topics', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(topicData) }); if (!res.ok) throw new Error('Hinzufügen fehlgeschlagen'); DOM.newTopicInput.value = ''; await refreshFriendDetails(state.selectedFriend.id); await refreshAllData(); } catch (err) { alert('Konnte Thema nicht hinzufügen: ' + err.message); } }); // --- Obsidian Import Submit --- DOM.obsidianImportForm.addEventListener('submit', async (e) => { e.preventDefault(); const importData = { filename: DOM.importFilename.value.trim() || 'Imported_Obsidian_Profile.md', content: DOM.importContent.value }; try { DOM.importResult.classList.remove('hidden'); DOM.importResult.innerHTML = '

Analysiere Markdown-Struktur...

'; const res = await fetch('/api/import-obsidian', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(importData) }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Import fehlgeschlagen'); DOM.importResult.innerHTML = `

${data.name} erfolgreich importiert!

${data.message}

`; lucide.createIcons(); DOM.obsidianImportForm.reset(); // Bind quick view button document.getElementById('btn-view-imported').addEventListener('click', (e) => { const friendId = e.currentTarget.getAttribute('data-id'); openFriendDetail(friendId); }); await refreshAllData(); } catch (err) { DOM.importResult.innerHTML = `

Fehler beim Importieren

${err.message}

`; lucide.createIcons(); } }); } // --- DYNAMIC RENDERING: DASHBOARD --- function renderDashboard() { const friends = state.friends; const meetings = state.meetings; // 1. STATS CALCULATIONS // Stat: Total Friends DOM.statTotalFriends.textContent = friends.length; // Stat: Meetings this month const today = new Date(); const currentYear = today.getFullYear(); const currentMonth = today.getMonth(); // 0-indexed const meetingsThisMonth = meetings.filter(m => { if (!m.date) return false; const mDate = new Date(m.date); return mDate.getFullYear() === currentYear && mDate.getMonth() === currentMonth; }).length; DOM.statMeetingsMonth.textContent = meetingsThisMonth; // Stat: Average Honor const totalHonor = friends.reduce((sum, f) => sum + (f.honor || 0), 0); const avgHonor = friends.length > 0 ? Math.round(totalHonor / friends.length) : 0; DOM.statAvgHonor.textContent = avgHonor; // Stat: Upcoming Birthdays count const upcomingBdays = friends.filter(f => { if (!f.birthday) return false; const days = getDaysUntilBirthday(f.birthday); return days !== null && days >= 0 && days <= 30; }); DOM.statUpcomingBirthdays.textContent = upcomingBdays.length; // 2. RENDER URGENT CONTACTS ("Lange nicht gesehen") DOM.urgentContactsList.innerHTML = ''; if (friends.length === 0) { DOM.urgentContactsList.innerHTML = '

Noch keine Freunde eingetragen.

'; } else { // Sort friends: those never met first, then those met longest ago const sortedUrgent = [...friends].sort((a, b) => { if (!a.last_meeting_date && !b.last_meeting_date) return a.name.localeCompare(b.name); if (!a.last_meeting_date) return -1; // a first if (!b.last_meeting_date) return 1; // b first return new Date(a.last_meeting_date) - new Date(b.last_meeting_date); }); // Show top 5 urgent contacts sortedUrgent.slice(0, 5).forEach(f => { const item = document.createElement('div'); item.className = 'list-item-glass'; let subText = ''; let warningClass = ''; if (!f.last_meeting_date) { subText = 'Noch nie getroffen'; warningClass = 'urgent'; } else { const days = getDaysSince(f.last_meeting_date); subText = `Zuletzt vor ${days} Tagen getroffen (${formatGermanDate(f.last_meeting_date)})`; if (days > 60) warningClass = 'urgent'; else if (days > 30) warningClass = 'warning'; } const initials = getInitials(f.name); item.innerHTML = `
${initials}
${f.name} ${subText}
`; DOM.urgentContactsList.appendChild(item); }); // Bind click triggers DOM.urgentContactsList.querySelectorAll('.btn-view-profile').forEach(btn => { btn.addEventListener('click', (e) => { const id = e.currentTarget.getAttribute('data-id'); openFriendDetail(id); }); }); } // 3. RENDER UPCOMING BIRTHDAYS PANEL DOM.upcomingBirthdaysList.innerHTML = ''; if (upcomingBdays.length === 0) { DOM.upcomingBirthdaysList.innerHTML = '

Keine Geburtstage in den nächsten 30 Tagen.

'; } else { // Sort upcoming by closest day upcomingBdays.sort((a, b) => getDaysUntilBirthday(a.birthday) - getDaysUntilBirthday(b.birthday)); upcomingBdays.forEach(f => { const days = getDaysUntilBirthday(f.birthday); const age = getAgeTurning(f.birthday); const item = document.createElement('div'); item.className = 'list-item-glass'; let dayString = ''; if (days === 0) { dayString = 'Heute! 🎉'; } else if (days === 1) { dayString = 'Morgen!'; } else { dayString = `in ${days} Tagen`; } const initials = getInitials(f.name); const cleanBday = formatGermanDate(f.birthday, false); // format without year or with dots item.innerHTML = `
${f.name} ${cleanBday} • wird ${age} (${dayString})
`; DOM.upcomingBirthdaysList.appendChild(item); }); // Bind click triggers DOM.upcomingBirthdaysList.querySelectorAll('.btn-view-profile').forEach(btn => { btn.addEventListener('click', (e) => { const id = e.currentTarget.getAttribute('data-id'); openFriendDetail(id); }); }); } // 4. POPULATE QUICK LOG FRIEND SELECT DOM.quickFriendSelect.innerHTML = ''; friends.forEach(f => { const opt = document.createElement('option'); opt.value = f.id; opt.textContent = f.name; DOM.quickFriendSelect.appendChild(opt); }); // Initialize Lucide Icons lucide.createIcons(); } // --- DYNAMIC RENDERING: FRIENDS DIRECTORY --- function renderFriendsDirectory(searchQuery = '') { const grid = DOM.friendsGrid; grid.innerHTML = ''; const query = searchQuery.trim().toLowerCase(); const filtered = state.friends.filter(f => { if (!query) return true; return (f.name || '').toLowerCase().includes(query) || (f.address || '').toLowerCase().includes(query) || (f.hobbies || '').toLowerCase().includes(query) || (f.job || '').toLowerCase().includes(query); }); if (filtered.length === 0) { grid.innerHTML = `

Keine Freunde gefunden

Passe deinen Suchbegriff an oder füge einen neuen Freund hinzu.

`; lucide.createIcons(); return; } filtered.forEach(f => { const card = document.createElement('div'); card.className = 'friend-card glass-panel'; card.setAttribute('data-id', f.id); const initials = getInitials(f.name); // Residential town extraction (last line of address or simply cut address) let town = 'Unbekannt'; if (f.address) { const addressLines = f.address.split('\n'); town = addressLines[addressLines.length - 1].trim(); } // Format last meeting subtext let lastMeetSub = 'Noch nie getroffen'; if (f.last_meeting_date) { lastMeetSub = `Zuletzt: ${formatGermanDate(f.last_meeting_date)}`; } card.innerHTML = `
${initials}

${f.name}

${town}
Ehre: ${f.honor || 0} ${f.birthday ? ` ${formatGermanDate(f.birthday, false)}` : ''}

${lastMeetSub}

`; // Event Listeners for Card Items card.querySelector('.btn-view-details').addEventListener('click', () => openFriendDetail(f.id)); // Card Honor +/- button handlers card.querySelectorAll('.btn-honor-card').forEach(btn => { btn.addEventListener('click', async (e) => { e.stopPropagation(); const action = btn.getAttribute('data-action'); const change = action === 'plus' ? 1 : -1; const valDisplay = card.querySelector('.honor-card-val'); try { const res = await fetch(`/api/friends/${f.id}/honor`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ change }) }); const data = await res.json(); if (res.ok) { // Update state locally const friendIndex = state.friends.findIndex(x => x.id === f.id); if (friendIndex !== -1) { state.friends[friendIndex].honor = data.honor; } // Direct UI bump animation valDisplay.textContent = data.honor; valDisplay.classList.add('bump-animate'); setTimeout(() => valDisplay.classList.remove('bump-animate'), 300); // Refresh dashboard in background without full render flashes renderDashboard(); } } catch (err) { console.error(err); } }); }); grid.appendChild(card); }); lucide.createIcons(); } // --- DYNAMIC RENDERING: FRIEND PROFILE DETAIL --- async function openFriendDetail(friendId) { showModal(DOM.detailModal); await refreshFriendDetails(friendId); } function populateFriendDetails(friend) { DOM.detailAvatar.textContent = getInitials(friend.name); DOM.detailName.textContent = friend.name; // Relationship badge DOM.detailRelationshipBadge.textContent = friend.relationship_status || 'Kein Beziehungsstatus'; if (friend.relationship_status && friend.relationship_status.toUpperCase().includes('ALONE')) { DOM.detailRelationshipBadge.className = 'relationship-badge forever-alone'; } else { DOM.detailRelationshipBadge.className = 'relationship-badge'; } DOM.detailHonorValue.textContent = friend.honor || 0; // Formatting General details let bdayText = '-'; if (friend.birthday) { const days = getDaysUntilBirthday(friend.birthday); const age = getAgeTurning(friend.birthday); let daysRemainingStr = ''; if (days === 0) daysRemainingStr = ' (Heute! 🎉)'; else if (days === 1) daysRemainingStr = ' (Morgen!)'; else daysRemainingStr = ` (in ${days} Tagen, wird ${age})`; bdayText = `${formatGermanDate(friend.birthday)} ${daysRemainingStr}`; } DOM.detailBirthday.textContent = bdayText; DOM.detailContact.textContent = friend.contact || '-'; DOM.detailAddress.textContent = friend.address || '-'; DOM.detailFamily.textContent = friend.family || '-'; // Job, Hobbies, Milestones DOM.detailLife.innerHTML = formatMarkdownParagraphs(friend.life_situation || '-'); let hobbiesHtml = '-'; if (friend.hobbies) { hobbiesHtml = friend.hobbies.split(',').map(h => `${h.trim()}`).join(' '); } else if (friend.job) { hobbiesHtml = `${friend.job}`; } DOM.detailHobbies.innerHTML = hobbiesHtml; DOM.detailMilestones.innerHTML = formatMarkdownParagraphs(friend.milestones || '-'); // Food & Random Notes DOM.detailFood.textContent = friend.food_preferences || '-'; DOM.detailNotes.innerHTML = formatMarkdownParagraphs(friend.random_notes || '-'); // RENDER TOPICS CHECKLIST renderTopics(friend.topics || []); // RENDER MEETINGS TIMELINE renderTimeline(friend.meetings || []); lucide.createIcons(); } function renderTopics(topics) { const list = DOM.detailTopicsList; list.innerHTML = ''; if (topics.length === 0) { list.innerHTML = '
  • Keine offenen Gesprächsthemen.
  • '; return; } topics.forEach(t => { const li = document.createElement('li'); li.className = `topic-item ${t.completed ? 'completed' : ''}`; li.innerHTML = ` `; // Checkbox toggle logic li.querySelector('.topic-checkbox').addEventListener('change', async (e) => { const checked = e.target.checked; try { const res = await fetch(`/api/topics/${t.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ completed: checked ? 1 : 0 }) }); if (res.ok) { li.classList.toggle('completed', checked); await refreshAllData(); } } catch (err) { console.error(err); } }); // Delete topic logic li.querySelector('.btn-delete-topic').addEventListener('click', async () => { try { const res = await fetch(`/api/topics/${t.id}`, { method: 'DELETE' }); if (res.ok) { li.remove(); await refreshFriendDetails(state.selectedFriend.id); await refreshAllData(); } } catch (err) { console.error(err); } }); list.appendChild(li); }); lucide.createIcons(); } function renderTimeline(meetings) { const timeline = DOM.detailMeetingsTimeline; timeline.innerHTML = ''; if (meetings.length === 0) { timeline.innerHTML = '

    Noch keine Treffen dokumentiert.

    '; return; } meetings.forEach(m => { const item = document.createElement('div'); item.className = 'timeline-item'; let moodBadge = ''; if (m.mood) { let moodClass = 'mood-default'; const cleanedMood = m.mood.toLowerCase(); if (cleanedMood.includes('legendär') || cleanedMood.includes('super') || cleanedMood.includes('genial')) { moodClass = 'mood-excellent'; } else if (cleanedMood.includes('entspannt') || cleanedMood.includes('gut') || cleanedMood.includes('zufrieden')) { moodClass = 'mood-good'; } moodBadge = `${m.mood}`; } item.innerHTML = `
    ${formatGermanDate(m.date)}
    ${moodBadge}

    ${m.activity}

    ${m.details ? `

    ${m.details.replace(/\n/g, '
    ')}

    ` : ''}
    `; // Delete meeting logic item.querySelector('.btn-delete-meeting').addEventListener('click', async () => { const confirmDel = confirm('Willst du diesen Treffen-Eintrag unwiderruflich löschen?'); if (!confirmDel) return; try { const res = await fetch(`/api/meetings/${m.id}`, { method: 'DELETE' }); if (res.ok) { item.remove(); await refreshFriendDetails(state.selectedFriend.id); await refreshAllData(); } } catch (err) { console.error(err); } }); timeline.appendChild(item); }); lucide.createIcons(); } // --- EHRE COUNTER ADJUSTMENT --- async function adjustHonor(change) { if (!state.selectedFriend) return; const f = state.selectedFriend; const valDisplay = DOM.detailHonorValue; try { const res = await fetch(`/api/friends/${f.id}/honor`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ change }) }); const data = await res.json(); if (res.ok) { f.honor = data.honor; valDisplay.textContent = data.honor; // Animate counter valDisplay.classList.add('bump-animate'); setTimeout(() => valDisplay.classList.remove('bump-animate'), 300); // Update main state and refresh background views const fIndex = state.friends.findIndex(x => x.id === f.id); if (fIndex !== -1) { state.friends[fIndex].honor = data.honor; } renderDashboard(); renderFriendsDirectory(); } } catch (err) { console.error(err); } } // --- GENERAL HELPER FUNCTIONS --- function openModal(modal) { modal.classList.add('active'); document.body.style.overflow = 'hidden'; // prevent bg scroll } function closeModal(modal) { modal.classList.remove('active'); document.body.style.overflow = ''; } function showModal(modal) { openModal(modal); } function setFormDefaultDates() { const todayStr = new Date().toISOString().split('T')[0]; DOM.quickDate.value = todayStr; DOM.profileMeetingDate.value = todayStr; } function showGlobalLoaders() { DOM.urgentContactsList.innerHTML = '
    '; DOM.upcomingBirthdaysList.innerHTML = '
    '; DOM.friendsGrid.innerHTML = '
    '; } function getInitials(name) { if (!name) return '?'; const parts = name.trim().split(/\s+/); if (parts.length === 1) return parts[0].substring(0, 2).toUpperCase(); return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); } function getDaysSince(dateStr) { const diff = new Date() - new Date(dateStr); return Math.floor(diff / (1000 * 60 * 60 * 24)); } function formatGermanDate(dateStr, includeYear = true) { if (!dateStr) return ''; const parts = dateStr.split('-'); if (parts.length !== 3) return dateStr; const day = parts[2]; const month = parts[1]; const year = parts[0]; const months = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember']; const monthIndex = parseInt(month, 10) - 1; if (includeYear) { return `${day}. ${months[monthIndex]} ${year}`; } else { return `${day}. ${months[monthIndex].substring(0, 3)}`; } } function getDaysUntilBirthday(birthdayStr) { if (!birthdayStr) return null; const parts = birthdayStr.split('-'); if (parts.length !== 3) return null; const birthMonth = parseInt(parts[1], 10) - 1; const birthDay = parseInt(parts[2], 10); const today = new Date(); // Midnight check for perfect calculation today.setHours(0,0,0,0); const currentYear = today.getFullYear(); let nextBday = new Date(currentYear, birthMonth, birthDay); nextBday.setHours(0,0,0,0); if (nextBday < today) { nextBday.setFullYear(currentYear + 1); } const oneDay = 24 * 60 * 60 * 1000; const diffDays = Math.round((nextBday.getTime() - today.getTime()) / oneDay); return diffDays; } function getAgeTurning(birthdayStr) { if (!birthdayStr) return null; const parts = birthdayStr.split('-'); if (parts.length !== 3) return null; const birthYear = parseInt(parts[0], 10); const birthMonth = parseInt(parts[1], 10) - 1; const birthDay = parseInt(parts[2], 10); const today = new Date(); const currentYear = today.getFullYear(); let nextBday = new Date(currentYear, birthMonth, birthDay); if (nextBday < today) { return currentYear + 1 - birthYear; } return currentYear - birthYear; } function formatMarkdownParagraphs(text) { if (!text) return ''; return text .split('\n\n') .map(p => { let cleaned = p.trim(); if (!cleaned) return ''; // Basic list items formatting if it starts with hyphen if (cleaned.startsWith('-')) { const listItems = cleaned.split('\n').map(li => `
  • ${li.replace(/^-/, '').trim()}
  • `).join(''); return ``; } return `

    ${cleaned.replace(/\n/g, '
    ')}

    `; }) .join(''); } // --- BOOTSTRAP APP --- init(); });