philippurbschat.de/plants/main.js

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);