CoffeeHealth-AI / static /script.js
SucoCafe's picture
Adicionar fluxo de redefinir senha
088e201
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 = '<i class="ph-bold ph-scan"></i> 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 = '<i class="ph-bold ph-scan"></i> 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 = '<i class="ph-duotone ph-spinner" style="animation: spin 1s linear infinite;"></i> 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 = '<i class="ph-fill ph-check-circle"></i> Resultado salvo com sucesso no banco de dados!';
saveMsg.style.color = "#2f855a";
saveMsg.style.display = 'block';
} catch (e) {
newSaveBtn.disabled = false;
newSaveBtn.innerHTML = '<i class="ph-bold ph-floppy-disk"></i> 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 = '<p style="color:var(--text-secondary); width: 100%;">Nenhum recorte gerado.</p>';
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 = '<i class="ph-bold ph-scan"></i> 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 = '<i class="ph-bold ph-scan"></i> 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 = '<i class="ph-duotone ph-spinner" style="animation: spin 1s linear infinite;"></i> 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 = `<i class="ph-fill ph-check-circle"></i> ${savedCount} resultado(s) salvo(s) com sucesso!`;
saveMsg.style.color = "#2f855a";
saveMsg.style.display = 'block';
} catch (e) {
newSaveBtn.disabled = false;
newSaveBtn.innerHTML = '<i class="ph-bold ph-floppy-disk-back"></i> 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 = `<i class="ph-bold ph-check" style="color: white; font-size: 1.2rem; display: ${isSelected ? 'block' : 'none'};"></i>`;
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 = `<i class="ph-bold ph-calendar-blank"></i> ${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}"` : '<em>Sem descrição fornecida.</em>';
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 = `<span style="background: rgba(47, 133, 90, 0.2); color: #48bb78; padding: 4px 8px; border-radius: 4px; font-size: 0.8rem; font-weight: bold; border: 1px solid rgba(47, 133, 90, 0.5); display: inline-block;"><i class="ph-bold ph-virus"></i> ${diseaseDictionary[pic.id_disease]}</span>`;
}
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 = '<div style="grid-column: 1 / -1; text-align: center; padding: 60px 20px; background: rgba(255,255,255,0.05); border-radius: 12px; border: 1px dashed rgba(255,255,255,0.2);"><i class="ph-duotone ph-image-broken" style="font-size: 4rem; color: #64748b; margin-bottom: 15px;"></i><p style="color: #cbd5e1; font-size: 1.2rem; margin: 0;">Nenhuma foto encontrada.</p></div>';
}
}
function updateGalleryActionBar() {
const count = selectedGalleryPictures.size;
selectedCountText.textContent = count;
if (count > 0) {
galleryActionBar.classList.remove('hidden');
btnAnalyzeSelected.disabled = false;
if (isHeatmapMode) {
analyzeActionText.innerHTML = '<i class="ph-bold ph-map-trifold"></i> 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 = '<i class="ph-bold ph-image"></i> Análise Individual';
analyzeActionHint.textContent = 'A imagem será enviada para a Análise Individual.';
} else {
analyzeActionText.innerHTML = '<i class="ph-bold ph-images"></i> 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: '&copy; OpenStreetMap contributors &copy; CARTO',
subdomains: 'abcd',
maxZoom: 20
}),
osm: L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; 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 += '<strong style="display:block; margin-bottom: 5px;">Densidade de Focos</strong>';
div.innerHTML += '<i style="background: blue; width: 12px; height: 12px; display: inline-block; margin-right: 5px;"></i> Baixa<br>';
div.innerHTML += '<i style="background: lime; width: 12px; height: 12px; display: inline-block; margin-right: 5px;"></i> Média<br>';
div.innerHTML += '<i style="background: red; width: 12px; height: 12px; display: inline-block; margin-right: 5px;"></i> Alta<br>';
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 = '<i class="ph-duotone ph-spinner spin"></i> 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 = '<i class="ph-bold ph-trash"></i> 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 = '<i class="ph-bold ph-map-trifold"></i> Selecionar Fotos para Mapa de Calor';
} else {
galleryTitle.innerHTML = '<i class="ph-bold ph-images"></i> 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 = '<option value="all" style="color: black;">Todas as Doenças</option>';
dData.forEach(d => {
heatmapDiseaseFilter.innerHTML += `<option value="${d.id}" style="color: black;">Apenas ${d.name}</option>`;
});
}
}
// 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 = '<option value="all" style="color: black;">Todas as Doenças</option>';
Object.entries(diseaseDictionary).forEach(([id, name]) => {
filterDiseaseSelectEl.innerHTML += `<option value="${id}" style="color: black;">${name}</option>`;
});
} 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 = `<p style="color: #fc8181; grid-column: 1 / -1; text-align: center;">Erro ao carregar fotos: ${error.message}</p>`;
}
});
}
if (closeGalleryBtn) {
closeGalleryBtn.addEventListener('click', () => {
galleryModal.classList.add('hidden');
galleryModal.style.display = 'none';
});
}
});