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.message}
${err.message}
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 = `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 = `Passe deinen Suchbegriff an oder füge einen neuen Freund hinzu.
${lastMeetSub}
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 = `${m.details.replace(/\n/g, '
')}
${cleaned.replace(/\n/g, '
')}