// ================================================================
// 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 = '📋 Todos os Clientes ';
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}
`;
}).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)}
${isDerived ? '🔍 Detalhes' : '🔄 Orientar'}
${image.enabled ? '🚫' : '✅'}
${!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
ℹ️ 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 = `
`;
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) => `
`).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 = '';
gtos.forEach((gto, idx) => {
const bg = idx % 2 === 0 ? '#fff' : '#f9f9f9';
html += `
${escapeHtml(gto.gto_number)}
${escapeHtml(gto.description || 'Sem descrição')}
📅 ${formatDate(gto.created_at)}
✅ Vincular
`;
});
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