Files
rx.scoreodonto.com/dental-server/public/app.js
T
2026-05-26 02:59:11 +02:00

1220 lines
47 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ================================================================
// 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 = `<b>⚠️ ${msg}</b>${detail ? `<br><small>${detail}</small>` : ''}`;
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 = '<option value="">📋 Todos os Clientes</option>';
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 `
<div class="image-card patient-card" onclick="openPatientByIndex(${index})">
<div class="image-preview" style="${thumb ? `background-image: url('${thumb}'); background-size: cover; background-position: center;` : 'background: linear-gradient(135deg,#667eea22,#764ba222);'}">
<div class="patient-count-badge">${count} imagem${count !== 1 ? 'ns' : ''}</div>
</div>
<div class="image-info">
<div class="image-patient-name" title="${escapeHtml(fullName)}">${escapeHtml(fullName)}</div>
<div class="image-meta"><span>📅 ${date}</span></div>
<div class="image-doctor-remark">
<div title="${escapeHtml(doctor)}">🩺 <b>Dentista:</b> ${escapeHtml(doctor)}</div>
<div title="${escapeHtml(remark)}">📝 <b>Obs:</b> ${escapeHtml(remark)}</div>
</div>
</div>
</div>`;
}).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 => `
<div class="image-card ${!image.enabled ? 'disabled' : ''}" data-id="${image.id}" onclick="handleImageClick(${image.id})">
<div class="image-preview" style="background-image: url('/uploads/${image.filename}'); background-size: cover; background-position: center;"></div>
<div class="image-info">
<div class="image-meta"><span>📅 ${formatDate(image.created_at)}</span></div>
<div class="image-guid">${escapeHtml(image.image_guid || image.filename)}</div>
<div class="image-actions">
<button class="btn btn-primary btn-small" style="flex:1" onclick="handleImageClick(${image.id}); event.stopPropagation();">
🔄 Orientar
</button>
<button class="btn btn-small" style="flex:1; background:${image.enabled ? 'rgba(255,107,107,0.12)' : 'rgba(81,207,102,0.12)'}; color:${image.enabled ? '#c0392b' : '#27ae60'}; border:1px solid ${image.enabled ? 'rgba(255,107,107,0.3)' : 'rgba(81,207,102,0.3)'};" onclick="toggleImageEnabled(${image.id}); event.stopPropagation();">
${image.enabled ? '🚫' : '✅'}
</button>
</div>
</div>
${!image.enabled ? '<div class="image-badge">Desabilitada</div>' : ''}
</div>
`).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 = `
<div style="margin-bottom: 5px;"><strong>👨‍⚕️ Dentista:</strong> ${escapeHtml(image.doctor || 'Não informado')}</div>
<div><strong>📝 Obs:</strong> ${escapeHtml(image.remark || 'Nenhuma observação')}</div>
`;
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 = `
<div style="grid-column:1/-1; text-align:center; padding:40px 0; color:#888;">
<div class="spinner" style="margin:0 auto 16px;"></div>
<p>Gerando variações...</p>
</div>`;
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) => `
<div class="transform-option" data-index="${i}" onclick="selectTransform(${i})">
<img src="${t.base64}" alt="${escapeHtml(t.name)}" loading="lazy">
<div class="transform-name">${escapeHtml(t.name)}</div>
</div>
`).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 = '<div style="padding:20px; text-align:center; color:#888;">Carregando GTOs...</div>';
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 = '<div style="padding:20px; text-align:center; color:#888;">Nenhuma GTO cadastrada para este paciente.</div>';
return;
}
let html = '<ul style="list-style:none; padding:0; margin:0;">';
gtos.forEach((gto, idx) => {
const bg = idx % 2 === 0 ? '#fff' : '#f9f9f9';
html += `
<li style="display:flex; justify-content:space-between; align-items:center; padding:15px; background:${bg}; border-bottom:1px solid #eee;">
<div>
<div style="font-weight:600; font-size:1.05rem;">${escapeHtml(gto.gto_number)}</div>
<div style="color:#666; font-size:0.9rem;">${escapeHtml(gto.description || 'Sem descrição')}</div>
<div style="color:#999; font-size:0.8rem; margin-top:4px;">📅 ${formatDate(gto.created_at)}</div>
</div>
<div>
<button class="btn btn-primary btn-small" onclick="linkImageToGto(${gto.id}, this)">✅ Vincular</button>
</div>
</li>
`;
});
html += '</ul>';
container.innerHTML = html;
} catch (e) {
console.error(e);
container.innerHTML = '<div style="padding:20px; text-align:center; color:red;">Erro ao carregar GTOs</div>';
}
}
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 = '<div class="spinner" style="width: 16px; height: 16px; border-width: 2px; border-top-color: white;"></div>';
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 = '<span class="spinner" style="width:14px;height:14px;display:inline-block;"></span> 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 = '<tr><td colspan="6" style="text-align: center; padding: 20px;">Carregando...</td></tr>';
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 = '<tr><td colspan="6" style="text-align: center; padding: 20px;">Nenhuma clínica cadastrada.</td></tr>';
return;
}
tbody.innerHTML = data.users.map(u => `
<tr style="border-bottom: 1px solid #eee;">
<td style="padding: 12px 15px;">#${u.id}</td>
<td style="padding: 12px 15px;"><strong>${escapeHtml(u.username)}</strong>${u.is_admin ? ' <span style="background: #e3f2fd; color: #1976d2; padding: 2px 6px; border-radius: 4px; font-size: 11px;">Admin</span>' : ''}</td>
<td style="padding: 12px 15px;">${escapeHtml(u.clinic_name || '—')}</td>
<td style="padding: 12px 15px;">${escapeHtml(u.email)}</td>
<td style="padding: 12px 15px;">${escapeHtml(u.pc_name || '—')}</td>
<td style="padding: 12px 15px; text-align: center;">
<button class="btn btn-primary btn-small" onclick="generateClinicToken(${u.id})" title="Gerar Token">🔑 Token</button>
${u.id !== currentUser.id ? `<button class="btn btn-danger btn-small" onclick="deleteClinic(${u.id}, '${escapeHtml(u.username)}')" title="Excluir">🗑️</button>` : ''}
</td>
</tr>
`).join('');
} catch (e) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; padding: 20px; color: red;">Erro ao carregar dados.</td></tr>';
}
}
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