// Version: 01.10.2025 11:30 (FINAL LEXICON FIX) import * as api from './api.js'; // === Zustandsvariablen für Event Listener === let activeModalListeners = new Map(); let settingsDebounceTimer = null; let pendingSettings = {}; // === Hilfsfunktionen für Event Listener === function addManagedListener(element, event, handler) { if (element) { element.addEventListener(event, handler); if (!activeModalListeners.has(element)) { activeModalListeners.set(element, []); } activeModalListeners.get(element).push({ event, handler }); } } function removeAllManagedListeners() { activeModalListeners.forEach((listeners, element) => { listeners.forEach(({ event, handler }) => { element.removeEventListener(event, handler); }); }); activeModalListeners.clear(); } // === Allgemeine UI-Funktionen === export function showToast(message, isError = false) { const toast = document.createElement('div'); toast.className = `toast ${isError ? 'error' : ''}`; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => toast.classList.add('show'), 10); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => document.body.removeChild(toast), 300); }, 3000); } // === Pflanzen-Dashboard & Karten === export function renderPlantDashboard(plants, dashboardElement) { dashboardElement.innerHTML = ''; const dashboardControls = document.querySelector('.dashboard-controls'); if (plants.length === 0) { dashboardElement.innerHTML = `

Noch keine Pflanzen hier!

Klicke oben auf "hinzufügen", um deine grüne Oase zu starten.

`; if (dashboardControls) dashboardControls.style.display = 'none'; return; } if (dashboardControls) dashboardControls.style.display = 'flex'; plants.forEach(plant => { dashboardElement.appendChild(createPlantCardElement(plant)); dashboardElement.appendChild(createPlantListItemElement(plant)); dashboardElement.appendChild(createPlantCompactCardElement(plant)); }); } function formatLastDate(dateString) { if (!dateString || dateString === '0000-00-00') return ''; const lastDate = new Date(dateString); const today = new Date(); today.setHours(0, 0, 0, 0); const yesterday = new Date(today); yesterday.setDate(today.getDate() - 1); lastDate.setHours(0, 0, 0, 0); if (lastDate.getTime() === today.getTime()) return 'heute'; if (lastDate.getTime() === yesterday.getTime()) return 'gestern'; return `am ${lastDate.toLocaleString('de-DE', { day: '2-digit', month: '2-digit' })}`; } function createPlantCardHTML(plant) { const today = new Date(); today.setHours(0, 0, 0, 0); const msPerDay = 1000 * 60 * 60 * 24; const getProgress = (diff, interval) => { if (diff <= 0) return { percent: 100, status: 'red' }; if (diff >= interval) return { percent: 0, status: 'green' }; return { percent: 100 - ((diff / interval) * 100), status: (diff / interval) < 0.25 ? 'yellow' : 'green' }; }; const getStatusText = (days, compact = false) => { if (days < 0) return `fällig!`; if (days === 0) return `heute`; if (days === 1) return `morgen`; return `in ${days} ${compact ? 'T.' : 'Tagen'}`; }; let waterButtonHTML, compactWaterPillHTML; if (plant.watering_interval > 0 && plant.next_watering_due) { const nextWatering = new Date(plant.next_watering_due + 'T00:00:00'); const diff = Math.round((nextWatering - today) / msPerDay); const progress = getProgress(diff, plant.watering_interval); waterButtonHTML = ``; compactWaterPillHTML = ``; } else { waterButtonHTML = ``; compactWaterPillHTML = ``; } let fertilizeButtonHTML, compactFertilizePillHTML; if (plant.fertilizing_interval > 0 && plant.next_fertilizing_due) { const nextFertilizing = new Date(plant.next_fertilizing_due + 'T00:00:00'); const diff = Math.round((nextFertilizing - today) / msPerDay); const progress = getProgress(diff, plant.fertilizing_interval); fertilizeButtonHTML = ``; compactFertilizePillHTML = ``; } else { fertilizeButtonHTML = ``; compactFertilizePillHTML = ``; } const placeholderImg = 'https://placehold.co/600x600/eeeeee/3D403D?text=' + encodeURIComponent(plant.name); let tipsContentHTML = plant.latin_name ? `
Botanisch: ${plant.latin_name}
Mehr erfahren →
` : ''; if (plant.care_tips) { tipsContentHTML += plant.care_tips.split(/\n\s*\n/).map(tip => `

${tip.replace(/(\p{Emoji}[^:]+:)/gu, '$1')}

