document.addEventListener('DOMContentLoaded', () => { const uploadArea = document.getElementById('upload-area'); const fileInput = document.getElementById('file-input'); const imagePreview = document.getElementById('image-preview'); const uploadContent = document.querySelector('.upload-content'); const analyzeBtn = document.getElementById('analyze-btn'); const btnText = document.querySelector('.btn-text'); const loader = document.querySelector('.loader'); const resultsPlaceholder = document.getElementById('results-placeholder'); const resultsContent = document.getElementById('results-content'); const mainDisease = document.getElementById('main-disease'); const chartCanvas = document.getElementById('resultsChart'); const cropsGallery = document.getElementById('crops-gallery'); let selectedFile = null; let chartInstance = null; // Tabs const tabIndividual = document.getElementById('tab-individual'); const tabBatch = document.getElementById('tab-batch'); const wrapperIndividual = document.getElementById('wrapper-individual'); const wrapperBatch = document.getElementById('wrapper-batch'); const tabAppCafe = document.getElementById('tab-appcafe'); const wrapperAppCafe = document.getElementById('wrapper-appcafe'); // Supabase Auth Elements const appcafeLoginForm = document.getElementById('appcafe-login-form'); const appcafeEmailInput = document.getElementById('appcafe-email'); const appcafePasswordInput = document.getElementById('appcafe-password'); const appcafeLoginBtn = document.getElementById('appcafe-login-btn'); const appcafeBtnText = appcafeLoginBtn?.querySelector('.btn-text'); const appcafeLoader = document.getElementById('appcafe-loader'); const appcafeError = document.getElementById('appcafe-error'); const appcafeLoginContainer = document.getElementById('appcafe-login-container'); const appcafeLoggedIn = document.getElementById('appcafe-logged-in'); const appcafeLogoutBtn = document.getElementById('appcafe-logout-btn'); const appcafePromo = document.querySelector('.appcafe-promo'); // Dashboard Buttons const btnOlharFotos = document.getElementById('btn-olhar-fotos'); const btnMapaCalor = document.getElementById('btn-mapa-calor'); const btnDashboardIndividual = document.getElementById('btn-dashboard-individual'); const btnDashboardLote = document.getElementById('btn-dashboard-lote'); // Gallery Modal const galleryModal = document.getElementById('gallery-modal'); const closeGalleryBtn = document.getElementById('close-gallery-btn'); const galleryContainer = document.getElementById('gallery-container'); const galleryLoader = document.getElementById('gallery-loader'); // Gallery Filters const filterDesc = document.getElementById('filter-desc'); const filterDateStart = document.getElementById('filter-date-start'); const filterDateEnd = document.getElementById('filter-date-end'); const clearFiltersBtn = document.getElementById('clear-filters-btn'); const galleryFilters = document.getElementById('gallery-filters'); let currentGalleryPictures = []; let selectedGalleryPictures = new Map(); // Gallery Actions const galleryActionBar = document.getElementById('gallery-action-bar'); const selectedCountText = document.getElementById('selected-count'); const btnCancelSelection = document.getElementById('btn-cancel-selection'); const btnAnalyzeSelected = document.getElementById('btn-analyze-selected'); const btnDeleteSelected = document.getElementById('btn-delete-selected'); const btnSelectAll = document.getElementById('select-all-btn'); const analyzeActionText = document.getElementById('analyze-action-text'); const analyzeActionLoader = document.getElementById('analyze-action-loader'); const analyzeActionHint = document.getElementById('analyze-action-hint'); // Heatmap let isHeatmapMode = false; let diseaseDictionary = {}; // id_disease -> name const heatmapModal = document.getElementById('heatmap-modal'); const closeHeatmapBtn = document.getElementById('close-heatmap-btn'); const heatmapDiseaseFilter = document.getElementById('heatmap-disease-filter'); const heatmapStyleFilter = document.getElementById('heatmap-style-filter'); let leafletMapInstance = null; let currentHeatLayer = null; let supabaseClient = null; if (window.supabase && window.SUPABASE_URL && window.SUPABASE_ANON_KEY) { supabaseClient = window.supabase.createClient(window.SUPABASE_URL, window.SUPABASE_ANON_KEY); } // Batch UI Elements const batchUploadArea = document.getElementById('batch-upload-area'); const batchFileInput = document.getElementById('batch-file-input'); const batchImagePreview = document.getElementById('batch-image-preview'); const batchUploadContent = document.getElementById('batch-upload-content'); const batchAnalyzeBtn = document.getElementById('batch-analyze-btn'); const batchBtnText = document.getElementById('batch-btn-text'); const batchLoader = document.getElementById('batch-loader'); const batchResultsPlaceholder = document.getElementById('batch-results-placeholder'); const batchResultsContent = document.getElementById('batch-results-content'); const batchTotalLeaves = document.getElementById('batch-total-leaves'); const batchMainDisease = document.getElementById('batch-main-disease'); const batchChartCanvas = document.getElementById('batchResultsChart'); let selectedBatchFiles = []; let batchChartInstance = null; // Theming Colors for the Chart const chartColors = [ '#2f855a', // Green '#8b5a2b', // Brown '#48bb78', // Light Green '#b7791f', // Yellow-Brown '#276749' // Dark Green ]; // Drag and Drop Events ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { uploadArea.addEventListener(eventName, preventDefaults, false); }); function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); } ['dragenter', 'dragover'].forEach(eventName => { uploadArea.addEventListener(eventName, () => uploadArea.classList.add('dragover'), false); }); ['dragleave', 'drop'].forEach(eventName => { uploadArea.addEventListener(eventName, () => uploadArea.classList.remove('dragover'), false); }); uploadArea.addEventListener('drop', handleDrop, false); uploadArea.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', handleFileSelect); function handleDrop(e) { const dt = e.dataTransfer; const file = dt.files[0]; handleFile(file); } function handleFileSelect(e) { const file = e.target.files[0]; handleFile(file); } function handleFile(file) { if (file && file.type.startsWith('image/')) { selectedFile = file; // Show preview const reader = new FileReader(); reader.onload = (e) => { imagePreview.src = e.target.result; imagePreview.classList.remove('hidden'); uploadContent.classList.add('hidden'); analyzeBtn.disabled = false; } reader.readAsDataURL(file); } else { alert('Por favor, selecione um arquivo de imagem válido.'); } } analyzeBtn.addEventListener('click', async () => { if (!selectedFile) return; // UI Loading State analyzeBtn.disabled = true; btnText.innerHTML = ' Analisando...'; loader.classList.remove('hidden'); resultsPlaceholder.querySelector('p').textContent = 'Processando IA (pode levar alguns segundos)...'; resultsContent.classList.add('hidden'); resultsPlaceholder.classList.remove('hidden'); const formData = new FormData(); formData.append('file', selectedFile); try { const response = await fetch('/predict', { method: 'POST', body: formData }); if (!response.ok) { const errData = await response.json(); throw new Error(errData.error || 'Erro na análise da imagem.'); } const data = await response.json(); updateResults(data); } catch (error) { alert(error.message); resultsPlaceholder.querySelector('p').textContent = 'Falha na análise. Tente novamente.'; } finally { analyzeBtn.disabled = false; btnText.innerHTML = ' Analisar Folha'; loader.classList.add('hidden'); } }); function updateResults(data) { // Hide placeholder, show results resultsPlaceholder.classList.add('hidden'); resultsContent.classList.remove('hidden'); // Update Text mainDisease.textContent = data.mais_frequente; // --- Save Result Logic --- const saveContainer = document.getElementById('save-result-container-individual'); const saveBtn = document.getElementById('btn-save-result-individual'); const saveMsg = document.getElementById('save-result-msg-individual'); if (selectedFile && selectedFile.supabasePictureId && data.mais_frequente !== "Nenhuma detecção encontrada" && supabaseClient) { saveContainer.classList.remove('hidden'); saveBtn.style.display = 'inline-flex'; saveMsg.style.display = 'none'; const newSaveBtn = saveBtn.cloneNode(true); saveBtn.parentNode.replaceChild(newSaveBtn, saveBtn); newSaveBtn.addEventListener('click', async () => { newSaveBtn.disabled = true; newSaveBtn.innerHTML = ' Salvando...'; try { const diseaseName = data.mais_frequente; // Find disease ID const { data: diseaseData, error: dError } = await supabaseClient.from('diseases').select('id').eq('name', diseaseName).single(); if (dError || !diseaseData) throw new Error("Doença não encontrada no banco de dados."); // Update picture const { error: pError } = await supabaseClient.from('pictures').update({ id_disease: diseaseData.id }).eq('id', selectedFile.supabasePictureId); if (pError) throw new Error("Erro ao atualizar a foto."); newSaveBtn.style.display = 'none'; saveMsg.innerHTML = ' Resultado salvo com sucesso no banco de dados!'; saveMsg.style.color = "#2f855a"; saveMsg.style.display = 'block'; } catch (e) { newSaveBtn.disabled = false; newSaveBtn.innerHTML = ' Tentar Novamente'; saveMsg.textContent = e.message; saveMsg.style.color = "#e53e3e"; saveMsg.style.display = 'block'; } }); } else { if (saveContainer) saveContainer.classList.add('hidden'); } // Render Chart renderChart(data.contagem); // Render Crops renderCrops(data.imagens || []); } function renderCrops(imagens) { cropsGallery.innerHTML = ''; if (imagens.length === 0) { cropsGallery.innerHTML = '

Nenhum recorte gerado.

'; return; } const timestamp = Date.now(); imagens.forEach(imgData => { const card = document.createElement('div'); card.className = 'crop-card'; const img = document.createElement('img'); // Cache buster for new analyses img.src = `${imgData.url}?t=${timestamp}`; img.alt = 'Recorte detectado'; const label = document.createElement('div'); label.className = 'crop-card-label'; label.textContent = imgData.classe; card.appendChild(img); card.appendChild(label); cropsGallery.appendChild(card); }); } function renderChart(counts) { const labels = Object.keys(counts); const data = Object.values(counts); if (chartInstance) { chartInstance.destroy(); } chartInstance = new Chart(chartCanvas, { type: 'doughnut', data: { labels: labels, datasets: [{ data: data, backgroundColor: chartColors, borderWidth: 0, hoverOffset: 4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { color: '#4a5568', font: { family: "'Outfit', sans-serif", size: 10 }, padding: 10, boxWidth: 12 } }, tooltip: { backgroundColor: 'rgba(255, 255, 255, 0.95)', titleColor: '#2d3748', bodyColor: '#2d3748', borderColor: 'rgba(47, 133, 90, 0.2)', borderWidth: 1, padding: 10, titleFont: { family: "'Outfit', sans-serif", size: 12, weight: 'bold' }, bodyFont: { family: "'Outfit', sans-serif", size: 12 } } }, cutout: '70%' } }); } // --- BATCH LOGIC --- // Tab Switching tabIndividual.addEventListener('click', () => { tabIndividual.classList.add('active'); tabBatch.classList.remove('active'); if (tabAppCafe) tabAppCafe.classList.remove('active'); wrapperIndividual.classList.remove('hidden'); wrapperBatch.classList.add('hidden'); if (wrapperAppCafe) wrapperAppCafe.classList.add('hidden'); }); tabBatch.addEventListener('click', () => { tabBatch.classList.add('active'); tabIndividual.classList.remove('active'); if (tabAppCafe) tabAppCafe.classList.remove('active'); wrapperBatch.classList.remove('hidden'); wrapperIndividual.classList.add('hidden'); if (wrapperAppCafe) wrapperAppCafe.classList.add('hidden'); }); if (tabAppCafe) { tabAppCafe.addEventListener('click', () => { tabAppCafe.classList.add('active'); tabIndividual.classList.remove('active'); tabBatch.classList.remove('active'); wrapperAppCafe.classList.remove('hidden'); wrapperIndividual.classList.add('hidden'); wrapperBatch.classList.add('hidden'); checkSession(); }); } // Drag and Drop for Batch ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { batchUploadArea.addEventListener(eventName, preventDefaults, false); }); ['dragenter', 'dragover'].forEach(eventName => { batchUploadArea.addEventListener(eventName, () => batchUploadArea.classList.add('dragover'), false); }); ['dragleave', 'drop'].forEach(eventName => { batchUploadArea.addEventListener(eventName, () => batchUploadArea.classList.remove('dragover'), false); }); batchUploadArea.addEventListener('drop', handleBatchDrop, false); batchUploadArea.addEventListener('click', () => batchFileInput.click()); batchFileInput.addEventListener('change', handleBatchFileSelect); function handleBatchDrop(e) { const dt = e.dataTransfer; handleBatchFiles(dt.files); } function handleBatchFileSelect(e) { handleBatchFiles(e.target.files); } function handleBatchFiles(files) { const validFiles = Array.from(files).filter(file => file.type.startsWith('image/')); if (validFiles.length > 0) { selectedBatchFiles = validFiles; batchImagePreview.innerHTML = ''; validFiles.forEach(file => { const reader = new FileReader(); reader.onload = (e) => { const img = document.createElement('img'); img.src = e.target.result; batchImagePreview.appendChild(img); }; reader.readAsDataURL(file); }); batchImagePreview.classList.remove('hidden'); batchUploadContent.classList.add('hidden'); batchAnalyzeBtn.disabled = false; } else { alert('Por favor, selecione arquivos de imagem válidos.'); } } batchAnalyzeBtn.addEventListener('click', async () => { if (selectedBatchFiles.length === 0) return; // UI Loading State batchAnalyzeBtn.disabled = true; batchBtnText.innerHTML = ' Analisando Lote...'; batchLoader.classList.remove('hidden'); batchResultsPlaceholder.querySelector('p').textContent = `Processando ${selectedBatchFiles.length} imagens (isso vai levar um tempo)...`; batchResultsContent.classList.add('hidden'); batchResultsPlaceholder.classList.remove('hidden'); const formData = new FormData(); selectedBatchFiles.forEach(file => { formData.append('files', file); }); try { const response = await fetch('/predict_batch', { method: 'POST', body: formData }); if (!response.ok) { const errData = await response.json(); throw new Error(errData.error || 'Erro na análise em lote.'); } const data = await response.json(); updateBatchResults(data); } catch (error) { alert(error.message); batchResultsPlaceholder.querySelector('p').textContent = 'Falha na análise em lote. Tente novamente.'; } finally { batchAnalyzeBtn.disabled = false; batchBtnText.innerHTML = ' Analisar Lote'; batchLoader.classList.add('hidden'); } }); function updateBatchResults(data) { batchResultsPlaceholder.classList.add('hidden'); batchResultsContent.classList.remove('hidden'); // --- Save Result Logic para Lote --- const saveContainer = document.getElementById('save-result-container-batch'); const saveBtn = document.getElementById('btn-save-result-batch'); const saveMsg = document.getElementById('save-result-msg-batch'); const hasSupabaseFiles = selectedBatchFiles && selectedBatchFiles.some(f => f.supabasePictureId); if (hasSupabaseFiles && data.detalhes && data.detalhes.length > 0 && supabaseClient) { saveContainer.classList.remove('hidden'); saveBtn.style.display = 'inline-flex'; saveMsg.style.display = 'none'; const newSaveBtn = saveBtn.cloneNode(true); saveBtn.parentNode.replaceChild(newSaveBtn, saveBtn); newSaveBtn.addEventListener('click', async () => { newSaveBtn.disabled = true; newSaveBtn.innerHTML = ' Salvando...'; try { const { data: allDiseases, error: dError } = await supabaseClient.from('diseases').select('id, name'); if (dError) throw new Error("Erro ao buscar doenças."); const diseaseMap = {}; allDiseases.forEach(d => { diseaseMap[d.name] = d.id; }); let savedCount = 0; for (const result of data.detalhes) { if (result.mais_frequente === "Nenhuma detecção encontrada") continue; const diseaseId = diseaseMap[result.mais_frequente]; if (!diseaseId) continue; const matchingFile = selectedBatchFiles.find(f => f.name === result.filename); if (matchingFile && matchingFile.supabasePictureId) { await supabaseClient.from('pictures').update({ id_disease: diseaseId }).eq('id', matchingFile.supabasePictureId); savedCount++; } } newSaveBtn.style.display = 'none'; saveMsg.innerHTML = ` ${savedCount} resultado(s) salvo(s) com sucesso!`; saveMsg.style.color = "#2f855a"; saveMsg.style.display = 'block'; } catch (e) { newSaveBtn.disabled = false; newSaveBtn.innerHTML = ' Tentar Novamente'; saveMsg.textContent = e.message; saveMsg.style.color = "#e53e3e"; saveMsg.style.display = 'block'; } }); } else { if (saveContainer) saveContainer.classList.add('hidden'); } batchTotalLeaves.textContent = data.total_folhas; if (data.folhas_com_deteccao > 0) { let mainDisease = "Nenhuma detectada"; let maxQtd = -1; let maxPct = 0; for (const [doenca, stats] of Object.entries(data.estatisticas)) { if (stats.quantidade > maxQtd) { maxQtd = stats.quantidade; maxPct = stats.porcentagem; mainDisease = doenca; } } batchMainDisease.textContent = `${mainDisease} (${maxPct}%)`; } else { batchMainDisease.textContent = "Nenhuma detectada"; } renderBatchChart(data.estatisticas); } function renderBatchChart(estatisticas) { const labels = []; const dataValues = []; // Adiciona a porcentagem direto no label (Ex: Ferrugem (60.0%)) e usa o valor para plotar for (const [doenca, stats] of Object.entries(estatisticas)) { labels.push(`${doenca} (${stats.porcentagem}%)`); dataValues.push(stats.porcentagem); } if (batchChartInstance) { batchChartInstance.destroy(); } batchChartInstance = new Chart(batchChartCanvas, { type: 'doughnut', data: { labels: labels, datasets: [{ data: dataValues, backgroundColor: chartColors, borderWidth: 0, hoverOffset: 4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { color: '#4a5568', font: { family: "'Outfit', sans-serif", size: 10 }, padding: 10, boxWidth: 12 } }, tooltip: { callbacks: { label: function(context) { let label = context.label || ''; return ' ' + label; } }, backgroundColor: 'rgba(255, 255, 255, 0.95)', titleColor: '#2d3748', bodyColor: '#2d3748', borderColor: 'rgba(47, 133, 90, 0.2)', borderWidth: 1, padding: 10, titleFont: { family: "'Outfit', sans-serif", size: 12, weight: 'bold' }, bodyFont: { family: "'Outfit', sans-serif", size: 12 } } }, cutout: '70%' } }); } // --- SUPABASE AUTH LOGIC --- async function checkSession() { if (!supabaseClient) return; const { data, error } = await supabaseClient.auth.getSession(); if (data && data.session) { showLoggedInState(data.session.user); } else { showLoginState(); } } function showLoggedInState(user) { if (appcafeLoginContainer) appcafeLoginContainer.classList.add('hidden'); if (appcafePromo) appcafePromo.classList.add('hidden'); if (appcafeLoggedIn) appcafeLoggedIn.classList.remove('hidden'); const welcomeText = document.getElementById('welcome-user-text'); if (welcomeText && user) { const name = user.user_metadata?.display_name || user.email.split('@')[0]; welcomeText.textContent = `Bem-vindo(a), ${name}`; } } function showLoginState() { if (appcafeLoginContainer) appcafeLoginContainer.classList.remove('hidden'); if (appcafePromo) appcafePromo.classList.remove('hidden'); if (appcafeLoggedIn) appcafeLoggedIn.classList.add('hidden'); if (appcafeError) appcafeError.style.display = 'none'; if (appcafeEmailInput) appcafeEmailInput.value = ''; if (appcafePasswordInput) appcafePasswordInput.value = ''; } if (appcafeLoginForm) { appcafeLoginForm.addEventListener('submit', async (e) => { e.preventDefault(); if (!supabaseClient) return; const email = appcafeEmailInput.value; const password = appcafePasswordInput.value; // UI Loading State appcafeLoginBtn.disabled = true; if (appcafeBtnText) appcafeBtnText.textContent = 'Entrando...'; if (appcafeLoader) appcafeLoader.classList.remove('hidden'); if (appcafeError) appcafeError.style.display = 'none'; const { data, error } = await supabaseClient.auth.signInWithPassword({ email: email, password: password, }); appcafeLoginBtn.disabled = false; if (appcafeBtnText) appcafeBtnText.textContent = 'Entrar'; if (appcafeLoader) appcafeLoader.classList.add('hidden'); if (error) { if (appcafeError) { appcafeError.textContent = error.message === 'Invalid login credentials' ? 'E-mail ou senha incorretos.' : error.message; appcafeError.style.display = 'block'; } } else { showLoggedInState(); } }); } const appcafeForgotPassword = document.getElementById('appcafe-forgot-password'); if (appcafeForgotPassword) { appcafeForgotPassword.addEventListener('click', async (e) => { e.preventDefault(); if (!supabaseClient) return; const email = appcafeEmailInput.value; if (!email) { alert('Por favor, preencha o campo de e-mail para recuperar a senha.'); return; } try { appcafeLoginBtn.disabled = true; if (appcafeBtnText) appcafeBtnText.textContent = 'Enviando...'; const { error } = await supabaseClient.auth.resetPasswordForEmail(email, { redirectTo: window.location.origin + '/redefinir-senha' }); if (error) throw error; alert('E-mail de recuperação enviado! Verifique sua caixa de entrada.'); } catch (err) { alert('Erro ao enviar e-mail: ' + err.message); } finally { appcafeLoginBtn.disabled = false; if (appcafeBtnText) appcafeBtnText.textContent = 'Entrar'; } }); } if (appcafeLogoutBtn) { appcafeLogoutBtn.addEventListener('click', async () => { if (!supabaseClient) return; await supabaseClient.auth.signOut(); showLoginState(); }); } // Inicializa verificando a sessão caso inicie na tab (embora comece escondida) checkSession(); // --- FILTER LOGIC --- function renderGalleryCards(picturesList) { galleryContainer.innerHTML = ''; if (picturesList && picturesList.length > 0) { picturesList.forEach(pic => { const isSelected = selectedGalleryPictures.has(pic.picture); const card = document.createElement('div'); card.style = `background: #1e293b; border-radius: 12px; overflow: hidden; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.5); display: flex; flex-direction: column; border: 2px solid ${isSelected ? 'var(--accent-primary)' : 'rgba(255,255,255,0.1)'}; transition: all 0.2s; cursor: pointer; position: relative; transform: ${isSelected ? 'scale(0.98)' : 'scale(1)'};`; // Overlay for selection checkmark const checkOverlay = document.createElement('div'); checkOverlay.style = `position: absolute; top: 10px; right: 10px; width: 30px; height: 30px; border-radius: 50%; background: ${isSelected ? 'var(--accent-primary)' : 'rgba(0,0,0,0.5)'}; border: 2px solid white; display: flex; align-items: center; justify-content: center; z-index: 5; transition: background 0.2s;`; checkOverlay.innerHTML = ``; card.onmouseover = () => { if (!selectedGalleryPictures.has(pic.picture)) card.style.transform = "translateY(-5px)"; }; card.onmouseout = () => { if (!selectedGalleryPictures.has(pic.picture)) card.style.transform = "translateY(0)"; }; card.addEventListener('click', () => { if (selectedGalleryPictures.has(pic.picture)) { selectedGalleryPictures.delete(pic.picture); card.style.border = "2px solid rgba(255,255,255,0.1)"; card.style.transform = "translateY(0)"; checkOverlay.style.background = "rgba(0,0,0,0.5)"; checkOverlay.querySelector('i').style.display = "none"; } else { selectedGalleryPictures.set(pic.picture, pic.id); card.style.border = "2px solid var(--accent-primary)"; card.style.transform = "scale(0.98)"; checkOverlay.style.background = "var(--accent-primary)"; checkOverlay.querySelector('i').style.display = "block"; } updateGalleryActionBar(); }); const imgContainer = document.createElement('div'); imgContainer.style = "width: 100%; height: 220px; background: #0f172a; overflow: hidden; position: relative;"; const img = document.createElement('img'); img.src = pic.picture; img.alt = "Foto do Café"; img.style = "width: 100%; height: 100%; object-fit: cover;"; const info = document.createElement('div'); info.style = "padding: 20px; display: flex; flex-direction: column; flex-grow: 1; justify-content: space-between;"; const dateText = document.createElement('p'); const dataFormatada = new Date(pic.data).toLocaleDateString('pt-BR', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }); dateText.innerHTML = ` ${dataFormatada}`; dateText.style = "margin: 0 0 8px 0; font-size: 0.85rem; color: #94a3b8; display: flex; align-items: center; gap: 6px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;"; const descText = document.createElement('p'); descText.innerHTML = pic.description ? `"${pic.description}"` : 'Sem descrição fornecida.'; descText.style = "margin: 0 0 10px 0; font-size: 1rem; color: #f8fafc; font-style: italic; line-height: 1.5;"; let diseaseHtml = ''; if (pic.id_disease && diseaseDictionary[pic.id_disease]) { diseaseHtml = ` ${diseaseDictionary[pic.id_disease]}`; } imgContainer.appendChild(img); card.appendChild(checkOverlay); info.appendChild(dateText); info.appendChild(descText); if (diseaseHtml) { const dWrap = document.createElement('div'); dWrap.innerHTML = diseaseHtml; info.appendChild(dWrap); } card.appendChild(imgContainer); card.appendChild(info); galleryContainer.appendChild(card); }); } else { galleryContainer.innerHTML = '

Nenhuma foto encontrada.

'; } } function updateGalleryActionBar() { const count = selectedGalleryPictures.size; selectedCountText.textContent = count; if (count > 0) { galleryActionBar.classList.remove('hidden'); btnAnalyzeSelected.disabled = false; if (isHeatmapMode) { analyzeActionText.innerHTML = ' Gerar Mapa de Calor'; analyzeActionHint.textContent = `As ${count} imagens serão usadas no mapa.`; if (btnDeleteSelected) btnDeleteSelected.style.display = 'none'; } else { if (btnDeleteSelected) btnDeleteSelected.style.display = 'flex'; if (count === 1) { analyzeActionText.innerHTML = ' Análise Individual'; analyzeActionHint.textContent = 'A imagem será enviada para a Análise Individual.'; } else { analyzeActionText.innerHTML = ' Análise em Lote'; analyzeActionHint.textContent = `As ${count} imagens serão enviadas para Análise em Lote.`; } } } else { galleryActionBar.classList.add('hidden'); btnAnalyzeSelected.disabled = true; } } // --- HEATMAP LOGIC --- const baseMaps = { dark: L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap contributors © CARTO', subdomains: 'abcd', maxZoom: 20 }), osm: L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap' }) }; function openHeatmapModal() { galleryModal.classList.add('hidden'); galleryModal.style.display = 'none'; heatmapModal.classList.remove('hidden'); heatmapModal.style.display = 'flex'; // Obter as imagens selecionadas const selectedIds = Array.from(selectedGalleryPictures.values()); const selectedPics = currentGalleryPictures.filter(p => selectedIds.includes(p.id)); if (!leafletMapInstance) { leafletMapInstance = L.map('leaflet-map').setView([-14.235, -51.925], 4); baseMaps.dark.addTo(leafletMapInstance); heatmapStyleFilter.addEventListener('change', (e) => { if (e.target.value === 'dark') { leafletMapInstance.removeLayer(baseMaps.osm); baseMaps.dark.addTo(leafletMapInstance); } else { leafletMapInstance.removeLayer(baseMaps.dark); baseMaps.osm.addTo(leafletMapInstance); } }); heatmapDiseaseFilter.addEventListener('change', () => { updateHeatmapLayer(selectedPics); }); // Adicionar Legenda const legend = L.control({position: 'bottomright'}); legend.onAdd = function (map) { const div = L.DomUtil.create('div', 'info legend'); div.style.backgroundColor = 'rgba(255, 255, 255, 0.9)'; div.style.padding = '10px'; div.style.borderRadius = '8px'; div.style.boxShadow = '0 0 15px rgba(0,0,0,0.2)'; div.style.color = '#333'; div.style.fontFamily = 'inherit'; div.innerHTML += 'Densidade de Focos'; div.innerHTML += ' Baixa
'; div.innerHTML += ' Média
'; div.innerHTML += ' Alta
'; return div; }; legend.addTo(leafletMapInstance); } setTimeout(() => { leafletMapInstance.invalidateSize(); updateHeatmapLayer(selectedPics); if (selectedPics.length > 0) { const lats = selectedPics.map(p => p.lat).filter(Boolean); const lngs = selectedPics.map(p => p.long).filter(Boolean); if (lats.length > 0 && lngs.length > 0) { const bounds = [ [Math.min(...lats), Math.min(...lngs)], [Math.max(...lats), Math.max(...lngs)] ]; leafletMapInstance.fitBounds(bounds, { padding: [50, 50] }); } } }, 300); } function updateHeatmapLayer(pics) { if (currentHeatLayer) { leafletMapInstance.removeLayer(currentHeatLayer); } const filterVal = heatmapDiseaseFilter.value; const heatData = []; pics.forEach(pic => { if (pic.lat && pic.long) { if (filterVal === 'all' || String(pic.id_disease) === filterVal) { heatData.push([pic.lat, pic.long, 1]); } } }); currentHeatLayer = L.heatLayer(heatData, { radius: 25, blur: 15, maxZoom: 10, gradient: {0.4: 'blue', 0.6: 'cyan', 0.7: 'lime', 0.8: 'yellow', 1.0: 'red'} }).addTo(leafletMapInstance); } if (closeHeatmapBtn) { closeHeatmapBtn.addEventListener('click', () => { heatmapModal.classList.add('hidden'); heatmapModal.style.display = 'none'; }); } async function urlToFile(url, filename, mimeType) { const res = await fetch(url); const buf = await res.arrayBuffer(); return new File([buf], filename, {type: mimeType}); } if (btnSelectAll) { btnSelectAll.addEventListener('click', () => { // Select only what is currently rendered/filtered const cards = galleryContainer.querySelectorAll('div > img'); // Hack to know there are cards if (cards.length > 0) { // Actually we just use currentGalleryPictures, but let's respect filters if we want. // Re-run filter logic to get currently visible: const descVal = filterDesc ? filterDesc.value.toLowerCase() : ''; const startVal = filterDateStart ? filterDateStart.value : ''; const endVal = filterDateEnd ? filterDateEnd.value : ''; const filtered = currentGalleryPictures.filter(pic => { const picDesc = (pic.description || '').toLowerCase(); if (descVal && !picDesc.includes(descVal)) return false; if (startVal || endVal) { const picDateObj = new Date(pic.data); const year = picDateObj.getFullYear(); const month = String(picDateObj.getMonth() + 1).padStart(2, '0'); const day = String(picDateObj.getDate()).padStart(2, '0'); const picDateStr = `${year}-${month}-${day}`; if (startVal && picDateStr < startVal) return false; if (endVal && picDateStr > endVal) return false; } return true; }); filtered.forEach(pic => selectedGalleryPictures.set(pic.picture, pic.id)); applyGalleryFilters(); // Re-render to show selection styling updateGalleryActionBar(); } }); } if (btnDeleteSelected) { btnDeleteSelected.addEventListener('click', async () => { if (selectedGalleryPictures.size === 0) return; const confirmDelete = confirm(`Tem certeza que deseja apagar ${selectedGalleryPictures.size} foto(s)? Isso não pode ser desfeito.`); if (!confirmDelete) return; btnDeleteSelected.disabled = true; btnDeleteSelected.innerHTML = ' Apagando...'; try { const idsToDelete = Array.from(selectedGalleryPictures.values()); const urlsToDelete = Array.from(selectedGalleryPictures.keys()); // 1. Delete from DB const { error: dbError } = await supabaseClient.from('pictures').delete().in('id', idsToDelete); if (dbError) throw new Error("Erro ao apagar do banco de dados: " + dbError.message); // 2. Delete from Storage const storagePaths = urlsToDelete.map(url => { // Extract filename from the end of publicUrl return url.split('/').pop(); }); const { error: stError } = await supabaseClient.storage.from('FotosCafe').remove(storagePaths); if (stError) console.warn("Aviso: Falha ao apagar arquivos do storage", stError); // Update UI state currentGalleryPictures = currentGalleryPictures.filter(p => !idsToDelete.includes(p.id)); selectedGalleryPictures.clear(); alert('Fotos apagadas com sucesso!'); applyGalleryFilters(); updateGalleryActionBar(); } catch (err) { alert(err.message); } finally { btnDeleteSelected.disabled = false; btnDeleteSelected.innerHTML = ' Excluir'; } }); } if (btnAnalyzeSelected) { btnAnalyzeSelected.addEventListener('click', async () => { if (isHeatmapMode) { openHeatmapModal(); return; } const items = Array.from(selectedGalleryPictures.entries()); // [url, id] if (items.length === 0) return; btnAnalyzeSelected.disabled = true; analyzeActionText.textContent = 'Baixando imagens...'; analyzeActionLoader.classList.remove('hidden'); try { // Fechar modal galleryModal.classList.add('hidden'); galleryModal.style.display = 'none'; // Baixar arquivos const files = []; for (let i = 0; i < items.length; i++) { const [url, picId] = items[i]; const filename = `foto_appcafe_${picId}.jpg`; const file = await urlToFile(url, filename, 'image/jpeg'); file.supabasePictureId = picId; files.push(file); } if (files.length === 1) { // Send to Individual Analysis tabIndividual.click(); handleFile(files[0]); // Auto click analyze setTimeout(() => analyzeBtn.click(), 500); } else { // Send to Batch Analysis tabBatch.click(); handleBatchFiles(files); // Auto click analyze setTimeout(() => batchAnalyzeBtn.click(), 500); } // Limpar seleção apos enviar selectedGalleryPictures.clear(); updateGalleryActionBar(); } catch (error) { console.error("Erro ao baixar imagens:", error); alert("Ocorreu um erro ao preparar as imagens para análise. Tente novamente."); galleryModal.classList.remove('hidden'); galleryModal.style.display = 'flex'; } finally { btnAnalyzeSelected.disabled = false; analyzeActionLoader.classList.add('hidden'); } }); } function applyGalleryFilters() { if (!currentGalleryPictures) return; const descVal = filterDesc ? filterDesc.value.toLowerCase() : ''; const startVal = filterDateStart ? filterDateStart.value : ''; const endVal = filterDateEnd ? filterDateEnd.value : ''; const filterDiseaseSelect = document.getElementById('filter-disease-select'); const diseaseVal = filterDiseaseSelect && !filterDiseaseSelect.parentElement.classList.contains('hidden') ? filterDiseaseSelect.value : 'all'; const filtered = currentGalleryPictures.filter(pic => { const picDesc = (pic.description || '').toLowerCase(); if (descVal && !picDesc.includes(descVal)) return false; if (startVal || endVal) { const picDateObj = new Date(pic.data); const year = picDateObj.getFullYear(); const month = String(picDateObj.getMonth() + 1).padStart(2, '0'); const day = String(picDateObj.getDate()).padStart(2, '0'); const picDateStr = `${year}-${month}-${day}`; if (startVal && picDateStr < startVal) return false; if (endVal && picDateStr > endVal) return false; } if (diseaseVal !== 'all' && String(pic.id_disease) !== diseaseVal) { return false; } return true; }); renderGalleryCards(filtered); } if (filterDesc) filterDesc.addEventListener('input', applyGalleryFilters); if (filterDateStart) filterDateStart.addEventListener('change', applyGalleryFilters); if (filterDateEnd) filterDateEnd.addEventListener('change', applyGalleryFilters); const filterDiseaseSelect = document.getElementById('filter-disease-select'); if (filterDiseaseSelect) filterDiseaseSelect.addEventListener('change', applyGalleryFilters); if (clearFiltersBtn) { clearFiltersBtn.addEventListener('click', () => { if (filterDesc) filterDesc.value = ''; if (filterDateStart) filterDateStart.value = ''; if (filterDateEnd) filterDateEnd.value = ''; if (filterDiseaseSelect) filterDiseaseSelect.value = 'all'; applyGalleryFilters(); }); } // --- DASHBOARD ACTIONS --- if (btnDashboardIndividual) { btnDashboardIndividual.addEventListener('click', () => { tabIndividual.click(); }); } if (btnDashboardLote) { btnDashboardLote.addEventListener('click', () => { tabBatch.click(); }); } if (btnMapaCalor) { btnMapaCalor.addEventListener('click', async () => { isHeatmapMode = true; btnOlharFotos.click(); // Reuse gallery open logic but in heatmap mode }); } if (btnOlharFotos) { btnOlharFotos.addEventListener('click', async (e) => { if (!supabaseClient) return; // If click was explicitly from "Olhar Fotos", disable heatmap mode if (e && e.isTrusted) { isHeatmapMode = false; } // Abre o modal galleryModal.classList.remove('hidden'); galleryModal.style.display = 'flex'; galleryContainer.innerHTML = ''; galleryLoader.classList.remove('hidden'); // Set gallery title const galleryTitle = galleryModal.querySelector('h2'); if (isHeatmapMode) { galleryTitle.innerHTML = ' Selecionar Fotos para Mapa de Calor'; } else { galleryTitle.innerHTML = ' Suas Fotos'; } try { // Preload diseases if not loaded if (Object.keys(diseaseDictionary).length === 0) { const { data: dData, error: dErr } = await supabaseClient.from('diseases').select('*'); if (!dErr && dData) { dData.forEach(d => { diseaseDictionary[d.id] = d.name; }); // Populate heatmap dropdown heatmapDiseaseFilter.innerHTML = ''; dData.forEach(d => { heatmapDiseaseFilter.innerHTML += ``; }); } } // Pega o ID do usuario logado const { data: userData, error: userError } = await supabaseClient.auth.getUser(); if (userError || !userData?.user) throw new Error('Erro ao identificar usuário logado.'); const userId = userData.user.id; // Busca as fotos na tabela pictures let query = supabaseClient.from('pictures').select('*').eq('id_user', userId); if (isHeatmapMode) { query = query.not('id_disease', 'is', null); } const { data: pictures, error: picError } = await query.order('data', { ascending: false }); if (picError) throw new Error(picError.message); currentGalleryPictures = pictures || []; galleryLoader.classList.add('hidden'); if (currentGalleryPictures.length > 0) { if (galleryFilters) galleryFilters.classList.remove('hidden'); const filterDiseaseContainer = document.getElementById('filter-disease-container'); const filterDiseaseSelectEl = document.getElementById('filter-disease-select'); if (isHeatmapMode && filterDiseaseContainer && filterDiseaseSelectEl) { filterDiseaseContainer.classList.remove('hidden'); filterDiseaseSelectEl.innerHTML = ''; Object.entries(diseaseDictionary).forEach(([id, name]) => { filterDiseaseSelectEl.innerHTML += ``; }); } else if (filterDiseaseContainer) { filterDiseaseContainer.classList.add('hidden'); } // Reset filters on fresh open if (filterDesc) filterDesc.value = ''; if (filterDateStart) filterDateStart.value = ''; if (filterDateEnd) filterDateEnd.value = ''; if (filterDiseaseSelectEl) filterDiseaseSelectEl.value = 'all'; // Reset selection selectedGalleryPictures.clear(); updateGalleryActionBar(); applyGalleryFilters(); } else { if (galleryFilters) galleryFilters.classList.add('hidden'); selectedGalleryPictures.clear(); updateGalleryActionBar(); renderGalleryCards([]); } } catch (error) { console.error("Erro ao buscar fotos:", error); galleryLoader.classList.add('hidden'); galleryContainer.innerHTML = `

Erro ao carregar fotos: ${error.message}

`; } }); } if (closeGalleryBtn) { closeGalleryBtn.addEventListener('click', () => { galleryModal.classList.add('hidden'); galleryModal.style.display = 'none'; }); } });