// ================================================================ // DENTAL IMAGE MANAGER - Main JavaScript // Two-Level Navigation: Patients → Images → Transform // ================================================================ const API_URL = '/api'; // ================================================================ // ERROR LOGGER — Logs to console AND shows on screen // Ative DEBUG=true só para diagnóstico; em produção fica silencioso // (info/warn/group viram no-op) para não poluir o console nem custar reflows. // ================================================================ const DEBUG = false; const _noop = () => {}; const _log = { info: DEBUG ? (tag, ...args) => console.log(`%c[${tag}]`, 'color:#667eea;font-weight:bold', ...args) : _noop, warn: DEBUG ? (tag, ...args) => console.warn(`%c[${tag}]`, 'color:#ffa500;font-weight:bold', ...args) : _noop, error: (tag, ...args) => console.error(`%c[${tag}]`, 'color:#ff4444;font-weight:bold', ...args), group: DEBUG ? (tag) => console.group(`%c[${tag}]`, 'color:#667eea;font-weight:bold') : _noop, end: DEBUG ? () => console.groupEnd() : _noop }; function showErrorBanner(msg, detail = '') { let banner = document.getElementById('_errBanner'); if (!banner) { banner = document.createElement('div'); banner.id = '_errBanner'; banner.style.cssText = [ 'position:fixed','bottom:70px','left:50%','transform:translateX(-50%)', 'background:#c0392b','color:#fff','padding:12px 20px','border-radius:8px', 'z-index:9999','font-size:13px','max-width:90vw','text-align:center', 'box-shadow:0 4px 20px rgba(0,0,0,0.3)','cursor:pointer' ].join(';'); banner.onclick = () => banner.remove(); document.body.appendChild(banner); } banner.innerHTML = `⚠️ ${msg}${detail ? `
${detail}` : ''}`; setTimeout(() => banner && banner.remove(), 8000); } function getAuthToken() { return localStorage.getItem('auth_token'); } let authVerificationInProgress = false; async function verifyAuth() { if (authVerificationInProgress) return false; const token = getAuthToken(); if (!token) { window.location.href = '/login'; return false; } authVerificationInProgress = true; try { const response = await fetch(`${API_URL}/auth/verify`, { headers: { 'Authorization': `Bearer ${token}` } }); if (!response.ok) { localStorage.removeItem('auth_token'); window.location.href = '/login'; return false; } const data = await response.json(); authVerificationInProgress = false; if (data.valid) { const isAdmin = !!data.user.is_admin; localStorage.setItem('is_admin', isAdmin ? 'true' : 'false'); localStorage.setItem('username', data.user.username || ''); localStorage.setItem('email', data.user.email || ''); // Controle de visibilidade de itens de menu updateAdminMenuVisibility(isAdmin); // Se usuário comum tentar acessar clients ou admin-clinics no front, joga de volta pra home if (!isAdmin && (window.location.pathname === '/clients' || window.location.pathname === '/admin-clinics')) { window.location.href = '/'; return false; } return true; } localStorage.removeItem('auth_token'); window.location.href = '/login'; return false; } catch { localStorage.removeItem('auth_token'); authVerificationInProgress = false; window.location.href = '/login'; return false; } } function updateAdminMenuVisibility(isAdmin) { if (isAdmin) { document.body.classList.add('is-admin'); } else { document.body.classList.remove('is-admin'); } } async function fetchWithAuth(url, options = {}) { const token = getAuthToken(); _log.info('FETCH', `${options.method || 'GET'} ${url}`); const t0 = Date.now(); try { const res = await fetch(url, { ...options, headers: { ...options.headers, 'Authorization': `Bearer ${token}` } }); const ms = Date.now() - t0; if (!res.ok) { _log.warn('FETCH', `${options.method || 'GET'} ${url} → ${res.status} (${ms}ms)`); } else { _log.info('FETCH', `${options.method || 'GET'} ${url} → ${res.status} OK (${ms}ms)`); } return res; } catch (err) { const ms = Date.now() - t0; _log.error('FETCH', `${options.method || 'GET'} ${url} → FALHOU após ${ms}ms`, err.message); throw err; } } // ================================================================ // STATE // ================================================================ let socket = null; let view = 'patients'; // 'patients' | 'patient-images' let patients = []; let patientImages = []; let selectedPatient = null; // { patient_name, first_name, last_name, ... } let selectedImageId = null; let clientsList = []; let selectedClient = ''; let showDisabled = false; let fineTuneAngle = 0; // ================================================================ // DOM REFS // ================================================================ const searchInput = document.getElementById('searchInput'); const clientFilter = document.getElementById('clientFilter'); const toggleDisabledBtn = document.getElementById('toggleDisabledBtn'); const toggleText = document.getElementById('toggleText'); const imagesGrid = document.getElementById('imagesGrid'); const loadingState = document.getElementById('loadingState'); const emptyState = document.getElementById('emptyState'); const transformModal = document.getElementById('transformModal'); const backBtn = document.getElementById('backBtn'); const patientHeader = document.getElementById('patientHeader'); const patientTitle = document.getElementById('patientTitle'); const patientMeta = document.getElementById('patientMeta'); // ================================================================ // INIT // ================================================================ document.addEventListener('DOMContentLoaded', async () => { _log.info('INIT', 'DOMContentLoaded — iniciando aplicação'); try { const ok = await verifyAuth(); if (!ok) { _log.warn('AUTH', 'Verificação de auth falhou, redirecionando...'); return; } _log.info('AUTH', 'Autenticação OK'); const token = getAuthToken(); if (token) { _log.info('SOCKET', 'Conectando Socket.IO...'); socket = io({ auth: { token } }); const debouncedReload = debounce(() => { if (view === 'patients') loadPatients(); else loadPatientImages(selectedPatient.patient_name); }, 1200); socket.on('new-image', () => { _log.info('SOCKET', 'Nova imagem recebida via socket'); showToast('Nova imagem recebida!', 'info'); debouncedReload(); }); socket.on('clients-list-updated', () => loadClientsList()); socket.on('connect', () => _log.info('SOCKET', '✅ Socket.IO conectado, ID:', socket.id)); socket.on('disconnect', (r) => _log.warn('SOCKET', '❌ Socket.IO desconectado. Motivo:', r)); socket.on('connect_error', (e) => _log.error('SOCKET', 'Erro de conexão:', e.message)); } await loadClientsList(); loadPatients(); // Registra busca, filtro de cliente, toggle "Ocultas", botão voltar e // fechamento do modal de transformação. (Antes nunca era chamado, então // a busca e os filtros do cabeçalho não funcionavam.) setupEventListeners(); // Add global modal close listener document.addEventListener('click', (e) => { if (e.target.classList.contains('modal-close') || e.target.classList.contains('close-modal')) { const modal = e.target.closest('.modal'); if (modal) modal.classList.remove('active'); } else if (e.target.classList.contains('modal')) { e.target.classList.remove('active'); } }); // Add hash routing listener window.addEventListener('hashchange', handleHashRoute); // Call once to process initial hash setTimeout(handleHashRoute, 200); // Add scroll listener for infinite scroll const contentScroll = document.querySelector('.content-scroll'); if (contentScroll) { contentScroll.addEventListener('scroll', () => { if (view === 'patients' && !isFetchingPatients && hasMorePatients) { // Trigger load more when 300px from the bottom if (contentScroll.scrollTop + contentScroll.clientHeight >= contentScroll.scrollHeight - 300) { loadPatients(true, true); } } }); } // Atualizações automáticas removidas para economizar banda (usando apenas eventos socket) // setInterval(loadClientsList, 5000); // Wasabi status update removed (was: setInterval(checkWasabiStatus, 15000)) initDragAndDrop(); checkWasabiStatus(); // Process Querystring actions (from navigation links in other pages) const urlParams = new URLSearchParams(window.location.search); const action = urlParams.get('action'); if (action) { setTimeout(() => { if (action === 'new-patient') window.openCreatePatientModal(); else if (action === 'settings') window.openSettingsModal(); else if (action === 'plugins') window.openPluginsModal(); else if (action === 'sync-devices') window.openSyncModal('devices'); else if (action === 'sync-upload') window.openSyncModal('upload'); else if (action === 'sync') window.openSyncModal('devices'); else if (action === 'users') window.openUserManagementModal(); // Remove querystring to prevent reopening on reload window.history.replaceState({}, document.title, window.location.pathname); }, 300); } } catch (error) { _log.error('INIT', 'Erro fatal na inicialização:', error.message, error); showErrorBanner('Erro na inicialização', error.message); localStorage.removeItem('auth_token'); window.location.replace('/login'); } }); // ================================================================ // EVENT LISTENERS // ================================================================ function setupEventListeners() { searchInput.addEventListener('input', debounce(() => { if (view === 'patients') loadPatients(); else loadPatientImages(selectedPatient.patient_name); }, 400)); clientFilter.addEventListener('change', e => { selectedClient = e.target.value; if (view === 'patients') loadPatients(); }); toggleDisabledBtn.addEventListener('click', () => { showDisabled = !showDisabled; toggleText.textContent = showDisabled ? 'Ver Ativas' : 'Ver Ocultas'; if (view === 'patients') loadPatients(); else loadPatientImages(selectedPatient.patient_name); }); backBtn.addEventListener('click', () => { window.location.hash = '#/'; }); // Transform modal close on backdrop transformModal.addEventListener('click', e => { if (e.target === transformModal) transformModal.classList.remove('active'); }); document.getElementById('closeTransformModal').addEventListener('click', () => { transformModal.classList.remove('active'); }); } // ================================================================ // CLIENTS LIST // ================================================================ async function loadClientsList() { try { let identified = []; const isAdmin = localStorage.getItem('is_admin') === 'true'; if (isAdmin) { const r = await fetchWithAuth('/api/socket/status'); if (r.ok) { const data = await r.json(); identified = data.identified || []; } } clientsList = identified.map(c => ({ ...c, name: c.name || 'Unknown', displayName: (c.clinicName && c.clinicName !== 'Clínica não identificada' ? c.clinicName + ' — ' : '') + (c.name || 'Unknown'), dbName: c.name || 'Unknown', status: 'identified' })); const imagesRes = await fetchWithAuth('/api/images/unique-clients'); if (imagesRes.ok) { const uniqueClients = await imagesRes.json(); uniqueClients.forEach(cn => { if (!clientsList.find(c => c.dbName === cn || c.name === cn)) { clientsList.push({ name: cn, displayName: cn, dbName: cn, type: 'historic', socketId: null, status: 'historic' }); } }); } updateClientsDropdown(); } catch (e) { console.error('Erro ao carregar clientes:', e); } } function updateClientsDropdown() { const current = clientFilter.value; clientFilter.innerHTML = ''; const sorted = [...clientsList].sort((a, b) => { const o = { identified: 1, connected: 2, historic: 3 }; return (o[a.status] || 99) - (o[b.status] || 99); }); sorted.forEach(c => { const opt = document.createElement('option'); opt.value = c.name; let lbl = c.name; if (c.name === 'Upload Web') { lbl = 'Upload Web 💻 (Imagens do Painel)'; } else { if (c.status === 'identified') lbl += ' ✅'; else if (c.status === 'historic') lbl += ' 📚'; } opt.textContent = lbl; clientFilter.appendChild(opt); }); clientFilter.value = current || selectedClient; } // ================================================================ // VIEW: PATIENTS LIST // ================================================================ let currentPatientsPage = 1; let isFetchingPatients = false; let hasMorePatients = true; function showPatientsView(pushHash = true) { view = 'patients'; selectedPatient = null; backBtn.style.display = 'none'; patientHeader.style.display = 'none'; if (pushHash && window.location.hash && window.location.hash !== '#/') { window.history.pushState(null, '', '#/'); } loadPatients(); } async function loadPatients(silent = false, loadMore = false) { if (isFetchingPatients) return; if (loadMore && !hasMorePatients) return; _log.group('LOAD_PATIENTS'); _log.info('LOAD_PATIENTS', `Iniciando carga. silent=${silent}, showDisabled=${showDisabled}, client="${selectedClient}", loadMore=${loadMore}`); isFetchingPatients = true; if (!loadMore) { currentPatientsPage = 1; hasMorePatients = true; if (!silent) { loadingState.style.display = 'block'; imagesGrid.style.display = 'none'; emptyState.style.display = 'none'; } } try { const search = searchInput.value.trim(); const clientParam = selectedClient ? `&client=${encodeURIComponent(selectedClient)}` : ''; const url = `/api/images/patients?disabled=${showDisabled}&search=${encodeURIComponent(search)}${clientParam}&page=${currentPatientsPage}&limit=50`; const res = await fetchWithAuth(url); if (!res.ok) { let errBody = ''; try { errBody = await res.text(); } catch (_) {} _log.error('LOAD_PATIENTS', `HTTP ${res.status} — body:`, errBody); if (res.status === 401 || res.status === 403) { _log.warn('AUTH', 'Token inválido/expirado, redirecionando para login'); localStorage.removeItem('auth_token'); window.location.href = '/login'; return; } throw new Error(`HTTP ${res.status}: ${errBody || 'Erro ao carregar pacientes'}`); } const newPatients = await res.json(); if (newPatients.length < 50) { hasMorePatients = false; } if (loadMore) { patients = patients.concat(newPatients); } else { patients = newPatients; } _log.info('LOAD_PATIENTS', `✅ ${newPatients.length} paciente(s) recebido(s), total: ${patients.length}`); if (patients.length > 0) { _log.info('LOAD_PATIENTS', 'Primeiro paciente:', { name: patients[0].patient_name, thumb: patients[0].thumb_filename, count: patients[0].image_count }); } loadingState.style.display = 'none'; if (patients.length === 0) { _log.warn('LOAD_PATIENTS', 'Nenhum paciente encontrado — exibindo empty state'); emptyState.style.display = 'block'; imagesGrid.style.display = 'none'; } else { emptyState.style.display = 'none'; imagesGrid.style.display = 'grid'; if (loadMore) { appendPatientCards(patients.length - newPatients.length); } else { renderPatientCards(); } } if (loadMore) { currentPatientsPage++; } else if (hasMorePatients) { currentPatientsPage = 2; } } catch (e) { _log.error('LOAD_PATIENTS', '❌ Exceção:', e.message, e); if (!loadMore) { loadingState.style.display = 'none'; emptyState.style.display = 'block'; } showErrorBanner('Erro ao carregar pacientes', e.message); showToast('Erro ao carregar pacientes', 'error'); } finally { isFetchingPatients = false; _log.end(); } } // Template de um cartão de paciente (reutilizado no render completo e no append) function patientCardHTML(p, index) { const fullName = p.patient_name || 'Sem nome'; const thumb = p.thumb_url || (p.thumb_filename ? `/uploads/${p.thumb_filename}` : ''); const count = p.image_count || 0; const date = formatDate(p.last_date); const doctor = p.doctor || '—'; const remark = p.remark || '—'; // SEMPRE exibir imagem ou placeholder — thumb em primeiro plano, cinza como fallback // loading="lazy" adia o download de miniaturas fora da viewport const preview = thumb ? `` : `
`; return `
${preview}
${count} imagem${count !== 1 ? 'ns' : ''}
${escapeHtml(fullName)}
📅 ${date}
🩺 Dentista: ${escapeHtml(doctor)}
📝 Obs: ${escapeHtml(remark)}
`; } // Render completo (primeira página / refresh) function renderPatientCards() { imagesGrid.innerHTML = patients.map((p, index) => patientCardHTML(p, index)).join(''); } // Scroll infinito: anexa apenas os cartões novos, sem repintar os já visíveis function appendPatientCards(startIndex) { const html = patients .slice(startIndex) .map((p, i) => patientCardHTML(p, startIndex + i)) .join(''); imagesGrid.insertAdjacentHTML('beforeend', html); } // Exclui todas as imagens de um determinado paciente (S3 e Local) async function deletePatientImages(patientName) { if (!patientName) return; // TODO(security): Usando confirm() para manter coerência com o restante das confirmações do painel do projeto const ok = confirm(`⚠️ ATENÇÃO: Tem certeza que deseja excluir permanentemente todas as imagens do paciente "${patientName}"?\n\nEsta ação apagará os arquivos locais, do Wasabi S3 e do banco de dados, e NÃO poderá ser desfeita.`); if (!ok) return; showToast('Excluindo imagens do paciente...', 'info'); try { const res = await fetchWithAuth(`/api/images/by-patient?name=${encodeURIComponent(patientName)}`, { method: 'DELETE' }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Erro ao excluir imagens do paciente.'); } showToast('✅ Todas as imagens do paciente foram excluídas!', 'success'); // Atualizar listagens await loadPatients(); await loadClientsList(); } catch (e) { console.error('Erro na exclusão do paciente:', e); showToast(`Falha ao excluir imagens: ${e.message}`, 'error'); } } // ================================================================ // VIEW: PATIENT IMAGES // ================================================================ function openPatient(patient, pushHash = true) { selectedPatient = patient; view = 'patient-images'; backBtn.style.display = 'inline-flex'; patientHeader.style.display = 'block'; const fullName = patient.patient_name || 'Sem nome'; patientTitle.textContent = fullName; patientMeta.textContent = `${patient.image_count || 0} imagem(ns) · ${patient.doctor || '—'}`; if (pushHash) { const newHash = `#/patient/${encodeURIComponent(patient.patient_name)}`; if (window.location.hash !== newHash) { window.history.pushState(null, '', newHash); } } loadPatientImages(patient.patient_name); } function handleHashRoute() { const hash = decodeURIComponent(window.location.hash); if (hash.startsWith('#/patient/')) { const pName = hash.split('#/patient/')[1]; if (pName && (!selectedPatient || selectedPatient.patient_name !== pName)) { openPatient({ patient_name: pName, image_count: '?', doctor: 'Carregando...' }, false); } } else if (hash === '#/' || hash === '') { if (view !== 'patients') { showPatientsView(false); } } } async function loadPatientImages(patientName) { _log.group('LOAD_IMAGES'); _log.info('LOAD_IMAGES', `Carregando imagens de: "${patientName}"`); loadingState.style.display = 'block'; imagesGrid.style.display = 'none'; emptyState.style.display = 'none'; try { const url = `/api/images/by-patient?name=${encodeURIComponent(patientName)}&disabled=${showDisabled}`; const res = await fetchWithAuth(url); if (!res.ok) { let errBody = ''; try { errBody = await res.text(); } catch (_) {} _log.error('LOAD_IMAGES', `HTTP ${res.status}:`, errBody); throw new Error(`HTTP ${res.status}: ${errBody}`); } patientImages = await res.json(); _log.info('LOAD_IMAGES', `✅ ${patientImages.length} imagem(ns) recebida(s)`); loadingState.style.display = 'none'; if (patientImages.length === 0) { _log.warn('LOAD_IMAGES', 'Nenhuma imagem encontrada'); emptyState.style.display = 'block'; imagesGrid.style.display = 'none'; } else { emptyState.style.display = 'none'; imagesGrid.style.display = 'grid'; renderPatientImages(); } } catch (e) { _log.error('LOAD_IMAGES', '❌ Exceção:', e.message, e); loadingState.style.display = 'none'; emptyState.style.display = 'block'; showErrorBanner('Erro ao carregar imagens', e.message); showToast('Erro ao carregar imagens', 'error'); } finally { _log.end(); } } function renderPatientImages() { imagesGrid.innerHTML = patientImages.map(image => { const thumbUrl = image.thumb_url || (image.thumb_filename ? `/uploads/${image.thumb_filename}` : (image.file_url || `/uploads/${image.filename}`)); return `
📅 ${formatDate(image.created_at)}
${escapeHtml(image.image_guid || image.filename)}
${!image.enabled ? '
Desabilitada
' : ''}
`; }).join(''); } // ================================================================ // IMAGE CLICK → TRANSFORM MODAL // ================================================================ async function handleImageClick(imageId) { selectedImageId = imageId; await showTransformModal(imageId); } async function showTransformModal(imageId) { try { transformModal.classList.add('active'); // Default view is 'transform' toggleModalView('transform'); // Find image to show details const image = patientImages.find(img => img.id === imageId); if (!image) { throw new Error('Imagem não encontrada na lista carregada.'); } // Painel "Preview" sempre mostra a versão atual (com transformações aplicadas) const imageUrl = image.filename.startsWith('http') ? image.filename : `/uploads/${image.filename}`; document.getElementById('transformedImgPreview').src = imageUrl; // Painel "Original": se a imagem foi transformada, busca a versão anterior ao processamento const btnResetImage = document.getElementById('btnResetImage'); if (image.original_image_id) { btnResetImage.style.display = 'flex'; btnResetImage.setAttribute('data-modified', 'true'); // Carrega a imagem-mãe (full-res, sem transformações) no painel Original fetchWithAuth(`${API_URL}/images/${image.original_image_id}/file-url`) .then(r => r.ok ? r.json() : null) .then(data => { const origUrl = data && data.url ? data.url : imageUrl; document.getElementById('originalImgPreview').src = origUrl; }) .catch(() => { document.getElementById('originalImgPreview').src = imageUrl; }); } else { btnResetImage.style.display = 'none'; btnResetImage.setAttribute('data-modified', 'false'); // Sem transformações: original = atual document.getElementById('originalImgPreview').src = imageUrl; } document.getElementById('transformModalTitle').innerText = `${escapeHtml(image.patient_name || 'Desconhecido')} - ${formatDate(image.created_at)}`; document.getElementById('transformExtraInfo').innerHTML = `
👨‍⚕️ Dentista: ${escapeHtml(image.doctor || 'Não informado')}
📝 Obs: ${escapeHtml(image.remark || 'Nenhuma observação')}
`; document.getElementById('transformExtraInfo').style.display = 'none'; // Keep hidden by default loadGtosForPatient(image.patient_name); // Reseta o ângulo sempre que abrir a imagem fineTuneAngle = 0; updateFineTuneDisplay(); // Exibe área de ação inferior (onde ficam os botões de salvar e observação) document.getElementById('transformActionArea').style.display = 'block'; } catch (e) { _log.error('TRANSFORM', '❌ Exceção:', e.message, e); showToast('Erro ao carregar variações', 'error'); transformModal.classList.remove('active'); } } // As transformações base foram removidas. Usamos apenas o ajuste fino. function applyFineTuneAngle(angleDelta) { if (angleDelta === 0) { fineTuneAngle = 0; } else { fineTuneAngle += angleDelta; } updateFineTuneDisplay(); // Apply to preview image const previewImg = document.getElementById('transformedImgPreview'); // Agora temos apenas o fineTuneAngle, sem outras transformações base (flip) previewImg.style.transform = `rotate(${fineTuneAngle}deg)`; } function clearTransformSelection() { resetFineTune(); document.getElementById('newImageRemark').value = ''; } async function saveSelectedTransform() { if (fineTuneAngle === 0) { showToast('Nenhuma edição aplicada.', 'error'); return; } const remark = document.getElementById('newImageRemark').value.trim(); const btn = document.getElementById('btnSaveTransform'); const originalText = btn.innerText; btn.innerText = 'Salvando...'; btn.disabled = true; try { const payload = { rotation: fineTuneAngle, flipH: false, flipV: false, remark: remark }; const res = await fetchWithAuth(`/api/images/${selectedImageId}/transform`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!res.ok) throw new Error('Erro ao salvar'); transformModal.classList.remove('active'); showToast('Orientação salva!', 'success'); if (selectedPatient) loadPatientImages(selectedPatient.patient_name); } catch (e) { console.error(e); showToast('Erro ao salvar orientação', 'error'); } } function adjustFineTune(offset) { fineTuneAngle += offset; updateFineTuneDisplay(); // Update the image style const previewImg = document.getElementById('transformedImgPreview'); previewImg.style.transform = `rotate(${fineTuneAngle}deg)`; } function resetFineTune() { fineTuneAngle = 0; updateFineTuneDisplay(); document.getElementById('transformedImgPreview').style.transform = `rotate(0deg)`; } function updateFineTuneDisplay() { const displayEl = document.getElementById('fineTuneValDisplay'); if (displayEl) { displayEl.textContent = `${fineTuneAngle > 0 ? '+' : ''}${fineTuneAngle}°`; } const resetEl = document.getElementById('btnResetFineTune'); if (resetEl) { resetEl.style.display = fineTuneAngle !== 0 ? 'inline-block' : 'none'; } } async function toggleImageEnabled(imageId) { try { const res = await fetchWithAuth(`/api/images/${imageId}/toggle-enabled`, { method: 'PUT' }); if (!res.ok) throw new Error(); const result = await res.json(); showToast(result.enabled ? 'Imagem habilitada!' : 'Imagem desabilitada!', 'success'); if (selectedPatient) loadPatientImages(selectedPatient.patient_name); } catch { showToast('Erro ao alterar status', 'error'); } } // ================================================================ // UTILITY FUNCTIONS // ================================================================ function escapeHtml(text) { if (!text) return ''; const d = document.createElement('div'); d.textContent = text; return d.innerHTML; } function formatDate(dateString) { if (!dateString) return '—'; const date = new Date(dateString); return date.toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); } function debounce(func, wait) { let timeout; return function (...args) { clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); }; } function showToast(message, type = 'info') { const toast = document.getElementById('toast'); toast.textContent = message; toast.className = `toast show ${type}`; setTimeout(() => toast.classList.remove('show'), 3000); } function openPatientByIndex(index) { if (patients[index]) { openPatient(patients[index]); } } window.openPatient = openPatient; window.openPatientByIndex = openPatientByIndex; window.handleImageClick = handleImageClick; window.toggleImageEnabled = toggleImageEnabled; // ================================================================ // PATIENT CREATION (DENTAL SENSOR) // ================================================================ function openCreatePatientModal() { document.getElementById('create-patient-modal').classList.add('active'); } function closeCreatePatientModal() { document.getElementById('create-patient-modal').classList.remove('active'); document.getElementById('create-patient-form').reset(); } async function submitCreatePatient(event) { event.preventDefault(); const submitBtn = event.target.querySelector('button[type="submit"]'); const originalText = submitBtn.textContent; submitBtn.textContent = 'Salvando...'; submitBtn.disabled = true; const payload = { firstName: document.getElementById('patient-first-name').value.trim(), lastName: document.getElementById('patient-last-name').value.trim(), doctor: document.getElementById('patient-doctor').value.trim(), birthday: document.getElementById('patient-birthday').value || null, gender: document.getElementById('patient-gender').value, phone: document.getElementById('patient-phone').value.trim(), remark: document.getElementById('patient-remark').value.trim() }; try { const response = await fetchWithAuth(`${API_URL}/patients`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (response.ok) { showToast('Paciente criado com sucesso no Dental Sensor!', 'success'); closeCreatePatientModal(); } else { const err = await response.json(); alert('Erro ao criar paciente: ' + (err.error || 'Desconhecido')); } } catch (err) { console.error(err); alert('Erro ao conectar com o servidor para criar paciente.'); } finally { submitBtn.textContent = originalText; submitBtn.disabled = false; } } // ================================================================ // GTO LOGIC // ================================================================ function toggleModalView(view) { const transformViewWrapper = document.getElementById('transformViewWrapper'); const gtoView = document.getElementById('gtoView'); const btnShowTransform = document.getElementById('btnShowTransform'); const btnShowGto = document.getElementById('btnShowGto'); const btnResetImage = document.getElementById('btnResetImage'); if (view === 'transform') { transformViewWrapper.style.display = 'flex'; gtoView.style.display = 'none'; btnShowTransform.className = 'btn btn-primary'; btnShowGto.className = 'btn btn-secondary'; // Restore reset button visibility if image is modified if (btnResetImage.getAttribute('data-modified') === 'true') { btnResetImage.style.display = 'flex'; } } else { transformViewWrapper.style.display = 'none'; gtoView.style.display = 'block'; btnShowTransform.className = 'btn btn-secondary'; btnShowGto.className = 'btn btn-primary'; // Hide reset button in GTO view btnResetImage.style.display = 'none'; } } function toggleModalInfo() { const infoDiv = document.getElementById('transformExtraInfo'); const btnShowInfo = document.getElementById('btnShowInfo'); if (infoDiv.style.display === 'none') { infoDiv.style.display = 'block'; btnShowInfo.className = 'btn btn-primary'; } else { infoDiv.style.display = 'none'; btnShowInfo.className = 'btn btn-secondary'; } } async function loadGtosForPatient(patientName) { const container = document.getElementById('gtoListContainer'); container.innerHTML = '
Carregando GTOs...
'; try { const res = await fetchWithAuth(`/api/gtos?patient=${encodeURIComponent(patientName)}`); if (!res.ok) throw new Error('Erro ao carregar GTOs'); const gtos = await res.json(); if (gtos.length === 0) { container.innerHTML = '
Nenhuma GTO cadastrada para este paciente.
'; return; } let html = ''; container.innerHTML = html; } catch (e) { console.error(e); container.innerHTML = '
Erro ao carregar GTOs
'; } } async function inlineCreateGto() { const numberInput = document.getElementById('inlineGtoNumber'); const descInput = document.getElementById('inlineGtoDesc'); const number = numberInput.value.trim(); if (!number) { showToast('O Número da GTO é obrigatório.', 'error'); return; } const image = patientImages.find(img => img.id === selectedImageId); if (!image || !image.patient_name) { showToast('Erro: Paciente não identificado.', 'error'); return; } const payload = { gto_number: number, description: descInput.value.trim(), patient_name: image.patient_name }; try { const res = await fetchWithAuth('/api/gtos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!res.ok) throw new Error('Erro ao criar GTO'); showToast('GTO criada com sucesso!', 'success'); // Limpar inputs numberInput.value = ''; descInput.value = ''; // Recarregar lista await loadGtosForPatient(image.patient_name); } catch (e) { console.error(e); showToast('Erro ao criar GTO.', 'error'); } } async function linkImageToGto(gtoId, btnElement) { if (!gtoId) return; const originalText = btnElement.innerText; btnElement.innerText = 'Vinculando...'; btnElement.disabled = true; try { const res = await fetchWithAuth(`/api/gtos/${gtoId}/images`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ image_id: selectedImageId }) }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Erro ao vincular'); } showToast('Imagem vinculada à GTO!', 'success'); btnElement.innerText = 'Vinculada!'; btnElement.classList.replace('btn-primary', 'btn-success'); } catch (e) { console.error(e); showToast(e.message, 'error'); btnElement.innerText = originalText; btnElement.disabled = false; } } async function resetImageToOriginal() { if (!confirm('Tem certeza que deseja restaurar esta imagem para o estado original em que foi capturada pelo sensor? Rotações, espelhamentos e observações serão descartados.')) { return; } const btn = document.getElementById('btnResetImage'); const originalContent = btn.innerHTML; btn.innerHTML = '
'; btn.disabled = true; try { const payload = { rotation: 0, flipH: false, flipV: false, remark: '' }; const res = await fetchWithAuth(`/api/images/${selectedImageId}/transform`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!res.ok) throw new Error('Erro ao resetar imagem'); showToast('Imagem restaurada para o original!', 'success'); transformModal.classList.remove('active'); clearTransformSelection(); const patientName = patientImages.find(img => img.id === selectedImageId)?.patient_name; await loadPatients(); if (patientName) { await loadPatientImages(patientName); } } catch (e) { console.error(e); showToast('Erro ao restaurar a imagem.', 'error'); } finally { btn.innerHTML = originalContent; btn.disabled = false; } } // Global scope exposures window.handleImageClick = handleImageClick; window.toggleImageEnabled = toggleImageEnabled; window.openCreatePatientModal = openCreatePatientModal; window.closeCreatePatientModal = closeCreatePatientModal; window.submitCreatePatient = submitCreatePatient; window.toggleModalView = toggleModalView; window.toggleModalInfo = toggleModalInfo; window.inlineCreateGto = inlineCreateGto; window.saveSelectedTransform = saveSelectedTransform; window.clearTransformSelection = clearTransformSelection; window.resetImageToOriginal = resetImageToOriginal; // ================================================================ // CONFIGURAÇÕES (Trocar Usuário/Senha) // ================================================================ function openSettingsModal() { const modal = document.getElementById('settingsModal'); if (modal) modal.classList.add('active'); // Limpar form e carregar valores atuais document.getElementById('currentPassword').value = ''; document.getElementById('newUsername').value = localStorage.getItem('username') || ''; document.getElementById('newEmail').value = localStorage.getItem('email') || ''; document.getElementById('newPassword').value = ''; } function closeSettingsModal() { const modal = document.getElementById('settingsModal'); if (modal) modal.classList.remove('active'); } async function updateCredentials(event) { event.preventDefault(); const currentPassword = document.getElementById('currentPassword').value; const newUsername = document.getElementById('newUsername').value.trim(); const newEmail = document.getElementById('newEmail').value.trim(); const newPassword = document.getElementById('newPassword').value; if (!currentPassword) { showToast('A senha atual é obrigatória.', 'error'); return; } if (!newUsername && !newEmail && !newPassword) { showToast('Preencha um novo usuário, e-mail ou nova senha.', 'error'); return; } const btn = document.getElementById('btnSaveSettings'); const originalText = btn.innerHTML; btn.innerHTML = ' Salvando...'; btn.disabled = true; try { const token = getAuthToken(); const res = await fetch(`${API_URL}/auth/update-credentials`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ currentPassword, newUsername, newEmail, newPassword }) }); const data = await res.json(); if (!res.ok) { throw new Error(data.error || 'Erro ao atualizar credenciais.'); } // Sucesso showToast('Credenciais atualizadas com sucesso!', 'success'); // Atualizar token no localStorage se tiver um novo if (data.token) { localStorage.setItem('auth_token', data.token); // Salvar novos dados locais decodificando token ou usando retorno if (data.user) { localStorage.setItem('username', data.user.username || ''); localStorage.setItem('email', data.user.email || ''); } } closeSettingsModal(); } catch (e) { console.error(e); showToast(e.message, 'error'); } finally { btn.innerHTML = originalText; btn.disabled = false; } } // Global scope window.openSettingsModal = openSettingsModal; window.closeSettingsModal = closeSettingsModal; window.updateCredentials = updateCredentials; // ================================================================ // GERENCIAMENTO DE USUÁRIOS (ADMIN ONLY) // ================================================================ function openUserManagementModal() { const modal = document.getElementById('userManagementModal'); if (modal) modal.classList.add('active'); // Limpar formulário de criação document.getElementById('new_username').value = ''; document.getElementById('new_email').value = ''; document.getElementById('new_password').value = ''; const isCheck = document.getElementById('new_is_admin'); if (isCheck) isCheck.checked = false; loadUsersList(); } function closeUserManagementModal() { const modal = document.getElementById('userManagementModal'); if (modal) modal.classList.remove('active'); } async function loadUsersList() { const listBody = document.getElementById('usersListBody'); if (!listBody) return; listBody.innerHTML = ' Carregando...'; try { const token = getAuthToken(); const res = await fetch(`${API_URL}/auth/users`, { headers: { 'Authorization': `Bearer ${token}` } }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Erro ao carregar usuários.'); if (data.success && data.users) { listBody.innerHTML = ''; // Decodifica o token atual para não permitir excluir a si mesmo let currentUserId = null; try { const payload = JSON.parse(atob(token.split('.')[1])); currentUserId = payload.id; } catch (e) {} data.users.forEach(u => { const role = u.is_admin ? '👑 Administrador' : '🩺 Dentista / Comum'; const tr = document.createElement('tr'); tr.style.borderBottom = '1px solid var(--border-color)'; const isSelf = currentUserId && parseInt(u.id) === parseInt(currentUserId); const deleteBtnHtml = isSelf ? 'Você' : ``; tr.innerHTML = ` ${u.username} ${u.email} ${role} ${deleteBtnHtml} `; listBody.appendChild(tr); }); } else { listBody.innerHTML = 'Falha ao carregar dados.'; } } catch (e) { console.error(e); listBody.innerHTML = `${e.message || 'Erro ao carregar.'}`; } } async function createUser(event) { event.preventDefault(); const username = document.getElementById('new_username').value.trim(); const email = document.getElementById('new_email').value.trim(); const password = document.getElementById('new_password').value; const isCheck = document.getElementById('new_is_admin'); const is_admin = isCheck ? isCheck.checked : false; if (!username || !email || !password) { showToast('Todos os campos obrigatórios.', 'error'); return; } const submitBtn = event.target.querySelector('button[type="submit"]'); const originalText = submitBtn.innerHTML; submitBtn.innerHTML = ' Cadastrando...'; submitBtn.disabled = true; try { const token = getAuthToken(); const res = await fetch(`${API_URL}/auth/create-user`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ username, email, password, is_admin }) }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Erro ao cadastrar usuário.'); showToast('Usuário cadastrado com sucesso!', 'success'); // Limpar formulário document.getElementById('new_username').value = ''; document.getElementById('new_email').value = ''; document.getElementById('new_password').value = ''; if (isCheck) isCheck.checked = false; // Atualizar lista loadUsersList(); } catch (e) { console.error(e); showToast(e.message, 'error'); } finally { submitBtn.innerHTML = originalText; submitBtn.disabled = false; } } async function deleteUser(userId) { if (!confirm('Tem certeza que deseja excluir este usuário permanentemente?')) return; try { const token = getAuthToken(); const res = await fetch(`${API_URL}/auth/users/${userId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Erro ao excluir usuário.'); showToast('Usuário excluído com sucesso!', 'success'); loadUsersList(); } catch (e) { console.error(e); showToast(e.message, 'error'); } } window.openUserManagementModal = openUserManagementModal; window.closeUserManagementModal = closeUserManagementModal; window.createUser = createUser; window.deleteUser = deleteUser; // ================================================================ // CONFIGURAÇÕES DE PLUGINS (Wasabi Storage) // ================================================================ function openPluginsModal() { const modal = document.getElementById('pluginsModal'); if (modal) modal.classList.add('active'); // Limpar campos document.getElementById('wasabiAccessKey').value = ''; document.getElementById('wasabiSecretKey').value = ''; document.getElementById('wasabiBucket').value = ''; document.getElementById('wasabiRegion').value = ''; document.getElementById('wasabiEndpoint').value = ''; const alertBox = document.getElementById('pluginsStatusAlert'); alertBox.style.display = 'none'; // Carregar configurações atuais loadPluginsConfig(); } function closePluginsModal() { const modal = document.getElementById('pluginsModal'); if (modal) modal.classList.remove('active'); } async function loadPluginsConfig() { try { const res = await fetchWithAuth('/api/system/storage-config'); if (!res.ok) throw new Error('Falha ao carregar configurações'); const config = await res.json(); document.getElementById('wasabiAccessKey').value = config.wasabiAccessKey || ''; document.getElementById('wasabiSecretKey').value = config.wasabiSecretKey || ''; document.getElementById('wasabiBucket').value = config.wasabiBucket || ''; document.getElementById('wasabiRegion').value = config.wasabiRegion || ''; document.getElementById('wasabiEndpoint').value = config.wasabiEndpoint || ''; const alertBox = document.getElementById('pluginsStatusAlert'); if (config.enabled) { alertBox.className = 'alert-success'; alertBox.style.background = '#eafaf1'; alertBox.style.color = '#27ae60'; alertBox.style.border = '1px solid #2ecc7133'; alertBox.innerHTML = '🟢 Status: Wasabi Storage está ATIVO e conectado.'; } else { alertBox.className = 'alert-warning'; alertBox.style.background = '#fffbeb'; alertBox.style.color = '#d97706'; alertBox.style.border = '1px solid #f59e0b33'; alertBox.innerHTML = '🟡 Status: Wasabi Storage está DESATIVADO (utilizando armazenamento local).'; } alertBox.style.display = 'block'; } catch (e) { console.error(e); showToast('Erro ao carregar configurações do Wasabi.', 'error'); } } async function checkWasabiStatus() { try { const res = await fetchWithAuth('/api/system/wasabi-status'); if (res.ok) { const status = await res.json(); const banner = document.getElementById('wasabi-alert-banner'); const errorMsg = document.getElementById('wasabi-alert-error'); if (status.enabled && status.error) { // Existe um erro crítico, exibe o banner if (errorMsg) errorMsg.textContent = status.error; if (banner) banner.style.display = 'flex'; } else { // Tudo certo, esconde o banner if (banner) banner.style.display = 'none'; } } } catch (e) { console.error('Falha ao verificar status do Wasabi:', e); } } async function updatePluginsConfig(event) { event.preventDefault(); const wasabiAccessKey = document.getElementById('wasabiAccessKey').value.trim(); const wasabiSecretKey = document.getElementById('wasabiSecretKey').value; const wasabiBucket = document.getElementById('wasabiBucket').value.trim(); const wasabiRegion = document.getElementById('wasabiRegion').value.trim(); const wasabiEndpoint = document.getElementById('wasabiEndpoint').value.trim(); const btn = document.getElementById('btnSavePlugins'); const originalText = btn.innerHTML; btn.innerHTML = ' Salvando...'; btn.disabled = true; try { const res = await fetchWithAuth('/api/system/storage-config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ wasabiAccessKey, wasabiSecretKey, wasabiBucket, wasabiRegion, wasabiEndpoint }) }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Erro ao salvar configurações.'); } showToast('Configurações do Wasabi salvas com sucesso!', 'success'); // Recarregar o status await loadPluginsConfig(); checkWasabiStatus(); // Atualizar o banner na mesma hora // Fechar após 1 segundo setTimeout(closePluginsModal, 1200); } catch (e) { console.error(e); // Se for um erro 400 de validação da key, usamos um Alert para chamar a atenção if (e.message.includes('Chave') || e.message.includes('bucket') || e.message.includes('Erro 403') || e.message.includes('conexão')) { alert("⚠️ " + e.message); } else { showToast(e.message, 'error'); } } finally { btn.innerHTML = originalText; btn.disabled = false; } } async function triggerSync() { showToast('🔄 Enviando sinal de sincronização para dispositivos...', 'info'); try { const res = await fetchWithAuth('/api/system/trigger-sync', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Erro ao acionar sincronização.'); } const data = await res.json(); if (data.devicesTriggered > 0) { showToast(`✅ Sinal enviado para ${data.devicesTriggered} dispositivo(s) conectado(s)!`, 'success'); } else { showToast('⚠️ Nenhum dispositivo Windows conectado no momento.', 'warning'); } } catch (e) { console.error(e); showToast(e.message, 'error'); } } // Global scope window.openPluginsModal = openPluginsModal; window.closePluginsModal = closePluginsModal; window.updatePluginsConfig = updatePluginsConfig; window.triggerSync = triggerSync; // ================================================================ // ÁREA DE SINCRONIZAÇÃO E UPLOAD MANUAL // ================================================================ let selectedUploadFiles = []; function openSyncModal(defaultTab = 'devices') { const modal = document.getElementById('syncModal'); if (!modal) return; // Usuário não-admin sempre vai para aba upload const isAdmin = document.body.classList.contains('is-admin'); const targetTab = (!isAdmin || defaultTab === 'upload') ? 'upload' : 'devices'; modal.classList.add('active'); // Resetar abas switchSyncTab(targetTab); // Limpar formulário de upload resetManualUploadForm(); // Carregar dados if (targetTab === 'devices') { loadSyncDevices(); } refreshUploadPatients(); // Escuta de clique fora para fechar modal modal.onclick = (e) => { if (e.target === modal) closeSyncModal(); }; } function closeSyncModal() { const modal = document.getElementById('syncModal'); if (!modal) return; modal.classList.remove('active'); } function switchSyncTab(tabName) { const btnDevices = document.getElementById('tabBtnDevices'); const btnUpload = document.getElementById('tabBtnUpload'); const tabDevices = document.getElementById('tab-devices'); const tabUpload = document.getElementById('tab-upload'); if (!btnDevices || !btnUpload || !tabDevices || !tabUpload) return; if (tabName === 'devices') { btnDevices.classList.add('active'); btnUpload.classList.remove('active'); tabDevices.classList.add('active'); tabUpload.classList.remove('active'); loadSyncDevices(); } else { btnDevices.classList.remove('active'); btnUpload.classList.add('active'); tabDevices.classList.remove('active'); tabUpload.classList.add('active'); } } async function loadSyncDevices() { const loader = document.getElementById('syncDevicesLoading'); const empty = document.getElementById('syncDevicesEmpty'); const list = document.getElementById('syncDeviceList'); if (!loader || !empty || !list) return; loader.style.display = 'block'; empty.style.display = 'none'; list.style.display = 'none'; try { const res = await fetchWithAuth('/api/socket/status'); if (!res.ok) throw new Error('Falha ao obter status dos dispositivos.'); const data = await res.json(); list.replaceChildren(); const windowsClients = (data.identified || []).filter(c => c.type === 'client' || c.type === 'windows'); if (windowsClients.length === 0) { loader.style.display = 'none'; empty.style.display = 'block'; list.style.display = 'none'; return; } windowsClients.forEach(client => { const card = document.createElement('div'); card.className = 'sync-device-card'; const left = document.createElement('div'); left.className = 'device-card-left'; const title = document.createElement('div'); title.className = 'device-card-title'; title.textContent = `💻 ${client.clinicName && client.clinicName !== 'Clínica não identificada' ? client.clinicName + ' — ' : ''}${client.name || 'Dispositivo Sem Nome'}`; const subtitle = document.createElement('div'); subtitle.className = 'device-card-subtitle'; subtitle.textContent = `SO: ${client.system || 'Windows'} | Versão: ${client.version || 'N/A'} | ID: ${client.socketId}`; left.appendChild(title); left.appendChild(subtitle); const badge = document.createElement('span'); badge.className = 'device-card-badge badge-connected'; badge.textContent = 'ONLINE'; card.appendChild(left); card.appendChild(badge); list.appendChild(card); }); loader.style.display = 'none'; empty.style.display = 'none'; list.style.display = 'flex'; } catch (e) { console.error(e); loader.style.display = 'none'; empty.style.display = 'block'; const p = empty.querySelector('p'); if (p) p.textContent = 'Erro ao carregar dispositivos: ' + e.message; } } async function triggerSyncFromModal() { const btn = document.getElementById('btnTriggerSyncModal'); if (!btn) return; const originalText = btn.innerHTML; btn.disabled = true; btn.innerHTML = '🔄 Enviando sinal...'; try { const res = await fetchWithAuth('/api/system/trigger-sync', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Erro ao acionar sincronização.'); } const data = await res.json(); if (data.devicesTriggered > 0) { showToast(`✅ Sinal enviado para ${data.devicesTriggered} dispositivo(s) conectado(s)!`, 'success'); } else { showToast('⚠️ Nenhum dispositivo Windows conectado para sincronizar.', 'warning'); } } catch (e) { console.error(e); showToast(e.message, 'error'); } finally { btn.disabled = false; btn.innerHTML = originalText; } } async function refreshUploadPatients() { const select = document.getElementById('uploadPatientSelect'); if (!select) return; // Manter as primeiras duas opções while (select.options.length > 2) { select.remove(2); } try { const res = await fetchWithAuth('/api/images/patients?disabled=false&search='); if (!res.ok) throw new Error('Erro ao listar pacientes.'); const patients = await res.json(); patients.forEach(p => { if (!p || !p.patient_name) return; const opt = document.createElement('option'); opt.value = p.patient_name; opt.textContent = p.patient_name; select.appendChild(opt); }); } catch (e) { console.error(e); showToast('Erro ao carregar lista de pacientes.', 'error'); } } function toggleNewPatientFields() { const select = document.getElementById('uploadPatientSelect'); const fields = document.getElementById('newPatientFields'); if (!select || !fields) return; if (select.value === 'NEW_PATIENT') { fields.style.display = 'block'; document.getElementById('uploadNewPatientFirstName').required = true; document.getElementById('uploadNewPatientLastName').required = true; } else { fields.style.display = 'none'; document.getElementById('uploadNewPatientFirstName').required = false; document.getElementById('uploadNewPatientLastName').required = false; } } function resetManualUploadForm() { selectedUploadFiles = []; const form = document.getElementById('manualUploadForm'); if (form) form.reset(); const previews = document.getElementById('uploadPreviewsContainer'); if (previews) previews.replaceChildren(); const progressContainer = document.getElementById('uploadProgressContainer'); if (progressContainer) progressContainer.style.display = 'none'; const fields = document.getElementById('newPatientFields'); if (fields) fields.style.display = 'none'; const btnSubmit = document.getElementById('btnSubmitManualUpload'); if (btnSubmit) btnSubmit.disabled = true; } // Inicializar Drag and Drop function initDragAndDrop() { const zone = document.getElementById('uploadDragDropZone'); const fileInput = document.getElementById('uploadFileInput'); if (!zone || !fileInput) return; // Evento de clique na zona abre o seletor de arquivos zone.addEventListener('click', () => fileInput.click()); ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { zone.addEventListener(eventName, e => { e.preventDefault(); e.stopPropagation(); }, false); }); ['dragenter', 'dragover'].forEach(eventName => { zone.addEventListener(eventName, () => { zone.classList.add('drag-active'); }, false); }); ['dragleave', 'drop'].forEach(eventName => { zone.addEventListener(eventName, () => { zone.classList.remove('drag-active'); }, false); }); zone.addEventListener('drop', e => { const dt = e.dataTransfer; if (dt) handleUploadFiles(dt.files); }); fileInput.addEventListener('change', e => { if (e.target && e.target.files) handleUploadFiles(e.target.files); }); } function handleUploadFiles(files) { if (!files || files.length === 0) return; for (let i = 0; i < files.length; i++) { const file = files[i]; if (!file.type.match('image.*')) { showToast(`O arquivo "${file.name}" não é uma imagem válida!`, 'warning'); continue; } if (file.size > 10 * 1024 * 1024) { showToast(`A imagem "${file.name}" excede o limite de tamanho de 10MB!`, 'warning'); continue; } if (selectedUploadFiles.some(f => f.name === file.name && f.size === file.size)) { continue; } selectedUploadFiles.push(file); } renderUploadPreviews(); } function renderUploadPreviews() { const container = document.getElementById('uploadPreviewsContainer'); const btnSubmit = document.getElementById('btnSubmitManualUpload'); if (!container) return; container.replaceChildren(); selectedUploadFiles.forEach((file, index) => { const item = document.createElement('div'); item.className = 'preview-item'; const img = document.createElement('img'); img.alt = file.name; const reader = new FileReader(); reader.onload = e => { if (e.target && e.target.result) img.src = e.target.result; }; reader.readAsDataURL(file); const btnRemove = document.createElement('button'); btnRemove.type = 'button'; btnRemove.className = 'preview-remove'; btnRemove.innerHTML = '×'; btnRemove.title = 'Remover Imagem'; btnRemove.onclick = (e) => { e.stopPropagation(); removeUploadPreview(index); }; item.appendChild(img); item.appendChild(btnRemove); container.appendChild(item); }); if (btnSubmit) { btnSubmit.disabled = selectedUploadFiles.length === 0; } } function removeUploadPreview(index) { selectedUploadFiles.splice(index, 1); renderUploadPreviews(); } function readFileAsBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = error => reject(error); reader.readAsDataURL(file); }); } async function handleManualUploadSubmit(event) { event.preventDefault(); if (selectedUploadFiles.length === 0) { showToast('Selecione pelo menos uma imagem para enviar.', 'warning'); return; } const select = document.getElementById('uploadPatientSelect'); const remarkInput = document.getElementById('uploadRemark'); const btnSubmit = document.getElementById('btnSubmitManualUpload'); if (!select || !btnSubmit) return; let patientName = ''; let doctor = ''; if (select.value === 'NEW_PATIENT') { const firstName = document.getElementById('uploadNewPatientFirstName').value.trim(); const lastName = document.getElementById('uploadNewPatientLastName').value.trim(); doctor = document.getElementById('uploadNewPatientDoctor').value.trim(); if (!firstName || !lastName) { showToast('Nome e sobrenome são obrigatórios para novo paciente.', 'warning'); return; } patientName = `${firstName} ${lastName}`; } else { patientName = select.value; } const remark = remarkInput ? remarkInput.value.trim() : ''; const progressContainer = document.getElementById('uploadProgressContainer'); const progressText = document.getElementById('uploadProgressText'); const progressBarFill = document.getElementById('uploadProgressBarFill'); const progressPercent = document.getElementById('uploadProgressPercent'); if (progressContainer) progressContainer.style.display = 'block'; btnSubmit.disabled = true; const totalFiles = selectedUploadFiles.length; let successCount = 0; for (let i = 0; i < totalFiles; i++) { const file = selectedUploadFiles[i]; if (progressText) progressText.textContent = `Enviando imagem ${i + 1} de ${totalFiles}...`; if (progressBarFill) { const pct = Math.round((i / totalFiles) * 100); progressBarFill.style.width = `${pct}%`; if (progressPercent) progressPercent.textContent = `${pct}%`; } try { const base64 = await readFileAsBase64(file); const response = await fetchWithAuth('/api/images/upload', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ imageBase64: base64, filename: file.name, patientName: patientName, doctor: doctor || null, remark: remark || null }) }); if (!response.ok) { const err = await response.json(); throw new Error(err.error || `Erro HTTP ${response.status}`); } successCount++; } catch (e) { console.error(`Erro ao enviar arquivo ${file.name}:`, e); showToast(`Falha ao enviar a imagem ${file.name}: ${e.message}`, 'error'); } } if (progressBarFill) { progressBarFill.style.width = '100%'; if (progressPercent) progressPercent.textContent = '100%'; } if (progressText) progressText.textContent = `Finalizado! ${successCount} de ${totalFiles} imagens enviadas.`; setTimeout(async () => { closeSyncModal(); showToast(`✅ ${successCount} imagem(ns) enviada(s) com sucesso!`, 'success'); await loadPatients(); if (window.loadClientsList) { await window.loadClientsList(); } }, 1000); } // Vinculando ao window window.openSyncModal = openSyncModal; window.closeSyncModal = closeSyncModal; window.switchSyncTab = switchSyncTab; window.triggerSyncFromModal = triggerSyncFromModal; window.refreshUploadPatients = refreshUploadPatients; window.toggleNewPatientFields = toggleNewPatientFields; window.handleManualUploadSubmit = handleManualUploadSubmit; window.initDragAndDrop = initDragAndDrop; window.deletePatientImages = deletePatientImages; window.adjustFineTune = adjustFineTune; window.resetFineTune = resetFineTune; // A aplicação é inicializada no evento DOMContentLoaded acima function logout() { localStorage.removeItem('auth_token'); window.location.href = '/login'; }