553 lines
32 KiB
JavaScript
553 lines
32 KiB
JavaScript
// 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 = `<div class="empty-state"><h2>Noch keine Pflanzen hier!</h2><p>Klicke oben auf "hinzufügen", um deine grüne Oase zu starten.</p></div>`;
|
|
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 = `<button class="action-btn water-btn" data-action="water"><div class="action-btn-progress" style="width: ${100 - progress.percent}%;" data-status="${progress.status}"></div><div class="action-btn-content"><span>Gießen</span><span class="care-status">${getStatusText(diff)}</span></div></button>`;
|
|
compactWaterPillHTML = `<button class="compact-action-pill" data-action="water"><div class="compact-action-progress" style="width: ${100 - progress.percent}%;" data-status="${progress.status}"></div><div class="compact-action-content"><div class="action-label"><span class="action-text">Gießen</span><span class="action-emoji">💧</span></div><span class="care-status">${getStatusText(diff, true)}</span></div></button>`;
|
|
} else {
|
|
waterButtonHTML = `<button class="action-btn water-btn" data-action="water"><div class="action-btn-progress" style="width: 0%;" data-status="disabled"></div><div class="action-btn-content"><span>Gießen</span><span class="care-status">${formatLastDate(plant.last_watered)}</span></div></button>`;
|
|
compactWaterPillHTML = `<button class="compact-action-pill" data-action="water"><div class="compact-action-progress" style="width: 0%;" data-status="disabled"></div><div class="compact-action-content"><div class="action-label"><span class="action-text">Gießen</span><span class="action-emoji">💧</span></div><span class="care-status">${formatLastDate(plant.last_watered)}</span></div></button>`;
|
|
}
|
|
|
|
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 = `<button class="action-btn fertilize-btn" data-action="fertilize"><div class="action-btn-progress" style="width: ${100 - progress.percent}%;" data-status="${progress.status}"></div><div class="action-btn-content"><span>Düngen</span><span class="care-status">${getStatusText(diff)}</span></div></button>`;
|
|
compactFertilizePillHTML = `<button class="compact-action-pill" data-action="fertilize"><div class="compact-action-progress" style="width: ${100 - progress.percent}%;" data-status="${progress.status}"></div><div class="compact-action-content"><div class="action-label"><span class="action-text">Düngen</span><span class="action-emoji">🌱</span></div><span class="care-status">${getStatusText(diff, true)}</span></div></button>`;
|
|
} else {
|
|
fertilizeButtonHTML = `<button class="action-btn fertilize-btn" data-action="fertilize"><div class="action-btn-progress" style="width: 0%;" data-status="disabled"></div><div class="action-btn-content"><span>Düngen</span><span class="care-status">${formatLastDate(plant.last_fertilized)}</span></div></button>`;
|
|
compactFertilizePillHTML = `<button class="compact-action-pill" data-action="fertilize"><div class="compact-action-progress" style="width: 0%;" data-status="disabled"></div><div class="compact-action-content"><div class="action-label"><span class="action-text">Düngen</span><span class="action-emoji">🌱</span></div><span class="care-status">${formatLastDate(plant.last_fertilized)}</span></div></button>`;
|
|
}
|
|
|
|
const placeholderImg = 'https://placehold.co/600x600/eeeeee/3D403D?text=' + encodeURIComponent(plant.name);
|
|
let tipsContentHTML = plant.latin_name ? `<div class="care-tips-latin-name" data-action="show-lexicon" data-latin-name="${plant.latin_name}" title="Lexikoneintrag öffnen"><div><strong>Botanisch:</strong> <em>${plant.latin_name}</em></div><span class="latin-name-hint">Mehr erfahren →</span></div>` : '';
|
|
if (plant.care_tips) {
|
|
tipsContentHTML += plant.care_tips.split(/\n\s*\n/).map(tip => `<p>${tip.replace(/(\p{Emoji}[^:]+:)/gu, '<strong>$1</strong>')}</p>`).join('');
|
|
}
|
|
if (!plant.care_tips && !plant.latin_name) {
|
|
tipsContentHTML = '<p>Keine Pflegetipps hinterlegt.</p>';
|
|
}
|
|
|
|
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 = `<div class="plant-card-inner"><div class="plant-card-front"><div class="plant-img-container"><img src="${plant.photo || placeholderImg}" alt="${plant.name}" class="plant-img" onerror="this.src='${placeholderImg}'"><div class="plant-name-overlay"><h3>${plant.name}</h3></div></div><div class="plant-info"><div class="card-actions">${waterButtonHTML}${fertilizeButtonHTML}</div></div><div class="card-overlay"><button class="card-btn" data-action="open-diary" title="Tagebuch"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg></button>${(plant.care_tips || plant.latin_name) ? `<button class="card-btn" data-action="flip" title="Pflegetipps"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></button>` : ''}<div class="menu-container"><button class="card-btn" data-action="toggle-menu" title="Menü"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="menu-dropdown"><button data-action="edit">Bearbeiten</button><button data-action="delete" class="delete-btn">Löschen</button></div></div></div></div><div class="plant-card-back" data-action="flip-back"><h3>${plant.name}</h3><div class="care-tips-content">${tipsContentHTML}</div><div class="back-controls"><span><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg> Zum Umdrehen klicken</span></div></div></div>`;
|
|
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 = `<div class="list-item-img-container"><img src="${plant.photo || placeholderImg}" alt="${plant.name}" class="list-item-img" onerror="this.src='${placeholderImg}'"></div><div class="list-item-info"><h3>${plant.name}</h3></div><div class="list-item-actions">${waterButtonHTML}${fertilizeButtonHTML}</div><div class="list-item-menu"><button class="card-btn" data-action="open-diary" title="Tagebuch"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg></button>${(plant.care_tips || plant.latin_name) ? `<button class="card-btn" data-action="show-care-tips" title="Pflegetipps"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></button>` : ''}<div class="menu-container"><button class="card-btn" data-action="toggle-menu" title="Menü"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="menu-dropdown"><button data-action="edit">Bearbeiten</button><button data-action="delete" class="delete-btn">Löschen</button></div></div></div><div class="list-item-care-tips"><div class="list-item-care-tips-content">${tipsContentHTML}</div></div>`;
|
|
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 = `<div class="compact-img-container" data-action="show-detail"><img src="${plant.photo || placeholderImg}" alt="${plant.name}" class="compact-img" onerror="this.src='${placeholderImg}'"><div class="compact-name-overlay">${plant.name}</div></div><div class="compact-card-actions">${compactWaterPillHTML}${compactFertilizePillHTML}</div>`;
|
|
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 = '<p class="empty-diary">Lade Einträge...</p>';
|
|
|
|
api.getDiaryEntries(plant.id)
|
|
.then(entries => renderDiaryEntries(entries, plant.id))
|
|
.catch(() => {
|
|
if(diaryContent) diaryContent.innerHTML = '<p class="empty-diary">Einträge konnten nicht geladen werden.</p>';
|
|
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 = '<p class="empty-diary">Noch keine Einträge vorhanden.</p>';
|
|
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 class="diary-photo-container"><img src="${entry.photo}" class="diary-photo"><button class="card-btn set-profile-btn" data-action="set-profile-pic" title="Als Profilbild"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg></button></div>` : '';
|
|
div.innerHTML = `<div class="diary-icon">${icons[effectiveType] || '📌'}</div><div class="diary-entry-content"><button class="card-btn delete-diary-entry-btn" data-action="delete-diary-entry" title="Löschen"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg></button><p class="diary-entry-title">${titles[effectiveType] || 'Eintrag'}</p>${(entry.notes && effectiveType !== 'added') ? `<p class="diary-entry-notes">${entry.notes.replace(/\n/g, '<br>')}</p>` : ''}${photoHTML}<span class="diary-entry-time">${new Date(entry.entry_date).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}</span></div>`;
|
|
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');
|
|
}
|
|
|