// ================================================================ // DENTAL IMAGE MANAGER - Main JavaScript // Two-Level Navigation: Patients → Images → Transform // ================================================================ const API_URL = '/api'; // ================================================================ // ERROR LOGGER — Logs to console AND shows on screen // ================================================================ const _log = { info: (tag, ...args) => console.log(`%c[${tag}]`, 'color:#667eea;font-weight:bold', ...args), warn: (tag, ...args) => console.warn(`%c[${tag}]`, 'color:#ffa500;font-weight:bold', ...args), error: (tag, ...args) => console.error(`%c[${tag}]`, 'color:#ff4444;font-weight:bold', ...args), group: (tag) => console.group(`%c[${tag}]`, 'color:#667eea;font-weight:bold'), end: () => console.groupEnd() }; 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) 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; } } 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 currentTransformations = []; 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(); setupEventListeners(); initDragAndDrop(); setInterval(loadClientsList, 5000); } 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', () => showPatientsView()); // 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 { const r = await fetch('/api/socket/status'); const data = await r.json(); clientsList = (data.identified || []).map(c => ({ ...c, name: c.name || 'Unknown', displayName: c.name || 'Unknown', dbName: c.name || 'Unknown', status: 'identified' })); const imagesRes = await fetchWithAuth('/api/images/patients?disabled=false&search='); if (imagesRes.ok) { const pts = await imagesRes.json(); const uniqueClients = [...new Set(pts.map(p => p.client_name).filter(Boolean))]; 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 // ================================================================ function showPatientsView() { view = 'patients'; selectedPatient = null; backBtn.style.display = 'none'; patientHeader.style.display = 'none'; loadPatients(); } async function loadPatients(silent = false) { _log.group('LOAD_PATIENTS'); _log.info('LOAD_PATIENTS', `Iniciando carga. silent=${silent}, showDisabled=${showDisabled}, client="${selectedClient}"`); 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}`; 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'}`); } patients = await res.json(); _log.info('LOAD_PATIENTS', `✅ ${patients.length} paciente(s) recebido(s)`); 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'; _log.info('LOAD_PATIENTS', `imagesGrid display=${imagesGrid.style.display}`); renderPatientCards(); } } catch (e) { _log.error('LOAD_PATIENTS', '❌ Exceção:', e.message, e); loadingState.style.display = 'none'; emptyState.style.display = 'block'; showErrorBanner('Erro ao carregar pacientes', e.message); showToast('Erro ao carregar pacientes', 'error'); } finally { _log.end(); } } function renderPatientCards() { _log.group('RENDER_CARDS'); _log.info('RENDER_CARDS', `Renderizando ${patients.length} card(s)...`); // Inspecionar o grid antes de renderizar const gridStyle = window.getComputedStyle(imagesGrid); _log.info('RENDER_CARDS', 'imagesGrid computed style:', { display: gridStyle.display, gridTemplateColumns: gridStyle.gridTemplateColumns, width: gridStyle.width, height: gridStyle.height, overflow: gridStyle.overflow }); imagesGrid.innerHTML = patients.map((p, index) => { const fullName = p.patient_name || 'Sem nome'; const thumb = 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 || '—'; if (index === 0) { _log.info('RENDER_CARDS', 'Card #0 dados:', { fullName, thumb, count, date, doctor }); } return `
${count} imagem${count !== 1 ? 'ns' : ''}
${escapeHtml(fullName)}
📅 ${date}
🩺 Dentista: ${escapeHtml(doctor)}
📝 Obs: ${escapeHtml(remark)}
`; }).join(''); // Inspecionar o primeiro card renderizado requestAnimationFrame(() => { const firstCard = imagesGrid.querySelector('.image-card'); if (firstCard) { const cs = window.getComputedStyle(firstCard); const preview = firstCard.querySelector('.image-preview'); const pcs = preview ? window.getComputedStyle(preview) : null; _log.info('RENDER_CARDS', 'Primeiro card computado:', { cardW: cs.width, cardH: cs.height, previewH: pcs ? pcs.height : 'n/a', previewBg: pcs ? pcs.backgroundImage.substring(0, 60) : 'n/a' }); } else { _log.error('RENDER_CARDS', '❌ Nenhum card encontrado no DOM após render!'); } _log.info('RENDER_CARDS', `Total de cards no DOM: ${imagesGrid.querySelectorAll('.image-card').length}`); _log.end(); }); } // 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) { 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 || '—'}`; loadPatientImages(patient.patient_name); } 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_filename ? `/uploads/${image.thumb_filename}` : `/uploads/${image.filename}`; const isDerived = !!image.original_image_id; 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) { transformModal.classList.add('active'); // Default view is 'transform' toggleModalView('transform'); // Find image to show details const image = patientImages.find(img => img.id === imageId); // Check if the image has been modified (has original_image_id) const btnResetImage = document.getElementById('btnResetImage'); const isDerived = !!(image && image.original_image_id); if (isDerived) { btnResetImage.style.display = 'flex'; btnResetImage.setAttribute('data-modified', 'true'); } else { btnResetImage.style.display = 'none'; btnResetImage.setAttribute('data-modified', 'false'); } if (image) { 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); } else { document.getElementById('transformModalTitle').innerText = `Opções da Imagem`; document.getElementById('transformExtraInfo').innerHTML = ''; document.getElementById('transformExtraInfo').style.display = 'none'; } if (isDerived) { const imageUrl = image.thumb_filename ? `/uploads/${image.thumb_filename}` : `/uploads/${image.filename}`; const container = document.getElementById('transformOptions'); container.innerHTML = `
Imagem Orientada
Imagem Orientada

ℹ️ Orientação Concluída

Esta imagem já é o resultado de uma rotação/espelhamento. Para aplicar novas transformações, primeiro restaure a imagem original clicando no botão de restaurar ↩️ no menu superior.

`; clearTransformSelection(); return; } document.getElementById('transformOptions').innerHTML = `

Gerando variações...

`; try { const image = patientImages.find(img => img.id === imageId); if (!image) { throw new Error('Imagem não encontrada localmente'); } // Se existir thumb_filename, usar ele; caso contrário, fallback para a imagem cheia const imageUrl = image.thumb_filename ? `/uploads/${image.thumb_filename}` : `/uploads/${image.filename}`; const transformations = [ { name: 'Original', rotation: 0, flipH: false, flipV: false, css: 'transform: none;' }, { name: 'Flip Horizontal', rotation: 0, flipH: true, flipV: false, css: 'transform: scaleX(-1);' }, { name: 'Flip Vertical', rotation: 0, flipH: false, flipV: true, css: 'transform: scaleY(-1);' }, { name: 'Rotação +90°', rotation: 90, flipH: false, flipV: false, css: 'transform: rotate(90deg);' }, { name: 'Rotação -90°', rotation: -90, flipH: false, flipV: false, css: 'transform: rotate(-90deg);' } ]; currentTransformations = transformations; const container = document.getElementById('transformOptions'); container.innerHTML = transformations.map((t, i) => `
${escapeHtml(t.name)}
${escapeHtml(t.name)}
`).join(''); // --------------------------------------------------------------- // Detectar orientação da imagem original → aplicar layout correto // --------------------------------------------------------------- const probe = new Image(); probe.onload = () => { const orientation = probe.naturalWidth >= probe.naturalHeight ? 'landscape' : 'portrait'; container.classList.remove('grid-landscape', 'grid-portrait'); container.classList.add(`grid-${orientation}`); _log.info('TRANSFORM', `Orientação detectada: ${orientation} (${probe.naturalWidth}×${probe.naturalHeight})`); }; probe.onerror = () => { _log.warn('TRANSFORM', 'Não foi possível detectar orientação — usando fallback'); }; probe.src = imageUrl; } catch (e) { _log.error('TRANSFORM', '❌ Exceção:', e.message, e); showToast('Erro ao carregar variações', 'error'); transformModal.classList.remove('active'); } } function selectTransform(index) { const el = document.querySelector(`.transform-option[data-index="${index}"]`); if (el.classList.contains('selected')) { // If clicking the already selected one, clear selection clearTransformSelection(); return; } const container = document.getElementById('transformOptions'); container.classList.add('selection-mode'); document.querySelectorAll('.transform-option').forEach(option => { option.classList.remove('selected'); const optIndex = parseInt(option.getAttribute('data-index')); if (optIndex === 0 || optIndex === index) { option.style.display = 'block'; } else { option.style.display = 'none'; } }); el.classList.add('selected'); // Reset fine tune angle when selecting a new transform fineTuneAngle = 0; updateFineTuneDisplay(); // Mover a legenda do card selecionado para o placeholder de ajuste fino const nameEl = el.querySelector('.transform-name'); const placeholder = document.getElementById('fineTuneLabelPlaceholder'); if (nameEl && placeholder) { placeholder.appendChild(nameEl); } // Mover o contêiner de ajuste fino para dentro do card selecionado const ftContainer = document.getElementById('fineTuneContainer'); if (ftContainer) { el.appendChild(ftContainer); ftContainer.style.display = 'flex'; } // Show action area document.getElementById('transformActionArea').style.display = 'block'; // Pre-fill remark if we have an image const image = patientImages.find(img => img.id === selectedImageId); if (image && !document.getElementById('newImageRemark').value) { document.getElementById('newImageRemark').value = image.remark || ''; } } function clearTransformSelection() { const container = document.getElementById('transformOptions'); container.classList.remove('selection-mode'); // Devolver a legenda ativa para o card selecionado const ftPlaceholder = document.getElementById('fineTuneLabelPlaceholder'); if (ftPlaceholder) { const activeLabel = ftPlaceholder.querySelector('.transform-name'); const selectedEl = document.querySelector('.transform-option.selected'); if (activeLabel && selectedEl) { selectedEl.appendChild(activeLabel); } } // Mover o contêiner de ajuste fino de volta para o Wrapper e esconder const ftContainer = document.getElementById('fineTuneContainer'); if (ftContainer) { const viewWrapper = document.getElementById('transformViewWrapper'); if (viewWrapper) { viewWrapper.appendChild(ftContainer); } else { document.body.appendChild(ftContainer); } ftContainer.style.display = 'none'; } // Restore original transform styles on all images document.querySelectorAll('.transform-option').forEach(option => { option.classList.remove('selected'); option.style.display = 'block'; const optIndex = parseInt(option.getAttribute('data-index')); const transform = currentTransformations[optIndex]; const imgEl = option.querySelector('img'); if (imgEl && transform) { imgEl.style.transform = transform.css; } const nameEl = option.querySelector('.transform-name'); if (nameEl && transform) { nameEl.innerText = transform.name; } }); fineTuneAngle = 0; updateFineTuneDisplay(); document.getElementById('transformActionArea').style.display = 'none'; document.getElementById('newImageRemark').value = ''; } async function saveSelectedTransform() { const selectedEl = document.querySelector('.transform-option.selected'); if (!selectedEl) { showToast('Selecione uma orientação primeiro.', 'error'); return; } const index = parseInt(selectedEl.getAttribute('data-index')); const transform = currentTransformations[index]; 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: transform.rotation + fineTuneAngle, flipH: transform.flipH, flipV: transform.flipV, 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'); clearTransformSelection(); 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) { const selectedEl = document.querySelector('.transform-option.selected'); if (!selectedEl) return; const index = parseInt(selectedEl.getAttribute('data-index')); const transform = currentTransformations[index]; fineTuneAngle += offset; updateFineTuneDisplay(); // Update the image style using the combined transform const imgEl = selectedEl.querySelector('img'); if (imgEl) { let totalRotation = transform.rotation + fineTuneAngle; let transformStr = `rotate(${totalRotation}deg)`; if (transform.flipH) transformStr += ` scaleX(-1)`; if (transform.flipV) transformStr += ` scaleY(-1)`; imgEl.style.transform = transformStr; } // Update display name inside the card const nameEl = selectedEl.querySelector('.transform-name'); if (nameEl) { nameEl.innerText = `${transform.name} (${fineTuneAngle > 0 ? '+' : ''}${fineTuneAngle}°)`; } } function resetFineTune() { adjustFineTune(-fineTuneAngle); } 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.selectTransform = selectTransform; 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.selectTransform = selectTransform; 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.style.display = 'block'; // Limpar form document.getElementById('currentPassword').value = ''; document.getElementById('newUsername').value = ''; document.getElementById('newPassword').value = ''; } function closeSettingsModal() { const modal = document.getElementById('settingsModal'); if (modal) modal.style.display = 'none'; } async function updateCredentials(event) { event.preventDefault(); const currentPassword = document.getElementById('currentPassword').value; const newUsername = document.getElementById('newUsername').value.trim(); const newPassword = document.getElementById('newPassword').value; if (!currentPassword) { showToast('A senha atual é obrigatória.', 'error'); return; } if (!newUsername && !newPassword) { showToast('Preencha um novo usuário 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, 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); } 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; // ================================================================ // CONFIGURAÇÕES DE PLUGINS (Wasabi Storage) // ================================================================ function openPluginsModal() { const modal = document.getElementById('pluginsModal'); if (modal) modal.style.display = 'block'; // 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.style.display = 'none'; } 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 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(); // Fechar após 1 segundo setTimeout(closePluginsModal, 1200); } catch (e) { console.error(e); 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() { const modal = document.getElementById('syncModal'); if (!modal) return; modal.classList.add('active'); modal.style.display = 'flex'; // Resetar abas switchSyncTab('devices'); // Limpar formulário de upload resetManualUploadForm(); // Carregar dados 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'); modal.style.display = 'none'; } 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.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