381 lines
15 KiB
JavaScript
381 lines
15 KiB
JavaScript
// Version: 01.10.2025 15:30 (Fix DB Autofill Logic)
|
|
import * as api from './api.js';
|
|
import * as ui from './ui.js';
|
|
import * as lexicon from './lexikon.js';
|
|
|
|
// === Globaler Zustand ===
|
|
const AppState = {
|
|
plants: [],
|
|
settings: {},
|
|
incorrectGuesses: [],
|
|
currentView: 'grid',
|
|
currentSort: 'date_desc',
|
|
fullAiAnalysisResult: null,
|
|
pendingEnrichmentPromise: null
|
|
};
|
|
|
|
// === Haupt-Initialisierung ===
|
|
async function loadInitialData() {
|
|
try {
|
|
const [plants, settings] = await Promise.all([
|
|
api.getPlants(),
|
|
api.getUserSettings()
|
|
]);
|
|
AppState.plants = plants;
|
|
AppState.settings = settings;
|
|
renderDashboard();
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Anfangsdaten:', error);
|
|
ui.showToast('App-Daten konnten nicht geladen werden.', true);
|
|
}
|
|
}
|
|
|
|
function renderDashboard() {
|
|
const sortedPlants = sortPlants(AppState.plants, AppState.currentSort);
|
|
const dashboard = document.getElementById('plant-dashboard');
|
|
if (dashboard) {
|
|
const useCompactView = window.matchMedia("(max-width: 768px)").matches && AppState.plants.length >= 3;
|
|
dashboard.classList.toggle('compact-view-enabled', useCompactView);
|
|
ui.renderPlantDashboard(sortedPlants, dashboard);
|
|
}
|
|
}
|
|
|
|
function sortPlants(plants, sortValue) {
|
|
const sorted = [...plants];
|
|
switch (sortValue) {
|
|
case 'name_asc': sorted.sort((a, b) => a.name.localeCompare(b.name)); break;
|
|
case 'watering_due':
|
|
sorted.sort((a, b) => {
|
|
const dateA = a.next_watering_due ? new Date(a.next_watering_due) : new Date('9999-12-31');
|
|
const dateB = b.next_watering_due ? new Date(b.next_watering_due) : new Date('9999-12-31');
|
|
return dateA - dateB;
|
|
});
|
|
break;
|
|
default: // 'date_desc'
|
|
sorted.sort((a, b) => {
|
|
if (String(a.id).startsWith('temp-')) return -1;
|
|
if (String(b.id).startsWith('temp-')) return 1;
|
|
return b.id - a.id;
|
|
});
|
|
break;
|
|
}
|
|
return sorted;
|
|
}
|
|
|
|
// === Event Handler ===
|
|
|
|
function fileToBase64(file) {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.readAsDataURL(file);
|
|
reader.onload = () => resolve(reader.result.split(',')[1]);
|
|
reader.onerror = error => reject(error);
|
|
});
|
|
}
|
|
|
|
// --- Dashboard & Globale Aktionen ---
|
|
async function handleDashboardClick(e) {
|
|
const actionElement = e.target.closest('[data-action]');
|
|
if (!actionElement) return;
|
|
|
|
if (actionElement.dataset.action === 'show-lexicon') {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const latinName = actionElement.dataset.latinName;
|
|
if (latinName) lexicon.openLexiconDetailModal(latinName);
|
|
return;
|
|
}
|
|
|
|
const card = e.target.closest('[data-id]');
|
|
if (!card) return;
|
|
|
|
const plantId = parseInt(card.dataset.id, 10);
|
|
const plant = AppState.plants.find(p => p.id === plantId);
|
|
if (!plant) return;
|
|
|
|
switch (actionElement.dataset.action) {
|
|
case 'show-detail': ui.openPlantDetailOverlay(plant); break;
|
|
case 'flip': case 'flip-back':
|
|
e.target.closest('.plant-card-inner')?.classList.toggle('is-flipped');
|
|
break;
|
|
case 'show-care-tips': e.target.closest('.plant-list-item')?.classList.toggle('tips-visible'); break;
|
|
case 'toggle-menu':
|
|
e.stopPropagation();
|
|
const menuContainer = actionElement.closest('.menu-container');
|
|
menuContainer.parentElement.closest('[data-id]')?.classList.add('menu-open');
|
|
menuContainer.querySelector('.menu-dropdown')?.classList.toggle('show');
|
|
break;
|
|
case 'edit':
|
|
ui.closePlantDetailOverlay();
|
|
setTimeout(() => ui.openPlantModal(plant, getModalHandlers()), 50);
|
|
break;
|
|
case 'delete':
|
|
if (confirm(`Möchtest du "${plant.name}" wirklich löschen?`)) {
|
|
try {
|
|
await api.deletePlant(plantId);
|
|
ui.closePlantDetailOverlay();
|
|
ui.showToast(`"${plant.name}" wurde gelöscht.`);
|
|
await loadInitialData();
|
|
} catch (error) { ui.showToast('Löschen fehlgeschlagen.', true); }
|
|
}
|
|
break;
|
|
case 'water': case 'fertilize':
|
|
e.stopPropagation();
|
|
actionElement.disabled = true;
|
|
actionElement.classList.add('is-loading');
|
|
try {
|
|
const { plant: updatedPlant } = await api.recordCareAction(plantId, actionElement.dataset.action);
|
|
const plantIndex = AppState.plants.findIndex(p => p.id === plantId);
|
|
if (plantIndex !== -1) AppState.plants[plantIndex] = updatedPlant;
|
|
renderDashboard();
|
|
if (document.getElementById('plantDetailOverlay').classList.contains('show')) ui.openPlantDetailOverlay(updatedPlant);
|
|
ui.showToast(actionElement.dataset.action === 'water' ? 'Gegossen!' : 'Gedüngt!');
|
|
} catch (error) { ui.showToast(`Aktion fehlgeschlagen.`, true);
|
|
} finally {
|
|
actionElement.disabled = false;
|
|
actionElement.classList.remove('is-loading');
|
|
}
|
|
break;
|
|
case 'open-diary':
|
|
ui.closePlantDetailOverlay();
|
|
setTimeout(() => ui.openDiaryModal(plant), e.target.closest('.plant-detail-overlay') ? 300 : 0);
|
|
break;
|
|
}
|
|
}
|
|
|
|
async function handleSaveSettings(settingsToUpdate) {
|
|
try {
|
|
await api.updateUserSettings(settingsToUpdate);
|
|
AppState.settings = {...AppState.settings, ...settingsToUpdate};
|
|
ui.showToast('Einstellungen gespeichert!');
|
|
} catch (error) {
|
|
ui.showToast('Einstellungen konnten nicht gespeichert werden.', true);
|
|
}
|
|
}
|
|
|
|
// --- Modal-spezifische Handler ---
|
|
function getModalHandlers() {
|
|
return {
|
|
onPhotoSelect: handlePhotoSelection,
|
|
onAnalyze: handleAiAnalysis,
|
|
onLatinNameBlur: handleLatinNameBlur,
|
|
onSubmit: handlePlantFormSubmit
|
|
};
|
|
}
|
|
|
|
async function handlePhotoSelection(e) {
|
|
const plantPhotoInput = e.target;
|
|
if (plantPhotoInput.files && plantPhotoInput.files[0]) {
|
|
const reader = new FileReader();
|
|
reader.onload = function() {
|
|
ui.showImagePreview(reader.result);
|
|
ui.showAiSection();
|
|
ui.clearAiResults(document.getElementById('plantForm'));
|
|
AppState.incorrectGuesses = [];
|
|
}
|
|
reader.readAsDataURL(plantPhotoInput.files[0]);
|
|
}
|
|
}
|
|
|
|
async function handleAiAnalysis() {
|
|
const plantPhotoInput = document.getElementById('plantPhoto');
|
|
const analyzeBtn = document.getElementById('analyzeBtn');
|
|
const plantForm = document.getElementById('plantForm');
|
|
const file = plantPhotoInput.files[0];
|
|
if (!file) return;
|
|
|
|
analyzeBtn.disabled = true;
|
|
AppState.pendingEnrichmentPromise = null;
|
|
let cleanupAnimation = () => {};
|
|
|
|
try {
|
|
cleanupAnimation = ui.runAiAnimation(document.getElementById('imagePreviewContainer'));
|
|
const base64Image = await fileToBase64(file);
|
|
const fullAnalysis = await api.performInitialAnalysis(base64Image, AppState.incorrectGuesses);
|
|
AppState.fullAiAnalysisResult = fullAnalysis;
|
|
cleanupAnimation();
|
|
|
|
const identification = fullAnalysis.identification;
|
|
if (!identification || identification.name.toLowerCase() === 'unbekannt' || identification.confidence_score < 75) {
|
|
ui.showToast(identification?.identification_notes || 'Pflanze nicht sicher erkannt.', true);
|
|
} else {
|
|
ui.populateFormWithIdentification(plantForm, identification);
|
|
plantForm.querySelector('#aiFullResponse').value = JSON.stringify(fullAnalysis);
|
|
AppState.incorrectGuesses.push(identification.latin_name.trim().toLowerCase());
|
|
document.getElementById('retryAiBtn').style.display = 'block';
|
|
|
|
const settings = AppState.settings;
|
|
const needsEnrichmentForUI = settings.auto_fill_watering || settings.auto_fill_fertilizing || settings.auto_fill_care_tips;
|
|
|
|
// Definiere den Promise, der die Daten holt
|
|
const enrichmentPromise = api.getLexiconEntryDetails(identification.latin_name)
|
|
.catch(() => api.aiEnrich(identification.latin_name));
|
|
|
|
// Weise den Promise dem globalen Zustand zu, damit die Speicherfunktion darauf warten kann
|
|
AppState.pendingEnrichmentPromise = enrichmentPromise;
|
|
|
|
// Führe die Logik aus, sobald der Promise erfüllt ist
|
|
enrichmentPromise.then(enrichmentData => {
|
|
if (enrichmentData) {
|
|
// Befülle das versteckte Feld IMMER
|
|
plantForm.querySelector('#aiEnrichmentResponse').value = JSON.stringify(enrichmentData);
|
|
|
|
// Befülle die sichtbaren Felder NUR, wenn der Nutzer es wünscht
|
|
if (settings.auto_fill_watering && plantForm.querySelector('#wateringInterval').value === '') {
|
|
plantForm.querySelector('#wateringInterval').value = enrichmentData.watering_interval_days || '';
|
|
}
|
|
if (settings.auto_fill_fertilizing && plantForm.querySelector('#fertilizingInterval').value === '') {
|
|
plantForm.querySelector('#fertilizingInterval').value = enrichmentData.fertilizing_interval_days || '';
|
|
}
|
|
if (settings.auto_fill_care_tips && plantForm.querySelector('#careTips').value === '') {
|
|
plantForm.querySelector('#careTips').value = enrichmentData.care_tips || '';
|
|
}
|
|
}
|
|
}).catch(err => console.error("Fehler bei der Datenanreicherung:", err));
|
|
|
|
// Zeige den Lade-Spinner NUR, wenn der Nutzer die Daten auch sehen will
|
|
if (needsEnrichmentForUI) {
|
|
ui.showEnrichmentLoader(true);
|
|
enrichmentPromise.finally(() => ui.showEnrichmentLoader(false));
|
|
}
|
|
}
|
|
} catch (error) {
|
|
ui.showToast(error.message, true);
|
|
cleanupAnimation();
|
|
} finally {
|
|
analyzeBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
async function handleLatinNameBlur(e) {
|
|
const latinName = e.target.value.trim();
|
|
if (latinName) {
|
|
try {
|
|
const lexiconEntry = await api.getLexiconEntryDetails(latinName);
|
|
// Hier nutzen wir dieselbe Logik wie oben, um die Felder basierend auf den Settings zu befüllen
|
|
const form = document.getElementById('plantForm');
|
|
const settings = AppState.settings;
|
|
if (settings.auto_fill_watering && form.querySelector('#wateringInterval').value === '') {
|
|
form.querySelector('#wateringInterval').value = lexiconEntry.watering_interval_days || '';
|
|
}
|
|
if (settings.auto_fill_fertilizing && form.querySelector('#fertilizingInterval').value === '') {
|
|
form.querySelector('#fertilizingInterval').value = lexiconEntry.fertilizing_interval_days || '';
|
|
}
|
|
if (settings.auto_fill_care_tips && form.querySelector('#careTips').value === '') {
|
|
form.querySelector('#careTips').value = lexiconEntry.care_tips || '';
|
|
}
|
|
form.querySelector('#aiEnrichmentResponse').value = JSON.stringify(lexiconEntry);
|
|
ui.showToast('Daten aus Lexikon geladen!', false);
|
|
} catch (error) { /* Kein Eintrag gefunden, still bleiben */ }
|
|
}
|
|
}
|
|
|
|
function handlePlantFormSubmit(e) {
|
|
e.preventDefault();
|
|
const form = e.target;
|
|
const formData = new FormData(form);
|
|
|
|
const tempId = `temp-${Date.now()}`;
|
|
const tempPlant = {
|
|
id: tempId,
|
|
name: formData.get('name'),
|
|
photo: document.getElementById('imagePreview').src,
|
|
latin_name: formData.get('latin_name'),
|
|
watering_interval: null,
|
|
fertilizing_interval: null,
|
|
next_watering_due: null,
|
|
next_fertilizing_due: null,
|
|
care_tips: 'Wird geladen...'
|
|
};
|
|
|
|
AppState.plants.unshift(tempPlant);
|
|
renderDashboard();
|
|
|
|
ui.closePlantModal();
|
|
ui.showToast('Pflanze wird gespeichert...');
|
|
|
|
(async () => {
|
|
try {
|
|
if (AppState.pendingEnrichmentPromise) {
|
|
await AppState.pendingEnrichmentPromise;
|
|
}
|
|
|
|
const { plant: finalPlant } = await api.savePlant(new FormData(form));
|
|
|
|
const plantIndex = AppState.plants.findIndex(p => p.id === tempId);
|
|
if (plantIndex !== -1) {
|
|
AppState.plants[plantIndex] = finalPlant;
|
|
} else {
|
|
AppState.plants.shift();
|
|
AppState.plants.unshift(finalPlant);
|
|
}
|
|
|
|
renderDashboard();
|
|
ui.showToast('Pflanze erfolgreich gespeichert!');
|
|
|
|
} catch (error) {
|
|
ui.showToast(`Speichern fehlgeschlagen: ${error.message}`, true);
|
|
AppState.plants = AppState.plants.filter(p => p.id !== tempId);
|
|
renderDashboard();
|
|
} finally {
|
|
AppState.pendingEnrichmentPromise = null;
|
|
}
|
|
})();
|
|
}
|
|
|
|
|
|
// === Initialisierung der globalen Listener ===
|
|
function init() {
|
|
document.getElementById('addPlantBtn')?.addEventListener('click', () => ui.openPlantModal(null, getModalHandlers()));
|
|
document.getElementById('settingsBtn')?.addEventListener('click', async () => {
|
|
try {
|
|
const settings = await api.getUserSettings();
|
|
AppState.settings = settings;
|
|
ui.openSettingsModal(settings, handleSaveSettings);
|
|
} catch(e){ ui.showToast("Einstellungen konnten nicht geladen werden.", true); }
|
|
});
|
|
|
|
document.getElementById('plant-dashboard')?.addEventListener('click', handleDashboardClick);
|
|
document.getElementById('sortSelect')?.addEventListener('change', (e) => {
|
|
AppState.currentSort = e.target.value;
|
|
renderDashboard();
|
|
});
|
|
|
|
document.getElementById('viewGridBtn')?.addEventListener('click', () => {
|
|
AppState.currentView = 'grid';
|
|
document.getElementById('plant-dashboard')?.classList.remove('list-view');
|
|
document.getElementById('viewGridBtn')?.classList.add('active');
|
|
document.getElementById('viewListBtn')?.classList.remove('active');
|
|
});
|
|
|
|
document.getElementById('viewListBtn')?.addEventListener('click', () => {
|
|
AppState.currentView = 'list';
|
|
document.getElementById('plant-dashboard')?.classList.add('list-view');
|
|
document.getElementById('viewListBtn')?.classList.add('active');
|
|
document.getElementById('viewGridBtn')?.classList.remove('active');
|
|
});
|
|
|
|
const plantDetailOverlay = document.getElementById('plantDetailOverlay');
|
|
plantDetailOverlay?.addEventListener('click', (e) => {
|
|
if (e.target === plantDetailOverlay) {
|
|
ui.closePlantDetailOverlay();
|
|
} else {
|
|
handleDashboardClick(e);
|
|
}
|
|
});
|
|
document.getElementById('plantDetailCloseBtn')?.addEventListener('click', ui.closePlantDetailOverlay);
|
|
|
|
window.addEventListener('click', (e) => {
|
|
if (!e.target.closest('.menu-container')) {
|
|
document.querySelectorAll('.menu-dropdown.show').forEach(menu => {
|
|
menu.classList.remove('show');
|
|
menu.closest('[data-id]')?.classList.remove('menu-open');
|
|
});
|
|
}
|
|
});
|
|
|
|
loadInitialData();
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
|