`).join(''); } if (!plant.care_tips && !plant.latin_name) { tipsContentHTML = '

Keine Pflegetipps hinterlegt.

'; } return { waterButtonHTML, fertilizeButtonHTML, placeholderImg, tipsContentHTML, compactWaterPillHTML, compactFertilizePillHTML }; } function createPlantCardElement(plant) { const cardWrapper = document.createElement('div'); cardWrapper.className = 'plant-card'; cardWrapper.dataset.id = plant.id; const { waterButtonHTML, fertilizeButtonHTML, placeholderImg, tipsContentHTML } = createPlantCardHTML(plant); cardWrapper.innerHTML = `
${plant.name}

${plant.name}

${waterButtonHTML}${fertilizeButtonHTML}
${(plant.care_tips || plant.latin_name) ? `` : ''}

${plant.name}

${tipsContentHTML}
Zum Umdrehen klicken
`; return cardWrapper; } function createPlantListItemElement(plant) { const listItem = document.createElement('div'); listItem.className = 'plant-list-item'; listItem.dataset.id = plant.id; const { waterButtonHTML, fertilizeButtonHTML, placeholderImg, tipsContentHTML } = createPlantCardHTML(plant); listItem.innerHTML = `
${plant.name}

${plant.name}

${waterButtonHTML}${fertilizeButtonHTML}
${(plant.care_tips || plant.latin_name) ? `` : ''}
${tipsContentHTML}
`; return listItem; } function createPlantCompactCardElement(plant) { const cardWrapper = document.createElement('div'); cardWrapper.className = 'plant-compact-card'; cardWrapper.dataset.id = plant.id; const { placeholderImg, compactWaterPillHTML, compactFertilizePillHTML } = createPlantCardHTML(plant); cardWrapper.innerHTML = `
${plant.name}
${plant.name}
${compactWaterPillHTML}${compactFertilizePillHTML}
`; return cardWrapper; } // === Pflanzen-Modal === export function openPlantModal(plant = null, handlers = {}) { const plantModal = document.getElementById('plantModal'); const plantForm = document.getElementById('plantForm'); if (!plantModal || !plantForm) return; plantForm.reset(); clearAiResults(plantForm); if (plant) { plantModal.querySelector('#modalTitle').textContent = 'Pflanze bearbeiten'; document.getElementById('ai-analysis-section').style.display = 'none'; plantForm.querySelector('#plantId').value = plant.id; plantForm.querySelector('#plantName').value = plant.name; plantForm.querySelector('#latinName').value = plant.latin_name || ''; plantForm.querySelector('#wateringInterval').value = plant.watering_interval > 0 ? plant.watering_interval : ''; plantForm.querySelector('#fertilizingInterval').value = plant.fertilizing_interval > 0 ? plant.fertilizing_interval : ''; plantForm.querySelector('#careTips').value = plant.care_tips || ''; plantForm.querySelector('#currentPhoto').value = plant.photo || ''; if (plant.photo) showImagePreview(plant.photo); else { document.getElementById('initialUploadArea').style.display = 'flex'; document.getElementById('imagePreviewContainer').style.display = 'none'; } } else { plantModal.querySelector('#modalTitle').textContent = 'Neue Pflanze hinzufügen'; document.getElementById('ai-analysis-section').style.display = 'none'; document.getElementById('initialUploadArea').style.display = 'flex'; document.getElementById('imagePreviewContainer').style.display = 'none'; } addManagedListener(plantForm, 'submit', handlers.onSubmit); addManagedListener(document.getElementById('plantPhoto'), 'change', handlers.onPhotoSelect); addManagedListener(document.getElementById('analyzeBtn'), 'click', handlers.onAnalyze); addManagedListener(document.getElementById('retryAiBtn'), 'click', handlers.onAnalyze); addManagedListener(document.getElementById('latinName'), 'blur', handlers.onLatinNameBlur); addManagedListener(document.getElementById('startCameraBtn'), 'click', () => startCamera('main')); addManagedListener(document.getElementById('retakePhotoBtn'), 'click', () => startCamera('main')); addManagedListener(document.getElementById('captureBtn'), 'click', () => capturePhoto('main')); addManagedListener(plantModal.querySelector('.close-btn'), 'click', closePlantModal); addManagedListener(plantModal, 'click', (e) => { if (e.target === plantModal) closePlantModal(); }); plantModal.classList.add('show'); document.body.classList.add('modal-open'); } export function closePlantModal() { const plantModal = document.getElementById('plantModal'); if (plantModal) { stopCamera('main'); removeAllManagedListeners(); plantModal.classList.remove('show'); if (!document.querySelector('.modal.show')) { document.body.classList.remove('modal-open'); } } } // === Detail Overlay (Mobile) === export function openPlantDetailOverlay(plant) { const plantDetailOverlay = document.getElementById('plantDetailOverlay'); const plantDetailContent = document.getElementById('plantDetailContent'); if (plantDetailOverlay && plantDetailContent) { plantDetailContent.innerHTML = ''; plantDetailContent.appendChild(createPlantCardElement(plant)); plantDetailOverlay.classList.add('show'); document.body.classList.add('modal-open'); } } export function closePlantDetailOverlay() { const plantDetailOverlay = document.getElementById('plantDetailOverlay'); if (plantDetailOverlay) { plantDetailOverlay.classList.remove('show'); if (!document.querySelector('.modal.show')) { document.body.classList.remove('modal-open'); } } } // === KI UI-Funktionen === export function showAiSection() { document.getElementById('ai-analysis-section').style.display = 'flex'; } export function clearAiResults(form) { if (!form) return; form.querySelectorAll('input[name^="ai_"]').forEach(el => el.value = ''); document.getElementById('retryAiBtn').style.display = 'none'; showEnrichmentLoader(false); } export function runAiAnimation(container) { if(!container) return () => {}; const analysisTextElement = container.querySelector('.ai-analysis-text'); const analysisMessages = ["Initialisiere Scan", "Analysiere Blattform", "Extrahiere Merkmale", "Vergleiche Datenbank", "Bestimme Gattung", "Finalisiere..."]; let messageIndex = 0; container.classList.add('scanning'); if(analysisTextElement) analysisTextElement.textContent = analysisMessages[messageIndex]; const textInterval = setInterval(() => { messageIndex = (messageIndex + 1) % analysisMessages.length; if(analysisTextElement) analysisTextElement.textContent = analysisMessages[messageIndex]; }, 1500); return function cleanup() { clearInterval(textInterval); container.classList.remove('scanning'); }; } function ensureEnrichmentLoaders() { ['wateringInterval', 'fertilizingInterval', 'careTips'].forEach(fieldId => { const label = document.querySelector(`label[for="${fieldId}"]`); if (label && !label.querySelector('.enrichment-loader')) { const loader = document.createElement('span'); loader.className = 'enrichment-loader'; loader.style.display = 'none'; label.appendChild(loader); } }); } export function showEnrichmentLoader(isLoading) { ensureEnrichmentLoaders(); document.querySelectorAll('.enrichment-loader').forEach(loader => { loader.style.display = isLoading ? 'inline-block' : 'none'; }); } export function populateFormWithIdentification(form, idData) { form.querySelector('#plantName').value = idData.name || ''; form.querySelector('#latinName').value = idData.latin_name || ''; } export function populateFormWithEnrichment(form, enrichmentData, settings) { if (settings.auto_fill_watering && form.querySelector('#wateringInterval').value === '') { form.querySelector('#wateringInterval').value = enrichmentData.watering_interval_days || ''; } if (settings.auto_fill_fertilizing && form.querySelector('#fertilizingInterval').value === '') { form.querySelector('#fertilizingInterval').value = enrichmentData.fertilizing_interval_days || ''; } if (settings.auto_fill_care_tips && form.querySelector('#careTips').value === '') { form.querySelector('#careTips').value = enrichmentData.care_tips || ''; } const enrichmentField = form.querySelector('input[name="ai_enrichment_json"]'); if (enrichmentField) { enrichmentField.value = JSON.stringify(enrichmentData); } } // === Tagebuch-Modal === export function openDiaryModal(plant) { const diaryModal = document.getElementById('diaryModal'); if(!diaryModal) return; diaryModal.querySelector('#diaryPlantName').textContent = plant.name; diaryModal.querySelector('#diaryPlantId').value = plant.id; document.getElementById('diaryForm')?.reset(); removeDiaryPhoto(); const diaryContent = document.getElementById('diaryContent'); if(diaryContent) diaryContent.innerHTML = '

Lade Einträge...

'; api.getDiaryEntries(plant.id) .then(entries => renderDiaryEntries(entries, plant.id)) .catch(() => { if(diaryContent) diaryContent.innerHTML = '

Einträge konnten nicht geladen werden.

'; showToast('Tagebuch konnte nicht geladen werden.', true); }); addManagedListener(document.getElementById('diaryForm'), 'submit', handleDiaryFormSubmit); addManagedListener(diaryModal.querySelector('.close-btn'), 'click', closeDiaryModal); addManagedListener(diaryModal, 'click', (e) => { if (e.target === diaryModal) closeDiaryModal(); }); diaryModal.classList.add('show'); document.body.classList.add('modal-open'); } async function handleDiaryFormSubmit(e) { e.preventDefault(); const diaryForm = document.getElementById('diaryForm'); const saveBtn = diaryForm.querySelector('button[type="submit"]'); saveBtn.disabled = true; try { const result = await api.addDiaryEntry(new FormData(diaryForm)); if(result.success) { showToast('Eintrag gespeichert!'); const plantId = result.entry.plant_id; const entries = await api.getDiaryEntries(plantId); renderDiaryEntries(entries, plantId); diaryForm.reset(); removeDiaryPhoto(); } else { throw new Error(result.error); } } catch (error) { showToast('Eintrag konnte nicht gespeichert werden.', true); } finally { saveBtn.disabled = false; } } export function closeDiaryModal() { const diaryModal = document.getElementById('diaryModal'); if (diaryModal) { stopCamera('diary'); removeAllManagedListeners(); diaryModal.classList.remove('show'); if (!document.querySelector('.modal.show')) { document.body.classList.remove('modal-open'); } } } export function renderDiaryEntries(entries, plantId) { const diaryContent = document.getElementById('diaryContent'); if (!diaryContent) return; diaryContent.innerHTML = ''; if (entries.length === 0) { diaryContent.innerHTML = '

Noch keine Einträge vorhanden.

'; return; } const groupedEntries = entries.reduce((acc, entry) => { const date = new Date(entry.entry_date).toLocaleDateString('de-DE', { year: 'numeric', month: 'long', day: 'numeric' }); if (!acc[date]) acc[date] = []; acc[date].push(entry); return acc; }, {}); for (const date in groupedEntries) { const dateHeader = document.createElement('h4'); dateHeader.className = 'diary-date-header'; dateHeader.textContent = date; diaryContent.appendChild(dateHeader); groupedEntries[date].forEach(entry => diaryContent.appendChild(createDiaryEntryElement(entry, plantId))); } } function createDiaryEntryElement(entry, plantId) { const div = document.createElement('div'); div.className = `diary-entry ${entry.entry_type === 'milestone' ? 'is-milestone' : ''}`; div.dataset.plantId = plantId; div.dataset.entryId = entry.id; if(entry.photo) div.dataset.photo = entry.photo; const icons = { manual: '📝', watered: '💧', fertilized: '🌱', milestone: '⭐', added: '🎉' }; const titles = { manual: 'Notiz', watered: 'Gegossen', fertilized: 'Gedüngt', milestone: 'Meilenstein', added: 'Pflanze hinzugefügt' }; let effectiveType = (entry.entry_type === 'milestone' && entry.notes === 'Pflanze hinzugefügt') ? 'added' : entry.entry_type; let photoHTML = entry.photo ? `
` : ''; div.innerHTML = `
${icons[effectiveType] || '📌'}

