// ================================================================ // 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; let currentUser = null; 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) { currentUser = data.user; if (currentUser && currentUser.is_admin) { const navAdmin = document.getElementById('navAdminClinics'); if (navAdmin) navAdmin.style.display = 'flex'; } 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 = []; // ================================================================ // 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(); 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.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(); }); } // ================================================================ // 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 => `
📅 ${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'); if (image && image.original_image_id) { 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'; } document.getElementById('transformOptions').innerHTML = `

Gerando variações...

`; try { const res = await fetchWithAuth(`/api/images/${imageId}/transformations`); if (!res.ok) { if (res.status === 401 || res.status === 403) { localStorage.removeItem('auth_token'); window.location.href = '/login'; return; } throw new Error('Erro ao gerar transformações'); } const transformations = await res.json(); 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 // --------------------------------------------------------------- if (transformations.length > 0 && transformations[0].base64) { 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 = transformations[0].base64; } } catch (e) { _log.error('TRANSFORM', '❌ Exceção:', e.message, e); showToast('Erro ao gerar 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'); // 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'); document.querySelectorAll('.transform-option').forEach(option => { option.classList.remove('selected'); option.style.display = 'block'; }); 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, 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({ rotation: transform.rotation, flipH: transform.flipH, flipV: transform.flipV }) }); 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'); } } 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; // ================================================================ // GERENCIAMENTO DE CLÍNICAS (ADMIN) // ================================================================ function showAdminClinics() { view = 'admin-clinics'; document.querySelector('.content-scroll').scrollTop = 0; // Esconder outras visões imagesGrid.style.display = 'none'; emptyState.style.display = 'none'; patientHeader.style.display = 'none'; backBtn.style.display = 'none'; // Mostrar painel admin document.getElementById('adminClinicsState').style.display = 'block'; // Atualizar menu document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active')); document.getElementById('navAdminClinics').classList.add('active'); loadAdminClinics(); } // Override para voltar aos pacientes e esconder o admin state const originalShowPatientsView = showPatientsView; showPatientsView = function() { document.getElementById('adminClinicsState').style.display = 'none'; document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active')); document.querySelector('a[href="/"]').classList.add('active'); originalShowPatientsView(); }; window.showPatientsView = showPatientsView; async function loadAdminClinics() { const tbody = document.getElementById('clinicsTableBody'); tbody.innerHTML = 'Carregando...'; try { const res = await fetchWithAuth(`${API_URL}/auth/users`); if (!res.ok) throw new Error('Erro ao carregar clínicas'); const data = await res.json(); if (data.users.length === 0) { tbody.innerHTML = 'Nenhuma clínica cadastrada.'; return; } tbody.innerHTML = data.users.map(u => ` #${u.id} ${escapeHtml(u.username)}${u.is_admin ? ' Admin' : ''} ${escapeHtml(u.clinic_name || '—')} ${escapeHtml(u.email)} ${escapeHtml(u.pc_name || '—')} ${u.id !== currentUser.id ? `` : ''} `).join(''); } catch (e) { tbody.innerHTML = 'Erro ao carregar dados.'; } } function openCreateClinicModal() { document.getElementById('createClinicModal').style.display = 'block'; } function closeCreateClinicModal() { document.getElementById('createClinicModal').style.display = 'none'; document.getElementById('createClinicForm').reset(); } async function submitCreateClinic(event) { event.preventDefault(); const btn = event.target.querySelector('button[type="submit"]'); const originalText = btn.innerHTML; btn.innerHTML = 'Salvando...'; btn.disabled = true; const payload = { username: document.getElementById('clinicUsername').value.trim(), email: document.getElementById('clinicEmail').value.trim(), password: document.getElementById('clinicPassword').value, clinic_name: document.getElementById('clinicName').value.trim(), pc_name: document.getElementById('clinicPcName').value.trim() }; try { const res = await fetchWithAuth(`${API_URL}/auth/create-user`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Erro ao criar'); showToast('Clínica cadastrada com sucesso!', 'success'); closeCreateClinicModal(); loadAdminClinics(); } catch (e) { alert(e.message); } finally { btn.innerHTML = originalText; btn.disabled = false; } } async function deleteClinic(id, username) { if (!confirm(`Tem certeza que deseja excluir permanentemente o acesso da clínica "${username}"?`)) return; try { const res = await fetchWithAuth(`${API_URL}/auth/users/${id}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Erro ao excluir'); showToast('Clínica excluída', 'success'); loadAdminClinics(); } catch (e) { showToast(e.message, 'error'); } } async function generateClinicToken(id) { try { const res = await fetchWithAuth(`${API_URL}/auth/users/${id}/token`, { method: 'POST' }); if (!res.ok) throw new Error('Erro ao gerar token'); const data = await res.json(); document.getElementById('clinicTokenInput').value = data.token; document.getElementById('tokenCopySuccess').style.display = 'none'; document.getElementById('showTokenModal').style.display = 'block'; } catch (e) { showToast(e.message, 'error'); } } function closeShowTokenModal() { document.getElementById('showTokenModal').style.display = 'none'; } function copyTokenToClipboard() { const input = document.getElementById('clinicTokenInput'); input.select(); input.setSelectionRange(0, 99999); navigator.clipboard.writeText(input.value).then(() => { document.getElementById('tokenCopySuccess').style.display = 'block'; }); } // Admin Exports window.showAdminClinics = showAdminClinics; window.openCreateClinicModal = openCreateClinicModal; window.closeCreateClinicModal = closeCreateClinicModal; window.submitCreateClinic = submitCreateClinic; window.deleteClinic = deleteClinic; window.generateClinicToken = generateClinicToken; window.closeShowTokenModal = closeShowTokenModal; window.copyTokenToClipboard = copyTokenToClipboard; // A aplicação é inicializada no evento DOMContentLoaded acima