From 5324a830b28ce48d1e0a4967a6f8f9c2da1693b4 Mon Sep 17 00:00:00 2001 From: VPS 4 Builder Date: Tue, 26 May 2026 05:26:37 +0200 Subject: [PATCH] Restore RX modal UI and sidebar dropdown --- dental-server/public/app.js | 848 ++++++++++++++++++++++++++------ dental-server/public/index.html | 234 ++++++--- dental-server/public/style.css | 237 +++++++++ 3 files changed, 1100 insertions(+), 219 deletions(-) diff --git a/dental-server/public/app.js b/dental-server/public/app.js index a999006..29f2876 100644 --- a/dental-server/public/app.js +++ b/dental-server/public/app.js @@ -34,18 +34,11 @@ function showErrorBanner(msg, detail = '') { setTimeout(() => banner && banner.remove(), 8000); } -function logout() { - localStorage.removeItem('auth_token'); - localStorage.removeItem('token'); - window.location.href = '/login'; -} - function getAuthToken() { return localStorage.getItem('auth_token'); } let authVerificationInProgress = false; -let currentUser = null; async function verifyAuth() { if (authVerificationInProgress) return false; @@ -59,14 +52,7 @@ async function verifyAuth() { 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; - } + if (data.valid) return true; localStorage.removeItem('auth_token'); window.location.href = '/login'; return false; } catch { localStorage.removeItem('auth_token'); @@ -112,6 +98,7 @@ let clientsList = []; let selectedClient = ''; let showDisabled = false; let currentTransformations = []; +let fineTuneAngle = 0; // ================================================================ // DOM REFS @@ -163,6 +150,7 @@ document.addEventListener('DOMContentLoaded', async () => { await loadClientsList(); loadPatients(); setupEventListeners(); + initDragAndDrop(); setInterval(loadClientsList, 5000); } catch (error) { _log.error('INIT', 'Erro fatal na inicialização:', error.message, error); @@ -243,8 +231,12 @@ function updateClientsDropdown() { const opt = document.createElement('option'); opt.value = c.name; let lbl = c.name; - if (c.status === 'identified') lbl += ' ✅'; - else if (c.status === 'historic') lbl += ' 📚'; + 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); }); @@ -355,6 +347,7 @@ function renderPatientCards() { return `
+
${count} imagem${count !== 1 ? 'ns' : ''}
@@ -379,7 +372,7 @@ function renderPatientCards() { 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!'); } @@ -388,6 +381,38 @@ function renderPatientCards() { }); } +// 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 // ================================================================ @@ -449,9 +474,11 @@ async function loadPatientImages(patientName) { } function renderPatientImages() { - imagesGrid.innerHTML = patientImages.map(image => ` + imagesGrid.innerHTML = patientImages.map(image => { + const thumbUrl = image.thumb_filename ? `/uploads/${image.thumb_filename}` : `/uploads/${image.filename}`; + return `
-
+
📅 ${formatDate(image.created_at)}
${escapeHtml(image.image_guid || image.filename)}
@@ -465,8 +492,8 @@ function renderPatientImages() {
${!image.enabled ? '
Desabilitada
' : ''} -
- `).join(''); +
`; + }).join(''); } // ================================================================ @@ -520,19 +547,28 @@ async function showTransformModal(imageId) { `; 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 image = patientImages.find(img => img.id === imageId); + if (!image) { + throw new Error('Imagem não encontrada localmente'); } - const transformations = await res.json(); + // Se existir thumb_filename, usar ele; caso contrário, fallback para a imagem cheia + const imageUrl = image.thumb_filename ? `/uploads/${image.thumb_filename}` : `/uploads/${image.filename}`; + + const transformations = [ + { name: 'Original', rotation: 0, flipH: false, flipV: false, css: 'transform: none;' }, + { name: 'Flip Horizontal', rotation: 0, flipH: true, flipV: false, css: 'transform: scaleX(-1);' }, + { name: 'Flip Vertical', rotation: 0, flipH: false, flipV: true, css: 'transform: scaleY(-1);' }, + { name: 'Rotação +90°', rotation: 90, flipH: false, flipV: false, css: 'transform: rotate(90deg);' }, + { name: 'Rotação -90°', rotation: -90, flipH: false, flipV: false, css: 'transform: rotate(-90deg);' } + ]; + currentTransformations = transformations; const container = document.getElementById('transformOptions'); container.innerHTML = transformations.map((t, i) => `
- ${escapeHtml(t.name)} + ${escapeHtml(t.name)}
${escapeHtml(t.name)}
`).join(''); @@ -540,23 +576,21 @@ async function showTransformModal(imageId) { // --------------------------------------------------------------- // 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; - } + 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 gerar variações', 'error'); + showToast('Erro ao carregar variações', 'error'); transformModal.classList.remove('active'); } } @@ -585,6 +619,10 @@ function selectTransform(index) { el.classList.add('selected'); + // Reset fine tune angle when selecting a new transform + fineTuneAngle = 0; + updateFineTuneDisplay(); + // Show action area document.getElementById('transformActionArea').style.display = 'block'; @@ -599,11 +637,26 @@ function clearTransformSelection() { const container = document.getElementById('transformOptions'); container.classList.remove('selection-mode'); + // 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 = ''; } @@ -626,7 +679,7 @@ async function saveSelectedTransform() { try { const payload = { - rotation: transform.rotation, + rotation: transform.rotation + fineTuneAngle, flipH: transform.flipH, flipV: transform.flipV, remark: remark @@ -635,7 +688,7 @@ async function saveSelectedTransform() { 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 }) + body: JSON.stringify(payload) }); if (!res.ok) throw new Error('Erro ao salvar'); @@ -648,6 +701,49 @@ async function saveSelectedTransform() { } } +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' }); @@ -1060,166 +1156,600 @@ window.closeSettingsModal = closeSettingsModal; window.updateCredentials = updateCredentials; // ================================================================ -// GERENCIAMENTO DE CLÍNICAS (ADMIN) +// CONFIGURAÇÕES DE PLUGINS (Wasabi Storage) // ================================================================ -function showAdminClinics() { - view = 'admin-clinics'; - document.querySelector('.content-scroll').scrollTop = 0; +function openPluginsModal() { + const modal = document.getElementById('pluginsModal'); + if (modal) modal.style.display = 'block'; - // Esconder outras visões - imagesGrid.style.display = 'none'; - emptyState.style.display = 'none'; - patientHeader.style.display = 'none'; - backBtn.style.display = 'none'; + // Limpar campos + document.getElementById('wasabiAccessKey').value = ''; + document.getElementById('wasabiSecretKey').value = ''; + document.getElementById('wasabiBucket').value = ''; + document.getElementById('wasabiRegion').value = ''; + document.getElementById('wasabiEndpoint').value = ''; - // 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(); + const alertBox = document.getElementById('pluginsStatusAlert'); + alertBox.style.display = 'none'; + + // Carregar configurações atuais + loadPluginsConfig(); } -// 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; +function closePluginsModal() { + const modal = document.getElementById('pluginsModal'); + if (modal) modal.style.display = 'none'; +} -async function loadAdminClinics() { - const tbody = document.getElementById('clinicsTableBody'); - tbody.innerHTML = 'Carregando...'; - +async function loadPluginsConfig() { 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(); + const res = await fetchWithAuth('/api/system/storage-config'); + if (!res.ok) throw new Error('Falha ao carregar configurações'); - if (data.users.length === 0) { - tbody.innerHTML = 'Nenhuma clínica cadastrada.'; - return; + 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).'; } - - tbody.innerHTML = data.users.map(u => ` - - #${u.id} - ${escapeHtml(u.username)}${u.is_admin ? ' Admin' : ''} - ${escapeHtml(u.clinic_name || '—')} - ${escapeHtml(u.email)} - ${escapeHtml(u.pc_name || '—')} - - - ${u.id !== currentUser.id ? `` : ''} - - - `).join(''); + alertBox.style.display = 'block'; } catch (e) { - tbody.innerHTML = 'Erro ao carregar dados.'; + console.error(e); + showToast('Erro ao carregar configurações do Wasabi.', 'error'); } } -function openCreateClinicModal() { - document.getElementById('createClinicModal').style.display = 'block'; -} - -function closeCreateClinicModal() { - document.getElementById('createClinicModal').style.display = 'none'; - document.getElementById('createClinicForm').reset(); -} - -async function submitCreateClinic(event) { +async function updatePluginsConfig(event) { event.preventDefault(); - const btn = event.target.querySelector('button[type="submit"]'); + 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.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`, { + const res = await fetchWithAuth('/api/system/storage-config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) + body: JSON.stringify({ + wasabiAccessKey, + wasabiSecretKey, + wasabiBucket, + wasabiRegion, + wasabiEndpoint + }) }); - const data = await res.json(); - if (!res.ok) throw new Error(data.error || 'Erro ao criar'); + 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); - showToast('Clínica cadastrada com sucesso!', 'success'); - closeCreateClinicModal(); - loadAdminClinics(); } catch (e) { - alert(e.message); + console.error(e); + showToast(e.message, 'error'); } 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; +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_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 res = await fetchWithAuth('/api/socket/status'); + if (!res.ok) throw new Error('Falha ao obter status dos dispositivos.'); const data = await res.json(); - document.getElementById('clinicTokenInput').value = data.token; - document.getElementById('tokenCopySuccess').style.display = 'none'; - document.getElementById('showTokenModal').style.display = 'block'; + 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) { - showToast(e.message, 'error'); + 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; } } -function closeShowTokenModal() { - document.getElementById('showTokenModal').style.display = 'none'; +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; + } } -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'; +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); }); } -// 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; +function handleUploadFiles(files) { + if (!files || files.length === 0) return; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + + if (!file.type.match('image.*')) { + showToast(`O arquivo "${file.name}" não é uma imagem válida!`, 'warning'); + continue; + } + + if (file.size > 10 * 1024 * 1024) { + showToast(`A imagem "${file.name}" excede o limite de tamanho de 10MB!`, 'warning'); + continue; + } + + if (selectedUploadFiles.some(f => f.name === file.name && f.size === file.size)) { + continue; + } + + selectedUploadFiles.push(file); + } + + renderUploadPreviews(); +} +function renderUploadPreviews() { + const container = document.getElementById('uploadPreviewsContainer'); + const btnSubmit = document.getElementById('btnSubmitManualUpload'); + if (!container) return; + + container.replaceChildren(); + + selectedUploadFiles.forEach((file, index) => { + const item = document.createElement('div'); + item.className = 'preview-item'; + + const img = document.createElement('img'); + img.alt = file.name; + + const reader = new FileReader(); + reader.onload = e => { + if (e.target && e.target.result) img.src = e.target.result; + }; + reader.readAsDataURL(file); + + const btnRemove = document.createElement('button'); + btnRemove.type = 'button'; + btnRemove.className = 'preview-remove'; + btnRemove.innerHTML = '×'; + btnRemove.title = 'Remover Imagem'; + btnRemove.onclick = (e) => { + e.stopPropagation(); + removeUploadPreview(index); + }; + + item.appendChild(img); + item.appendChild(btnRemove); + container.appendChild(item); + }); + + if (btnSubmit) { + btnSubmit.disabled = selectedUploadFiles.length === 0; + } +} + +function removeUploadPreview(index) { + selectedUploadFiles.splice(index, 1); + renderUploadPreviews(); +} + +function readFileAsBase64(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = error => reject(error); + reader.readAsDataURL(file); + }); +} + +async function handleManualUploadSubmit(event) { + event.preventDefault(); + + if (selectedUploadFiles.length === 0) { + showToast('Selecione pelo menos uma imagem para enviar.', 'warning'); + return; + } + + const select = document.getElementById('uploadPatientSelect'); + const remarkInput = document.getElementById('uploadRemark'); + const btnSubmit = document.getElementById('btnSubmitManualUpload'); + + if (!select || !btnSubmit) return; + + let patientName = ''; + let doctor = ''; + + if (select.value === 'NEW_PATIENT') { + const firstName = document.getElementById('uploadNewPatientFirstName').value.trim(); + const lastName = document.getElementById('uploadNewPatientLastName').value.trim(); + doctor = document.getElementById('uploadNewPatientDoctor').value.trim(); + + if (!firstName || !lastName) { + showToast('Nome e sobrenome são obrigatórios para novo paciente.', 'warning'); + return; + } + + patientName = `${firstName} ${lastName}`; + } else { + patientName = select.value; + } + + const remark = remarkInput ? remarkInput.value.trim() : ''; + + const progressContainer = document.getElementById('uploadProgressContainer'); + const progressText = document.getElementById('uploadProgressText'); + const progressBarFill = document.getElementById('uploadProgressBarFill'); + const progressPercent = document.getElementById('uploadProgressPercent'); + + if (progressContainer) progressContainer.style.display = 'block'; + btnSubmit.disabled = true; + + const totalFiles = selectedUploadFiles.length; + let successCount = 0; + + for (let i = 0; i < totalFiles; i++) { + const file = selectedUploadFiles[i]; + + if (progressText) progressText.textContent = `Enviando imagem ${i + 1} de ${totalFiles}...`; + if (progressBarFill) { + const pct = Math.round((i / totalFiles) * 100); + progressBarFill.style.width = `${pct}%`; + if (progressPercent) progressPercent.textContent = `${pct}%`; + } + + try { + const base64 = await readFileAsBase64(file); + + const response = await fetchWithAuth('/api/images/upload', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + imageBase64: base64, + filename: file.name, + patientName: patientName, + doctor: doctor || null, + remark: remark || null + }) + }); + + if (!response.ok) { + const err = await response.json(); + throw new Error(err.error || `Erro HTTP ${response.status}`); + } + + successCount++; + } catch (e) { + console.error(`Erro ao enviar arquivo ${file.name}:`, e); + showToast(`Falha ao enviar a imagem ${file.name}: ${e.message}`, 'error'); + } + } + + if (progressBarFill) { + progressBarFill.style.width = '100%'; + if (progressPercent) progressPercent.textContent = '100%'; + } + if (progressText) progressText.textContent = `Finalizado! ${successCount} de ${totalFiles} imagens enviadas.`; + + setTimeout(async () => { + closeSyncModal(); + showToast(`✅ ${successCount} imagem(ns) enviada(s) com sucesso!`, 'success'); + + await loadPatients(); + if (window.loadClientsList) { + await window.loadClientsList(); + } + }, 1000); +} + +// Vinculando ao window +window.openSyncModal = openSyncModal; +window.closeSyncModal = closeSyncModal; +window.switchSyncTab = switchSyncTab; +window.triggerSyncFromModal = triggerSyncFromModal; +window.refreshUploadPatients = refreshUploadPatients; +window.toggleNewPatientFields = toggleNewPatientFields; +window.handleManualUploadSubmit = handleManualUploadSubmit; +window.initDragAndDrop = initDragAndDrop; +window.deletePatientImages = deletePatientImages; +window.adjustFineTune = adjustFineTune; +window.resetFineTune = resetFineTune; // A aplicação é inicializada no evento DOMContentLoaded acima +function logout() { localStorage.removeItem('token'); window.location.href = '/login.html'; } diff --git a/dental-server/public/index.html b/dental-server/public/index.html index d78378c..3c5ac8d 100644 --- a/dental-server/public/index.html +++ b/dental-server/public/index.html @@ -25,19 +25,30 @@ 🖥️ Dispositivos - ⚙️ Configurações + + 🔌 Plugins / Wasabi + + + 🔄 Sincronização + ⚠️ Zerar Sistema + + 🏢 Gerenciar Clínicas + 🚪 Sair +
+ +
@@ -56,9 +67,6 @@

Galeria de Imagens

- As imagens aparecerão aqui quando forem enviadas pelo cliente Windows

- - -
@@ -152,7 +136,27 @@
-