${titles[effectiveType] || 'Eintrag'}

${(entry.notes && effectiveType !== 'added') ? `

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

` : ''}${photoHTML}${new Date(entry.entry_date).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
`; return div; } export function removeDiaryPhoto() { const diaryPhotoInput = document.getElementById('diaryPhoto'); if(diaryPhotoInput) diaryPhotoInput.value = ''; document.getElementById('diaryImagePreviewContainer').style.display = 'none'; document.getElementById('diaryInitialUploadArea').style.display = 'flex'; } // === Settings Modal --- function handleSettingsChange(e, onSave) { const target = e.target; let key, value; if (target.matches('.toggle-switch input')) { key = target.id.replace(/-/g, '_'); value = target.checked; } else if (target.matches('.theme-btn')) { key = 'theme'; value = target.dataset.theme; document.body.dataset.theme = value; document.querySelectorAll('.theme-btn').forEach(btn => btn.classList.remove('active')); target.classList.add('active'); } else { return; } pendingSettings[key] = value; clearTimeout(settingsDebounceTimer); settingsDebounceTimer = setTimeout(() => { if (Object.keys(pendingSettings).length > 0) { onSave(pendingSettings); pendingSettings = {}; } }, 750); } export function openSettingsModal(data, onSave) { const modal = document.getElementById('settingsModal'); if (modal) { populateSettingsModal(data); addManagedListener(modal, 'change', (e) => handleSettingsChange(e, onSave)); addManagedListener(modal, 'click', (e) => handleSettingsChange(e, onSave)); addManagedListener(modal.querySelector('.close-btn'), 'click', closeSettingsModal); addManagedListener(modal, 'click', (e) => { if(e.target === modal) closeSettingsModal(); }); modal.classList.add('show'); document.body.classList.add('modal-open'); } } export function closeSettingsModal() { const modal = document.getElementById('settingsModal'); if (modal) { removeAllManagedListeners(); modal.classList.remove('show'); if (!document.querySelector('.modal.show')) { document.body.classList.remove('modal-open'); } } } function populateSettingsModal(data) { if (!data) return; const safeSet = (id, property, value) => { const el = document.getElementById(id); if (el) el[property] = value; }; safeSet('settings-profile-pic', 'src', data.profile_image || `https://placehold.co/100x100/eeeeee/3D403D?text=${data.username.charAt(0)}`); safeSet('settings-username', 'textContent', data.username); safeSet('stats-plant-count', 'textContent', data.plant_count); safeSet('stats-ai-credits', 'textContent', data.ai_credits); safeSet('allow-photo-sharing', 'checked', data.allow_photo_sharing == 1); safeSet('auto-fill-watering', 'checked', data.auto_fill_watering == 1); safeSet('auto-fill-fertilizing', 'checked', data.auto_fill_fertilizing == 1); safeSet('auto-fill-care-tips', 'checked', data.auto_fill_care_tips == 1); document.querySelectorAll('.theme-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.theme === data.theme); }); } // === Kamera UI-Funktionen === let cameraStream = null; export function showImagePreview(src) { const imagePreview = document.getElementById('imagePreview'); const imagePreviewContainer = document.getElementById('imagePreviewContainer'); const initialUploadArea = document.getElementById('initialUploadArea'); if (imagePreview) imagePreview.src = src; if (imagePreviewContainer) imagePreviewContainer.style.display = 'block'; if (initialUploadArea) initialUploadArea.style.display = 'none'; } export async function startCamera(context = 'main') { if (cameraStream) stopCamera(context); const elements = context === 'main' ? { view: document.getElementById('cameraView'), form: document.getElementById('formContent'), video: document.getElementById('videoElement') } : { view: document.getElementById('diaryCameraView'), form: document.getElementById('diaryContentWrapper'), video: document.getElementById('diaryVideoElement'), footer: document.querySelector('#diaryModal .modal-footer') }; if (!elements.view || !elements.form || !elements.video) return; try { cameraStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment', width: 1080, height: 1080 } }); elements.video.srcObject = cameraStream; elements.form.style.display = 'none'; if(elements.footer) elements.footer.style.display = 'none'; elements.view.style.display = 'block'; } catch (err) { showToast("Kamerazugriff verweigert.", true); } } export function stopCamera(context = 'main') { if (cameraStream) { cameraStream.getTracks().forEach(track => track.stop()); cameraStream = null; } const elements = context === 'main' ? { view: document.getElementById('cameraView'), form: document.getElementById('formContent') } : { view: document.getElementById('diaryCameraView'), form: document.getElementById('diaryContentWrapper'), footer: document.querySelector('#diaryModal .modal-footer') }; if (elements.view) elements.view.style.display = 'none'; if (elements.form) elements.form.style.display = 'block'; if (elements.footer) elements.footer.style.display = 'block'; } export function capturePhoto(context = 'main') { const elements = context === 'main' ? { canvas: document.getElementById('photoCanvas'), video: document.getElementById('videoElement'), input: document.getElementById('plantPhoto'), showPreview: showImagePreview } : { canvas: document.getElementById('diaryPhotoCanvas'), video: document.getElementById('diaryVideoElement'), input: document.getElementById('diaryPhoto'), showPreview: (files) => document.getElementById('plantPhoto').dispatchEvent(new CustomEvent('change', { detail: files })) }; if (!elements.canvas || !elements.video || !elements.input) return; const context2d = elements.canvas.getContext('2d'); const size = Math.min(elements.video.videoWidth, elements.video.videoHeight); elements.canvas.width = size; elements.canvas.height = size; context2d.drawImage(elements.video, (elements.video.videoWidth - size) / 2, (elements.video.videoHeight - size) / 2, size, size, 0, 0, size, size); const dataUrl = elements.canvas.toDataURL('image/webp'); if (context === 'main') { elements.showPreview(dataUrl); showAiSection(); clearAiResults(document.getElementById('plantForm')); } elements.canvas.toBlob(blob => { const imageFile = new File([blob], `aufnahme.webp`, { type: 'image/webp' }); const dataTransfer = new DataTransfer(); dataTransfer.items.add(imageFile); elements.input.files = dataTransfer.files; // Manually trigger change event const changeEvent = new Event('change', { bubbles: true }); elements.input.dispatchEvent(changeEvent); stopCamera(context); }, 'image/webp'); }