// 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}
${waterButtonHTML}${fertilizeButtonHTML}
${(plant.care_tips || plant.latin_name) ? `
` : ''}
${plant.name}
${tipsContentHTML}
`;
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}
${waterButtonHTML}${fertilizeButtonHTML}
`;
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}
${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');
}