diff --git a/dental-server/public/admin-clinics.html b/dental-server/public/admin-clinics.html index a0f0450..aecfdef 100644 --- a/dental-server/public/admin-clinics.html +++ b/dental-server/public/admin-clinics.html @@ -14,7 +14,7 @@ - + @@ -196,6 +196,6 @@
Notificação
- + diff --git a/dental-server/public/public/admin-clinics.css b/dental-server/public/public/admin-clinics.css deleted file mode 100644 index ea13657..0000000 --- a/dental-server/public/public/admin-clinics.css +++ /dev/null @@ -1,616 +0,0 @@ -:root { - --primary: #4361ee; - --primary-hover: #3a56d4; - --primary-light: rgba(67, 97, 238, 0.1); - --success: #2ec4b6; - --success-light: rgba(46, 196, 182, 0.15); - --danger: #ef476f; - --danger-light: rgba(239, 71, 111, 0.15); - --warning: #ff9f1c; - --dark: #0b132b; - --gray-900: #1c2541; - --gray-800: #2b3a67; - --gray-200: #e2e8f0; - --light: #f8f9fa; - --white: #ffffff; - - --glass-bg: rgba(255, 255, 255, 0.05); - --glass-border: rgba(255, 255, 255, 0.1); - --glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3); - - --font-heading: 'Outfit', sans-serif; - --font-body: 'Inter', sans-serif; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: var(--font-body); - background: linear-gradient(135deg, var(--dark) 0%, var(--gray-900) 100%); - color: var(--light); - min-height: 100vh; - display: flex; - overflow-x: hidden; -} - -/* Glassmorphism Classes */ -.glass-card { - background: var(--glass-bg); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid var(--glass-border); - border-radius: 16px; - box-shadow: var(--glass-shadow); -} - -/* Animations */ -@keyframes pulse { - 0% { box-shadow: 0 0 0 0 rgba(67, 97, 238, 0.4); } - 70% { box-shadow: 0 0 0 10px rgba(67, 97, 238, 0); } - 100% { box-shadow: 0 0 0 0 rgba(67, 97, 238, 0); } -} - -.pulse-animation { - animation: pulse 2s infinite; -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } -} - -/* Sidebar */ -.sidebar { - width: 260px; - background: rgba(11, 19, 43, 0.8); - border-right: 1px solid var(--glass-border); - display: flex; - flex-direction: column; - padding: 24px 0; - z-index: 10; -} - -.sidebar-header { - padding: 0 24px 32px; - display: flex; - align-items: center; - gap: 12px; -} - -.logo-icon { - width: 40px; - height: 40px; - background: linear-gradient(135deg, #4cc9f0, var(--primary)); - border-radius: 10px; - display: flex; - align-items: center; - justify-content: center; - font-size: 20px; - color: white; -} - -.sidebar-header h2 { - font-family: var(--font-heading); - font-size: 22px; - font-weight: 600; - letter-spacing: 0.5px; -} - -.sidebar-nav { - display: flex; - flex-direction: column; - gap: 8px; - padding: 0 16px; - flex: 1; -} - -.nav-item { - padding: 12px 16px; - border-radius: 10px; - color: #a0aec0; - text-decoration: none; - font-weight: 500; - display: flex; - align-items: center; - gap: 12px; - transition: all 0.3s ease; -} - -.nav-item i { - font-size: 18px; - width: 20px; - text-align: center; -} - -.nav-item:hover { - background: var(--glass-bg); - color: var(--white); - transform: translateX(4px); -} - -.nav-item.active { - background: var(--primary-light); - color: #4cc9f0; - border-left: 3px solid #4cc9f0; -} - -.sidebar-footer { - padding: 0 24px; -} - -.logout-btn { - width: 100%; - padding: 12px; - background: transparent; - border: 1px solid var(--glass-border); - color: #a0aec0; - border-radius: 10px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - font-family: var(--font-body); - font-weight: 500; - transition: all 0.3s; -} - -.logout-btn:hover { - background: var(--danger-light); - color: var(--danger); - border-color: var(--danger); -} - -/* Main Content */ -.main-content { - flex: 1; - padding: 32px 40px; - overflow-y: auto; -} - -.top-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 40px; - animation: fadeIn 0.5s ease-out; -} - -.header-title h1 { - font-family: var(--font-heading); - font-size: 32px; - margin-bottom: 8px; -} - -.header-title p { - color: #a0aec0; - font-size: 15px; -} - -/* Buttons */ -.primary-btn { - background: var(--primary); - color: white; - border: none; - padding: 12px 24px; - border-radius: 10px; - font-family: var(--font-body); - font-weight: 600; - font-size: 15px; - cursor: pointer; - display: flex; - align-items: center; - gap: 8px; - transition: all 0.3s ease; - box-shadow: 0 4px 15px rgba(67, 97, 238, 0.3); -} - -.primary-btn:hover { - background: var(--primary-hover); - transform: translateY(-2px); - box-shadow: 0 6px 20px rgba(67, 97, 238, 0.4); -} - -.secondary-btn { - background: transparent; - color: var(--white); - border: 1px solid var(--glass-border); - padding: 12px 24px; - border-radius: 10px; - font-family: var(--font-body); - font-weight: 500; - cursor: pointer; - transition: all 0.3s ease; -} - -.secondary-btn:hover { - background: var(--glass-bg); -} - -.icon-btn { - background: transparent; - border: none; - color: #a0aec0; - cursor: pointer; - padding: 6px; - border-radius: 6px; - transition: all 0.2s; -} - -.icon-btn:hover { - color: var(--white); - background: var(--glass-bg); -} - -.icon-btn.delete:hover { - color: var(--danger); - background: var(--danger-light); -} - -/* Stats */ -.dashboard-stats { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 24px; - margin-bottom: 40px; - animation: fadeIn 0.6s ease-out; -} - -.stat-card { - padding: 24px; - display: flex; - align-items: center; - gap: 20px; - transition: transform 0.3s ease; -} - -.stat-card:hover { - transform: translateY(-5px); -} - -.stat-icon { - width: 60px; - height: 60px; - border-radius: 16px; - display: flex; - align-items: center; - justify-content: center; - font-size: 24px; -} - -.bg-blue { background: rgba(67, 97, 238, 0.15); color: #4cc9f0; } -.bg-green { background: var(--success-light); color: var(--success); } -.bg-red { background: var(--danger-light); color: var(--danger); } - -.stat-info h3 { - font-size: 14px; - color: #a0aec0; - font-weight: 500; - margin-bottom: 4px; -} - -.stat-info p { - font-family: var(--font-heading); - font-size: 28px; - font-weight: 700; -} - -/* Table */ -.data-section { - padding: 24px; - animation: fadeIn 0.7s ease-out; -} - -.table-container { - overflow-x: auto; -} - -.modern-table { - width: 100%; - border-collapse: separate; - border-spacing: 0 8px; -} - -.modern-table th { - text-align: left; - padding: 0 16px 12px; - color: #a0aec0; - font-weight: 500; - font-size: 13px; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.modern-table td { - padding: 16px; - background: rgba(255, 255, 255, 0.02); -} - -.modern-table tr td:first-child { - border-radius: 10px 0 0 10px; -} - -.modern-table tr td:last-child { - border-radius: 0 10px 10px 0; -} - -.modern-table tbody tr { - transition: all 0.2s; -} - -.modern-table tbody tr:hover td { - background: rgba(255, 255, 255, 0.05); -} - -/* Status Badge */ -.status-badge { - padding: 6px 12px; - border-radius: 20px; - font-size: 12px; - font-weight: 600; - display: inline-flex; - align-items: center; - gap: 6px; -} - -.status-active { - background: var(--success-light); - color: var(--success); -} - -.status-inactive { - background: var(--danger-light); - color: var(--danger); -} - -.token-blur { - filter: blur(4px); - transition: filter 0.3s; - cursor: pointer; - font-family: monospace; - background: rgba(0,0,0,0.2); - padding: 4px 8px; - border-radius: 4px; -} - -.token-blur:hover { - filter: blur(0); -} - -/* Modals */ -.modal-overlay { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background: rgba(0, 0, 0, 0.6); - backdrop-filter: blur(5px); - display: flex; - align-items: center; - justify-content: center; - z-index: 100; - opacity: 0; - visibility: hidden; - transition: all 0.3s ease; -} - -.modal-overlay.active { - opacity: 1; - visibility: visible; -} - -.modal-content { - width: 100%; - max-width: 500px; - padding: 32px; - transform: scale(0.9); - transition: all 0.3s ease; -} - -.modal-overlay.active .modal-content { - transform: scale(1); -} - -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 24px; -} - -.modal-header h2 { - font-family: var(--font-heading); -} - -/* Forms */ -.modern-form .form-group { - margin-bottom: 20px; -} - -.modern-form label { - display: block; - margin-bottom: 8px; - font-size: 14px; - color: #a0aec0; -} - -.input-with-icon { - position: relative; - display: flex; - align-items: center; -} - -.input-with-icon i { - position: absolute; - left: 16px; - color: #a0aec0; -} - -.input-with-icon input { - width: 100%; - background: rgba(0, 0, 0, 0.2); - border: 1px solid var(--glass-border); - color: white; - padding: 14px 16px 14px 45px; - border-radius: 10px; - font-family: var(--font-body); - font-size: 15px; - transition: all 0.3s; -} - -.input-with-icon input:focus { - outline: none; - border-color: var(--primary); - background: rgba(0, 0, 0, 0.4); -} - -.form-group small { - display: block; - margin-top: 6px; - color: #718096; - font-size: 12px; -} - -.form-actions { - display: flex; - justify-content: flex-end; - gap: 16px; - margin-top: 32px; -} - -/* Success Modal */ -.success-modal { - text-align: center; -} - -.success-icon { - font-size: 64px; - color: var(--success); - margin-bottom: 16px; -} - -.success-modal h2 { - margin-bottom: 12px; -} - -.success-modal p { - color: #a0aec0; - margin-bottom: 24px; - font-size: 14px; - line-height: 1.5; -} - -.token-container { - background: rgba(0, 0, 0, 0.3); - padding: 16px; - border-radius: 10px; - display: flex; - align-items: center; - justify-content: space-between; - border: 1px solid var(--primary-light); -} - -.token-container code { - font-family: monospace; - font-size: 16px; - color: #4cc9f0; - word-break: break-all; -} - -/* Toast */ -.toast { - position: fixed; - bottom: 24px; - right: 24px; - padding: 16px 24px; - background: var(--glass-bg); - backdrop-filter: blur(12px); - border-left: 4px solid var(--primary); - color: white; - border-radius: 8px; - box-shadow: 0 10px 30px rgba(0,0,0,0.3); - transform: translateX(120%); - transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55); - z-index: 1000; -} - -.toast.show { - transform: translateX(0); -} - -.toast.error { - border-left-color: var(--danger); -} - -.toast.success { - border-left-color: var(--success); -} - -.loading-cell { - text-align: center; - padding: 40px !important; - color: #a0aec0; -} - -.spinner { - width: 30px; - height: 30px; - border: 3px solid rgba(255,255,255,0.1); - border-top-color: var(--primary); - border-radius: 50%; - animation: spin 1s linear infinite; - margin: 0 auto 16px; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -/* Custom Switch Toggle */ -.toggle-switch { - position: relative; - display: inline-block; - width: 44px; - height: 24px; -} - -.toggle-switch input { - opacity: 0; - width: 0; - height: 0; -} - -.slider { - position: absolute; - cursor: pointer; - top: 0; left: 0; right: 0; bottom: 0; - background-color: rgba(255,255,255,0.1); - transition: .4s; - border-radius: 34px; -} - -.slider:before { - position: absolute; - content: ""; - height: 18px; - width: 18px; - left: 3px; - bottom: 3px; - background-color: #a0aec0; - transition: .4s; - border-radius: 50%; -} - -input:checked + .slider { - background-color: var(--success); -} - -input:checked + .slider:before { - transform: translateX(20px); - background-color: white; -} diff --git a/dental-server/public/public/admin-clinics.html b/dental-server/public/public/admin-clinics.html deleted file mode 100644 index aecfdef..0000000 --- a/dental-server/public/public/admin-clinics.html +++ /dev/null @@ -1,201 +0,0 @@ - - - - - - Gerenciamento de Clínicas - RF Dental - - - - - - - - - - - - - - - - - - -
- -
-
-

Dispositivos e Clínicas

-

Gerencie o acesso do Aplicativo Desktop aos computadores das clínicas.

-
-
- -
-
- -
-
-
- -
-
-

Total de Dispositivos

-

0

-
-
-
-
- -
-
-

Ativos

-

0

-
-
-
-
- -
-
-

Bloqueados

-

0

-
-
-
- -
-
- - - - - - - - - - - - - - - - - - -
StatusClínicaComputadorEmail de AcessoÚltimo IPToken de AutenticaçãoAções
-
-

Carregando dispositivos...

-
-
-
- -
- - - - - - - -
Notificação
- - - - diff --git a/dental-server/public/public/admin-clinics.js b/dental-server/public/public/admin-clinics.js deleted file mode 100644 index 396139d..0000000 --- a/dental-server/public/public/admin-clinics.js +++ /dev/null @@ -1,251 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - // Configurações - const API_URL = '/api/admin/clinics'; - - // Pegar Token JWT do Admin - const token = localStorage.getItem('token') || ''; - - // Se não tem token, redirecionar imediatamente para login - if (!token) { - window.location.href = '/login'; - return; - } - - const fetchOptions = { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - } - }; - - // Elementos UI - const clinicsTableBody = document.getElementById('clinicsTableBody'); - const addClinicBtn = document.getElementById('addClinicBtn'); - const clinicModal = document.getElementById('clinicModal'); - const tokenModal = document.getElementById('tokenModal'); - const closeModals = document.querySelectorAll('.close-modal'); - const closeTokenModal = document.querySelector('.close-token-modal'); - const clinicForm = document.getElementById('clinicForm'); - const generatedTokenElem = document.getElementById('generatedToken'); - const copyTokenBtn = document.getElementById('copyTokenBtn'); - - // Stats - const totalDevicesElem = document.getElementById('totalDevices'); - const activeDevicesElem = document.getElementById('activeDevices'); - const blockedDevicesElem = document.getElementById('blockedDevices'); - - // Inicializar - loadClinics(); - - // ========================================== - // Carregar Clínicas - // ========================================== - async function loadClinics() { - try { - const response = await fetch(API_URL, fetchOptions); - - // Se recebeu 401 ou 403, token expirou — vai para login - if (response.status === 401 || response.status === 403) { - localStorage.removeItem('token'); - window.location.href = '/login'; - return; - } - - // Verificar se a resposta é realmente JSON (evita erro com HTML) - const contentType = response.headers.get('content-type') || ''; - if (!contentType.includes('application/json')) { - throw new Error('Sessão expirada ou servidor indisponível. Faça login novamente.'); - } - - if (!response.ok) { - throw new Error(`Erro HTTP ${response.status}`); - } - - const data = await response.json(); - if (data.success) { - renderClinics(data.clinics); - updateStats(data.clinics); - } else { - showToast(data.message, 'error'); - } - } catch (error) { - console.error(error); - clinicsTableBody.innerHTML = `${error.message || 'Erro ao carregar dados.'}`; - } - } - - // ========================================== - // Renderizar Tabela - // ========================================== - function renderClinics(clinics) { - if (clinics.length === 0) { - clinicsTableBody.innerHTML = `Nenhum dispositivo cadastrado.`; - return; - } - - clinicsTableBody.innerHTML = ''; - clinics.forEach(c => { - const isAct = c.is_active; - const statusClass = isAct ? 'status-active' : 'status-inactive'; - const statusIcon = isAct ? 'fa-check' : 'fa-ban'; - const statusText = isAct ? 'Ativo' : 'Bloqueado'; - - const tr = document.createElement('tr'); - tr.innerHTML = ` - ${statusText} - ${c.clinic_name} - ${c.pc_name || '-'} - ${c.email} - ${c.last_ip || 'Nunca conectou'} - ${c.machine_token.split('-')[0]}... - -
- - -
- - `; - clinicsTableBody.appendChild(tr); - }); - } - - function updateStats(clinics) { - const total = clinics.length; - const active = clinics.filter(c => c.is_active).length; - const blocked = total - active; - - totalDevicesElem.textContent = total; - activeDevicesElem.textContent = active; - blockedDevicesElem.textContent = blocked; - } - - // ========================================== - // Cadastrar Clínica - // ========================================== - clinicForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const submitBtn = document.getElementById('submitBtn'); - submitBtn.disabled = true; - submitBtn.innerHTML = ' Salvando...'; - - const formData = new FormData(clinicForm); - const bodyData = Object.fromEntries(formData.entries()); - - try { - const response = await fetch(API_URL, { - method: 'POST', - headers: fetchOptions.headers, - body: JSON.stringify(bodyData) - }); - - const data = await response.json(); - - if (data.success) { - clinicModal.classList.remove('active'); - clinicForm.reset(); - loadClinics(); - - // Show Token Modal - generatedTokenElem.textContent = data.machine_token; - tokenModal.classList.add('active'); - } else { - showToast(data.message, 'error'); - } - } catch (error) { - showToast('Erro de conexão ao salvar', 'error'); - } finally { - submitBtn.disabled = false; - submitBtn.innerHTML = ' Salvar e Gerar Token'; - } - }); - - // ========================================== - // Toggle Status - // ========================================== - window.toggleStatus = async (id, isActive) => { - try { - const response = await fetch(`${API_URL}/${id}/status`, { - method: 'PUT', - headers: fetchOptions.headers, - body: JSON.stringify({ is_active: isActive }) - }); - const data = await response.json(); - if(data.success) { - showToast(isActive ? 'Dispositivo Ativado' : 'Dispositivo Bloqueado', 'success'); - loadClinics(); - } else { - showToast(data.message, 'error'); - loadClinics(); // revert UI - } - } catch (error) { - showToast('Erro ao alterar status', 'error'); - loadClinics(); - } - }; - - // ========================================== - // Deletar - // ========================================== - window.deleteClinic = async (id) => { - if(!confirm('Tem certeza que deseja excluir este dispositivo permanentemente? O Desktop perderá acesso instantaneamente.')) return; - - try { - const response = await fetch(`${API_URL}/${id}`, { - method: 'DELETE', - headers: fetchOptions.headers - }); - const data = await response.json(); - if(data.success) { - showToast('Dispositivo removido', 'success'); - loadClinics(); - } else { - showToast(data.message, 'error'); - } - } catch (error) { - showToast('Erro ao excluir', 'error'); - } - }; - - // ========================================== - // UI Actions (Modals, Copy, Logout) - // ========================================== - addClinicBtn.addEventListener('click', () => clinicModal.classList.add('active')); - - closeModals.forEach(btn => btn.addEventListener('click', () => { - clinicModal.classList.remove('active'); - })); - - closeTokenModal.addEventListener('click', () => { - tokenModal.classList.remove('active'); - }); - - copyTokenBtn.addEventListener('click', () => { - navigator.clipboard.writeText(generatedTokenElem.textContent).then(() => { - showToast('Token copiado com sucesso!', 'success'); - copyTokenBtn.innerHTML = ''; - setTimeout(() => { - copyTokenBtn.innerHTML = ''; - }, 2000); - }); - }); - - document.getElementById('logoutBtn').addEventListener('click', () => { - localStorage.removeItem('token'); - window.location.href = '/login'; - }); - - function showToast(message, type = 'success') { - const toast = document.getElementById('toast'); - toast.textContent = message; - toast.className = `toast show ${type}`; - - setTimeout(() => { - toast.className = 'toast'; - }, 3000); - } -}); diff --git a/dental-server/public/public/app.js b/dental-server/public/public/app.js deleted file mode 100644 index 29f2876..0000000 --- a/dental-server/public/public/app.js +++ /dev/null @@ -1,1755 +0,0 @@ -// ================================================================ -// DENTAL IMAGE MANAGER - Main JavaScript -// Two-Level Navigation: Patients → Images → Transform -// ================================================================ - -const API_URL = '/api'; - -// ================================================================ -// ERROR LOGGER — Logs to console AND shows on screen -// ================================================================ -const _log = { - info: (tag, ...args) => console.log(`%c[${tag}]`, 'color:#667eea;font-weight:bold', ...args), - warn: (tag, ...args) => console.warn(`%c[${tag}]`, 'color:#ffa500;font-weight:bold', ...args), - error: (tag, ...args) => console.error(`%c[${tag}]`, 'color:#ff4444;font-weight:bold', ...args), - group: (tag) => console.group(`%c[${tag}]`, 'color:#667eea;font-weight:bold'), - end: () => console.groupEnd() -}; - -function showErrorBanner(msg, detail = '') { - let banner = document.getElementById('_errBanner'); - if (!banner) { - banner = document.createElement('div'); - banner.id = '_errBanner'; - banner.style.cssText = [ - 'position:fixed','bottom:70px','left:50%','transform:translateX(-50%)', - 'background:#c0392b','color:#fff','padding:12px 20px','border-radius:8px', - 'z-index:9999','font-size:13px','max-width:90vw','text-align:center', - 'box-shadow:0 4px 20px rgba(0,0,0,0.3)','cursor:pointer' - ].join(';'); - banner.onclick = () => banner.remove(); - document.body.appendChild(banner); - } - banner.innerHTML = `⚠️ ${msg}${detail ? `
${detail}` : ''}`; - setTimeout(() => banner && banner.remove(), 8000); -} - -function getAuthToken() { - return localStorage.getItem('auth_token'); -} - -let authVerificationInProgress = false; - -async function verifyAuth() { - if (authVerificationInProgress) return false; - const token = getAuthToken(); - if (!token) { window.location.href = '/login'; return false; } - authVerificationInProgress = true; - try { - const response = await fetch(`${API_URL}/auth/verify`, { - headers: { 'Authorization': `Bearer ${token}` } - }); - if (!response.ok) { localStorage.removeItem('auth_token'); window.location.href = '/login'; return false; } - const data = await response.json(); - authVerificationInProgress = false; - if (data.valid) return true; - localStorage.removeItem('auth_token'); window.location.href = '/login'; return false; - } catch { - localStorage.removeItem('auth_token'); - authVerificationInProgress = false; - window.location.href = '/login'; return false; - } -} - -async function fetchWithAuth(url, options = {}) { - const token = getAuthToken(); - _log.info('FETCH', `${options.method || 'GET'} ${url}`); - const t0 = Date.now(); - try { - const res = await fetch(url, { - ...options, - headers: { ...options.headers, 'Authorization': `Bearer ${token}` } - }); - const ms = Date.now() - t0; - if (!res.ok) { - _log.warn('FETCH', `${options.method || 'GET'} ${url} → ${res.status} (${ms}ms)`); - } else { - _log.info('FETCH', `${options.method || 'GET'} ${url} → ${res.status} OK (${ms}ms)`); - } - return res; - } catch (err) { - const ms = Date.now() - t0; - _log.error('FETCH', `${options.method || 'GET'} ${url} → FALHOU após ${ms}ms`, err.message); - throw err; - } -} - -// ================================================================ -// STATE -// ================================================================ - -let socket = null; -let view = 'patients'; // 'patients' | 'patient-images' -let patients = []; -let patientImages = []; -let selectedPatient = null; // { patient_name, first_name, last_name, ... } -let selectedImageId = null; -let clientsList = []; -let selectedClient = ''; -let showDisabled = false; -let currentTransformations = []; -let fineTuneAngle = 0; - -// ================================================================ -// DOM REFS -// ================================================================ - -const searchInput = document.getElementById('searchInput'); -const clientFilter = document.getElementById('clientFilter'); -const toggleDisabledBtn = document.getElementById('toggleDisabledBtn'); -const toggleText = document.getElementById('toggleText'); -const imagesGrid = document.getElementById('imagesGrid'); -const loadingState = document.getElementById('loadingState'); -const emptyState = document.getElementById('emptyState'); -const transformModal = document.getElementById('transformModal'); -const backBtn = document.getElementById('backBtn'); -const patientHeader = document.getElementById('patientHeader'); -const patientTitle = document.getElementById('patientTitle'); -const patientMeta = document.getElementById('patientMeta'); - -// ================================================================ -// INIT -// ================================================================ - -document.addEventListener('DOMContentLoaded', async () => { - _log.info('INIT', 'DOMContentLoaded — iniciando aplicação'); - try { - const ok = await verifyAuth(); - if (!ok) { _log.warn('AUTH', 'Verificação de auth falhou, redirecionando...'); return; } - _log.info('AUTH', 'Autenticação OK'); - - const token = getAuthToken(); - if (token) { - _log.info('SOCKET', 'Conectando Socket.IO...'); - socket = io({ auth: { token } }); - const debouncedReload = debounce(() => { - if (view === 'patients') loadPatients(); - else loadPatientImages(selectedPatient.patient_name); - }, 1200); - socket.on('new-image', () => { - _log.info('SOCKET', 'Nova imagem recebida via socket'); - showToast('Nova imagem recebida!', 'info'); - debouncedReload(); - }); - socket.on('clients-list-updated', () => loadClientsList()); - socket.on('connect', () => _log.info('SOCKET', '✅ Socket.IO conectado, ID:', socket.id)); - socket.on('disconnect', (r) => _log.warn('SOCKET', '❌ Socket.IO desconectado. Motivo:', r)); - socket.on('connect_error', (e) => _log.error('SOCKET', 'Erro de conexão:', e.message)); - } - - await loadClientsList(); - loadPatients(); - setupEventListeners(); - initDragAndDrop(); - setInterval(loadClientsList, 5000); - } catch (error) { - _log.error('INIT', 'Erro fatal na inicialização:', error.message, error); - showErrorBanner('Erro na inicialização', error.message); - localStorage.removeItem('auth_token'); - window.location.replace('/login'); - } -}); - -// ================================================================ -// EVENT LISTENERS -// ================================================================ - -function setupEventListeners() { - searchInput.addEventListener('input', debounce(() => { - if (view === 'patients') loadPatients(); - else loadPatientImages(selectedPatient.patient_name); - }, 400)); - - clientFilter.addEventListener('change', e => { - selectedClient = e.target.value; - if (view === 'patients') loadPatients(); - }); - - toggleDisabledBtn.addEventListener('click', () => { - showDisabled = !showDisabled; - toggleText.textContent = showDisabled ? 'Ver Ativas' : 'Ver Ocultas'; - if (view === 'patients') loadPatients(); - else loadPatientImages(selectedPatient.patient_name); - }); - - backBtn.addEventListener('click', () => showPatientsView()); - - // Transform modal close on backdrop - transformModal.addEventListener('click', e => { - if (e.target === transformModal) transformModal.classList.remove('active'); - }); - document.getElementById('closeTransformModal').addEventListener('click', () => { - transformModal.classList.remove('active'); - }); -} - -// ================================================================ -// CLIENTS LIST -// ================================================================ - -async function loadClientsList() { - try { - const r = await fetch('/api/socket/status'); - const data = await r.json(); - clientsList = (data.identified || []).map(c => ({ - ...c, name: c.name || 'Unknown', displayName: c.name || 'Unknown', - dbName: c.name || 'Unknown', status: 'identified' - })); - - const imagesRes = await fetchWithAuth('/api/images/patients?disabled=false&search='); - if (imagesRes.ok) { - const pts = await imagesRes.json(); - const uniqueClients = [...new Set(pts.map(p => p.client_name).filter(Boolean))]; - uniqueClients.forEach(cn => { - if (!clientsList.find(c => c.dbName === cn || c.name === cn)) { - clientsList.push({ name: cn, displayName: cn, dbName: cn, type: 'historic', socketId: null, status: 'historic' }); - } - }); - } - updateClientsDropdown(); - } catch (e) { console.error('Erro ao carregar clientes:', e); } -} - -function updateClientsDropdown() { - const current = clientFilter.value; - clientFilter.innerHTML = ''; - const sorted = [...clientsList].sort((a, b) => { - const o = { identified: 1, connected: 2, historic: 3 }; - return (o[a.status] || 99) - (o[b.status] || 99); - }); - sorted.forEach(c => { - const opt = document.createElement('option'); - opt.value = c.name; - let lbl = c.name; - if (c.name === 'Upload Web') { - lbl = 'Upload Web 💻 (Imagens do Painel)'; - } else { - if (c.status === 'identified') lbl += ' ✅'; - else if (c.status === 'historic') lbl += ' 📚'; - } - opt.textContent = lbl; - clientFilter.appendChild(opt); - }); - clientFilter.value = current || selectedClient; -} - -// ================================================================ -// VIEW: PATIENTS LIST -// ================================================================ - -function showPatientsView() { - view = 'patients'; - selectedPatient = null; - backBtn.style.display = 'none'; - patientHeader.style.display = 'none'; - loadPatients(); -} - -async function loadPatients(silent = false) { - _log.group('LOAD_PATIENTS'); - _log.info('LOAD_PATIENTS', `Iniciando carga. silent=${silent}, showDisabled=${showDisabled}, client="${selectedClient}"`); - - if (!silent) { - loadingState.style.display = 'block'; - imagesGrid.style.display = 'none'; - emptyState.style.display = 'none'; - } - - try { - const search = searchInput.value.trim(); - const clientParam = selectedClient ? `&client=${encodeURIComponent(selectedClient)}` : ''; - const url = `/api/images/patients?disabled=${showDisabled}&search=${encodeURIComponent(search)}${clientParam}`; - - const res = await fetchWithAuth(url); - - if (!res.ok) { - let errBody = ''; - try { errBody = await res.text(); } catch (_) {} - _log.error('LOAD_PATIENTS', `HTTP ${res.status} — body:`, errBody); - if (res.status === 401 || res.status === 403) { - _log.warn('AUTH', 'Token inválido/expirado, redirecionando para login'); - localStorage.removeItem('auth_token'); - window.location.href = '/login'; - return; - } - throw new Error(`HTTP ${res.status}: ${errBody || 'Erro ao carregar pacientes'}`); - } - - patients = await res.json(); - _log.info('LOAD_PATIENTS', `✅ ${patients.length} paciente(s) recebido(s)`); - if (patients.length > 0) { - _log.info('LOAD_PATIENTS', 'Primeiro paciente:', { - name: patients[0].patient_name, - thumb: patients[0].thumb_filename, - count: patients[0].image_count - }); - } - - loadingState.style.display = 'none'; - - if (patients.length === 0) { - _log.warn('LOAD_PATIENTS', 'Nenhum paciente encontrado — exibindo empty state'); - emptyState.style.display = 'block'; - imagesGrid.style.display = 'none'; - } else { - emptyState.style.display = 'none'; - imagesGrid.style.display = 'grid'; - _log.info('LOAD_PATIENTS', `imagesGrid display=${imagesGrid.style.display}`); - renderPatientCards(); - } - } catch (e) { - _log.error('LOAD_PATIENTS', '❌ Exceção:', e.message, e); - loadingState.style.display = 'none'; - emptyState.style.display = 'block'; - showErrorBanner('Erro ao carregar pacientes', e.message); - showToast('Erro ao carregar pacientes', 'error'); - } finally { - _log.end(); - } -} - -function renderPatientCards() { - _log.group('RENDER_CARDS'); - _log.info('RENDER_CARDS', `Renderizando ${patients.length} card(s)...`); - - // Inspecionar o grid antes de renderizar - const gridStyle = window.getComputedStyle(imagesGrid); - _log.info('RENDER_CARDS', 'imagesGrid computed style:', { - display: gridStyle.display, - gridTemplateColumns: gridStyle.gridTemplateColumns, - width: gridStyle.width, - height: gridStyle.height, - overflow: gridStyle.overflow - }); - - imagesGrid.innerHTML = patients.map((p, index) => { - const fullName = p.patient_name || 'Sem nome'; - const thumb = p.thumb_filename ? `/uploads/${p.thumb_filename}` : ''; - const count = p.image_count || 0; - const date = formatDate(p.last_date); - const doctor = p.doctor || '—'; - const remark = p.remark || '—'; - - if (index === 0) { - _log.info('RENDER_CARDS', 'Card #0 dados:', { fullName, thumb, count, date, doctor }); - } - - return ` -
-
- -
${count} imagem${count !== 1 ? 'ns' : ''}
-
-
-
${escapeHtml(fullName)}
-
📅 ${date}
-
-
🩺 Dentista: ${escapeHtml(doctor)}
-
📝 Obs: ${escapeHtml(remark)}
-
-
-
`; - }).join(''); - - // Inspecionar o primeiro card renderizado - requestAnimationFrame(() => { - const firstCard = imagesGrid.querySelector('.image-card'); - if (firstCard) { - const cs = window.getComputedStyle(firstCard); - const preview = firstCard.querySelector('.image-preview'); - const pcs = preview ? window.getComputedStyle(preview) : null; - _log.info('RENDER_CARDS', 'Primeiro card computado:', { - cardW: cs.width, cardH: cs.height, - previewH: pcs ? pcs.height : 'n/a', - previewBg: pcs ? pcs.backgroundImage.substring(0, 60) : 'n/a' - }); - } else { - _log.error('RENDER_CARDS', '❌ Nenhum card encontrado no DOM após render!'); - } - _log.info('RENDER_CARDS', `Total de cards no DOM: ${imagesGrid.querySelectorAll('.image-card').length}`); - _log.end(); - }); -} - -// Exclui todas as imagens de um determinado paciente (S3 e Local) -async function deletePatientImages(patientName) { - if (!patientName) return; - - // TODO(security): Usando confirm() para manter coerência com o restante das confirmações do painel do projeto - const ok = confirm(`⚠️ ATENÇÃO: Tem certeza que deseja excluir permanentemente todas as imagens do paciente "${patientName}"?\n\nEsta ação apagará os arquivos locais, do Wasabi S3 e do banco de dados, e NÃO poderá ser desfeita.`); - if (!ok) return; - - showToast('Excluindo imagens do paciente...', 'info'); - - try { - const res = await fetchWithAuth(`/api/images/by-patient?name=${encodeURIComponent(patientName)}`, { - method: 'DELETE' - }); - - if (!res.ok) { - const err = await res.json(); - throw new Error(err.error || 'Erro ao excluir imagens do paciente.'); - } - - showToast('✅ Todas as imagens do paciente foram excluídas!', 'success'); - - // Atualizar listagens - await loadPatients(); - await loadClientsList(); - - } catch (e) { - console.error('Erro na exclusão do paciente:', e); - showToast(`Falha ao excluir imagens: ${e.message}`, 'error'); - } -} - -// ================================================================ -// VIEW: PATIENT IMAGES -// ================================================================ - -function openPatient(patient) { - selectedPatient = patient; - view = 'patient-images'; - backBtn.style.display = 'inline-flex'; - patientHeader.style.display = 'block'; - - const fullName = patient.patient_name || 'Sem nome'; - patientTitle.textContent = fullName; - patientMeta.textContent = `${patient.image_count || 0} imagem(ns) · ${patient.doctor || '—'}`; - - loadPatientImages(patient.patient_name); -} - -async function loadPatientImages(patientName) { - _log.group('LOAD_IMAGES'); - _log.info('LOAD_IMAGES', `Carregando imagens de: "${patientName}"`); - - loadingState.style.display = 'block'; - imagesGrid.style.display = 'none'; - emptyState.style.display = 'none'; - - try { - const url = `/api/images/by-patient?name=${encodeURIComponent(patientName)}&disabled=${showDisabled}`; - const res = await fetchWithAuth(url); - - if (!res.ok) { - let errBody = ''; - try { errBody = await res.text(); } catch (_) {} - _log.error('LOAD_IMAGES', `HTTP ${res.status}:`, errBody); - throw new Error(`HTTP ${res.status}: ${errBody}`); - } - - patientImages = await res.json(); - _log.info('LOAD_IMAGES', `✅ ${patientImages.length} imagem(ns) recebida(s)`); - loadingState.style.display = 'none'; - - if (patientImages.length === 0) { - _log.warn('LOAD_IMAGES', 'Nenhuma imagem encontrada'); - emptyState.style.display = 'block'; - imagesGrid.style.display = 'none'; - } else { - emptyState.style.display = 'none'; - imagesGrid.style.display = 'grid'; - renderPatientImages(); - } - } catch (e) { - _log.error('LOAD_IMAGES', '❌ Exceção:', e.message, e); - loadingState.style.display = 'none'; - emptyState.style.display = 'block'; - showErrorBanner('Erro ao carregar imagens', e.message); - showToast('Erro ao carregar imagens', 'error'); - } finally { - _log.end(); - } -} - -function renderPatientImages() { - imagesGrid.innerHTML = patientImages.map(image => { - const thumbUrl = image.thumb_filename ? `/uploads/${image.thumb_filename}` : `/uploads/${image.filename}`; - return ` -
-
-
-
📅 ${formatDate(image.created_at)}
-
${escapeHtml(image.image_guid || image.filename)}
-
- - -
-
- ${!image.enabled ? '
Desabilitada
' : ''} -
`; - }).join(''); -} - -// ================================================================ -// IMAGE CLICK → TRANSFORM MODAL -// ================================================================ - -async function handleImageClick(imageId) { - selectedImageId = imageId; - await showTransformModal(imageId); -} - -async function showTransformModal(imageId) { - transformModal.classList.add('active'); - - // Default view is 'transform' - toggleModalView('transform'); - - // Find image to show details - const image = patientImages.find(img => img.id === imageId); - - // Check if the image has been modified (has original_image_id) - const btnResetImage = document.getElementById('btnResetImage'); - if (image && image.original_image_id) { - btnResetImage.style.display = 'flex'; - btnResetImage.setAttribute('data-modified', 'true'); - } else { - btnResetImage.style.display = 'none'; - btnResetImage.setAttribute('data-modified', 'false'); - } - - if (image) { - document.getElementById('transformModalTitle').innerText = `${escapeHtml(image.patient_name || 'Desconhecido')} - ${formatDate(image.created_at)}`; - - document.getElementById('transformExtraInfo').innerHTML = ` -
👨‍⚕️ Dentista: ${escapeHtml(image.doctor || 'Não informado')}
-
📝 Obs: ${escapeHtml(image.remark || 'Nenhuma observação')}
- `; - document.getElementById('transformExtraInfo').style.display = 'none'; // Keep hidden by default - - loadGtosForPatient(image.patient_name); - } else { - document.getElementById('transformModalTitle').innerText = `Opções da Imagem`; - document.getElementById('transformExtraInfo').innerHTML = ''; - document.getElementById('transformExtraInfo').style.display = 'none'; - } - - document.getElementById('transformOptions').innerHTML = ` -
-
-

Gerando variações...

-
`; - - try { - const image = patientImages.find(img => img.id === imageId); - if (!image) { - throw new Error('Imagem não encontrada localmente'); - } - - // Se existir thumb_filename, usar ele; caso contrário, fallback para a imagem cheia - const imageUrl = image.thumb_filename ? `/uploads/${image.thumb_filename}` : `/uploads/${image.filename}`; - - const transformations = [ - { name: 'Original', rotation: 0, flipH: false, flipV: false, css: 'transform: none;' }, - { name: 'Flip Horizontal', rotation: 0, flipH: true, flipV: false, css: 'transform: scaleX(-1);' }, - { name: 'Flip Vertical', rotation: 0, flipH: false, flipV: true, css: 'transform: scaleY(-1);' }, - { name: 'Rotação +90°', rotation: 90, flipH: false, flipV: false, css: 'transform: rotate(90deg);' }, - { name: 'Rotação -90°', rotation: -90, flipH: false, flipV: false, css: 'transform: rotate(-90deg);' } - ]; - - currentTransformations = transformations; - - const container = document.getElementById('transformOptions'); - container.innerHTML = transformations.map((t, i) => ` -
- ${escapeHtml(t.name)} -
${escapeHtml(t.name)}
-
- `).join(''); - - // --------------------------------------------------------------- - // Detectar orientação da imagem original → aplicar layout correto - // --------------------------------------------------------------- - const probe = new Image(); - probe.onload = () => { - const orientation = probe.naturalWidth >= probe.naturalHeight ? 'landscape' : 'portrait'; - container.classList.remove('grid-landscape', 'grid-portrait'); - container.classList.add(`grid-${orientation}`); - _log.info('TRANSFORM', `Orientação detectada: ${orientation} (${probe.naturalWidth}×${probe.naturalHeight})`); - }; - probe.onerror = () => { - _log.warn('TRANSFORM', 'Não foi possível detectar orientação — usando fallback'); - }; - probe.src = imageUrl; - - } catch (e) { - _log.error('TRANSFORM', '❌ Exceção:', e.message, e); - showToast('Erro ao carregar variações', 'error'); - transformModal.classList.remove('active'); - } -} - - -function selectTransform(index) { - const el = document.querySelector(`.transform-option[data-index="${index}"]`); - if (el.classList.contains('selected')) { - // If clicking the already selected one, clear selection - clearTransformSelection(); - return; - } - - const container = document.getElementById('transformOptions'); - container.classList.add('selection-mode'); - - document.querySelectorAll('.transform-option').forEach(option => { - option.classList.remove('selected'); - const optIndex = parseInt(option.getAttribute('data-index')); - if (optIndex === 0 || optIndex === index) { - option.style.display = 'block'; - } else { - option.style.display = 'none'; - } - }); - - el.classList.add('selected'); - - // Reset fine tune angle when selecting a new transform - fineTuneAngle = 0; - updateFineTuneDisplay(); - - // 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'); - - // 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'); - showToast('Orientação salva!', 'success'); - if (selectedPatient) loadPatientImages(selectedPatient.patient_name); - } catch (e) { - console.error(e); - showToast('Erro ao salvar orientação', 'error'); - } -} - -function adjustFineTune(offset) { - const selectedEl = document.querySelector('.transform-option.selected'); - if (!selectedEl) return; - - const index = parseInt(selectedEl.getAttribute('data-index')); - const transform = currentTransformations[index]; - - fineTuneAngle += offset; - updateFineTuneDisplay(); - - // Update the image style using the combined transform - const imgEl = selectedEl.querySelector('img'); - if (imgEl) { - let totalRotation = transform.rotation + fineTuneAngle; - let transformStr = `rotate(${totalRotation}deg)`; - if (transform.flipH) transformStr += ` scaleX(-1)`; - if (transform.flipV) transformStr += ` scaleY(-1)`; - imgEl.style.transform = transformStr; - } - - // Update display name inside the card - const nameEl = selectedEl.querySelector('.transform-name'); - if (nameEl) { - nameEl.innerText = `${transform.name} (${fineTuneAngle > 0 ? '+' : ''}${fineTuneAngle}°)`; - } -} - -function resetFineTune() { - adjustFineTune(-fineTuneAngle); -} - -function updateFineTuneDisplay() { - const displayEl = document.getElementById('fineTuneValDisplay'); - if (displayEl) { - displayEl.textContent = `${fineTuneAngle > 0 ? '+' : ''}${fineTuneAngle}°`; - } - - const resetEl = document.getElementById('btnResetFineTune'); - if (resetEl) { - resetEl.style.display = fineTuneAngle !== 0 ? 'inline-block' : 'none'; - } -} - -async function toggleImageEnabled(imageId) { - try { - const res = await fetchWithAuth(`/api/images/${imageId}/toggle-enabled`, { method: 'PUT' }); - if (!res.ok) throw new Error(); - const result = await res.json(); - showToast(result.enabled ? 'Imagem habilitada!' : 'Imagem desabilitada!', 'success'); - if (selectedPatient) loadPatientImages(selectedPatient.patient_name); - } catch { - showToast('Erro ao alterar status', 'error'); - } -} - -// ================================================================ -// UTILITY FUNCTIONS -// ================================================================ - -function escapeHtml(text) { - if (!text) return ''; - const d = document.createElement('div'); - d.textContent = text; - return d.innerHTML; -} - -function formatDate(dateString) { - if (!dateString) return '—'; - const date = new Date(dateString); - return date.toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); -} - -function debounce(func, wait) { - let timeout; - return function (...args) { - clearTimeout(timeout); - timeout = setTimeout(() => func(...args), wait); - }; -} - -function showToast(message, type = 'info') { - const toast = document.getElementById('toast'); - toast.textContent = message; - toast.className = `toast show ${type}`; - setTimeout(() => toast.classList.remove('show'), 3000); -} - -function openPatientByIndex(index) { - if (patients[index]) { - openPatient(patients[index]); - } -} - -window.openPatient = openPatient; -window.openPatientByIndex = openPatientByIndex; -window.handleImageClick = handleImageClick; -window.selectTransform = selectTransform; -window.toggleImageEnabled = toggleImageEnabled; - -// ================================================================ -// PATIENT CREATION (DENTAL SENSOR) -// ================================================================ - -function openCreatePatientModal() { - document.getElementById('create-patient-modal').classList.add('active'); -} - -function closeCreatePatientModal() { - document.getElementById('create-patient-modal').classList.remove('active'); - document.getElementById('create-patient-form').reset(); -} - -async function submitCreatePatient(event) { - event.preventDefault(); - - const submitBtn = event.target.querySelector('button[type="submit"]'); - const originalText = submitBtn.textContent; - submitBtn.textContent = 'Salvando...'; - submitBtn.disabled = true; - - const payload = { - firstName: document.getElementById('patient-first-name').value.trim(), - lastName: document.getElementById('patient-last-name').value.trim(), - doctor: document.getElementById('patient-doctor').value.trim(), - birthday: document.getElementById('patient-birthday').value || null, - gender: document.getElementById('patient-gender').value, - phone: document.getElementById('patient-phone').value.trim(), - remark: document.getElementById('patient-remark').value.trim() - }; - - try { - const response = await fetchWithAuth(`${API_URL}/patients`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - - if (response.ok) { - showToast('Paciente criado com sucesso no Dental Sensor!', 'success'); - closeCreatePatientModal(); - } else { - const err = await response.json(); - alert('Erro ao criar paciente: ' + (err.error || 'Desconhecido')); - } - } catch (err) { - console.error(err); - alert('Erro ao conectar com o servidor para criar paciente.'); - } finally { - submitBtn.textContent = originalText; - submitBtn.disabled = false; - } -} - -// ================================================================ -// GTO LOGIC -// ================================================================ - -function toggleModalView(view) { - const transformViewWrapper = document.getElementById('transformViewWrapper'); - const gtoView = document.getElementById('gtoView'); - const btnShowTransform = document.getElementById('btnShowTransform'); - const btnShowGto = document.getElementById('btnShowGto'); - const btnResetImage = document.getElementById('btnResetImage'); - - if (view === 'transform') { - transformViewWrapper.style.display = 'flex'; - gtoView.style.display = 'none'; - btnShowTransform.className = 'btn btn-primary'; - btnShowGto.className = 'btn btn-secondary'; - - // Restore reset button visibility if image is modified - if (btnResetImage.getAttribute('data-modified') === 'true') { - btnResetImage.style.display = 'flex'; - } - } else { - transformViewWrapper.style.display = 'none'; - gtoView.style.display = 'block'; - btnShowTransform.className = 'btn btn-secondary'; - btnShowGto.className = 'btn btn-primary'; - - // Hide reset button in GTO view - btnResetImage.style.display = 'none'; - } -} - -function toggleModalInfo() { - const infoDiv = document.getElementById('transformExtraInfo'); - const btnShowInfo = document.getElementById('btnShowInfo'); - if (infoDiv.style.display === 'none') { - infoDiv.style.display = 'block'; - btnShowInfo.className = 'btn btn-primary'; - } else { - infoDiv.style.display = 'none'; - btnShowInfo.className = 'btn btn-secondary'; - } -} - -async function loadGtosForPatient(patientName) { - const container = document.getElementById('gtoListContainer'); - container.innerHTML = '
Carregando GTOs...
'; - - try { - const res = await fetchWithAuth(`/api/gtos?patient=${encodeURIComponent(patientName)}`); - if (!res.ok) throw new Error('Erro ao carregar GTOs'); - const gtos = await res.json(); - - if (gtos.length === 0) { - container.innerHTML = '
Nenhuma GTO cadastrada para este paciente.
'; - return; - } - - let html = ''; - container.innerHTML = html; - - } catch (e) { - console.error(e); - container.innerHTML = '
Erro ao carregar GTOs
'; - } -} - -async function inlineCreateGto() { - const numberInput = document.getElementById('inlineGtoNumber'); - const descInput = document.getElementById('inlineGtoDesc'); - - const number = numberInput.value.trim(); - if (!number) { - showToast('O Número da GTO é obrigatório.', 'error'); - return; - } - - const image = patientImages.find(img => img.id === selectedImageId); - if (!image || !image.patient_name) { - showToast('Erro: Paciente não identificado.', 'error'); - return; - } - - const payload = { - gto_number: number, - description: descInput.value.trim(), - patient_name: image.patient_name - }; - - try { - const res = await fetchWithAuth('/api/gtos', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - - if (!res.ok) throw new Error('Erro ao criar GTO'); - - showToast('GTO criada com sucesso!', 'success'); - - // Limpar inputs - numberInput.value = ''; - descInput.value = ''; - - // Recarregar lista - await loadGtosForPatient(image.patient_name); - - } catch (e) { - console.error(e); - showToast('Erro ao criar GTO.', 'error'); - } -} - -async function linkImageToGto(gtoId, btnElement) { - if (!gtoId) return; - - const originalText = btnElement.innerText; - btnElement.innerText = 'Vinculando...'; - btnElement.disabled = true; - - try { - const res = await fetchWithAuth(`/api/gtos/${gtoId}/images`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ image_id: selectedImageId }) - }); - - if (!res.ok) { - const err = await res.json(); - throw new Error(err.error || 'Erro ao vincular'); - } - - showToast('Imagem vinculada à GTO!', 'success'); - btnElement.innerText = 'Vinculada!'; - btnElement.classList.replace('btn-primary', 'btn-success'); - } catch (e) { - console.error(e); - showToast(e.message, 'error'); - btnElement.innerText = originalText; - btnElement.disabled = false; - } -} - -async function resetImageToOriginal() { - if (!confirm('Tem certeza que deseja restaurar esta imagem para o estado original em que foi capturada pelo sensor? Rotações, espelhamentos e observações serão descartados.')) { - return; - } - - const btn = document.getElementById('btnResetImage'); - const originalContent = btn.innerHTML; - btn.innerHTML = '
'; - btn.disabled = true; - - try { - const payload = { - rotation: 0, - flipH: false, - flipV: false, - remark: '' - }; - - const res = await fetchWithAuth(`/api/images/${selectedImageId}/transform`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - - if (!res.ok) throw new Error('Erro ao resetar imagem'); - - showToast('Imagem restaurada para o original!', 'success'); - - transformModal.classList.remove('active'); - clearTransformSelection(); - - const patientName = patientImages.find(img => img.id === selectedImageId)?.patient_name; - await loadPatients(); - if (patientName) { - await loadPatientImages(patientName); - } - - } catch (e) { - console.error(e); - showToast('Erro ao restaurar a imagem.', 'error'); - } finally { - btn.innerHTML = originalContent; - btn.disabled = false; - } -} - -// Global scope exposures -window.handleImageClick = handleImageClick; -window.toggleImageEnabled = toggleImageEnabled; -window.selectTransform = selectTransform; -window.openCreatePatientModal = openCreatePatientModal; -window.closeCreatePatientModal = closeCreatePatientModal; -window.submitCreatePatient = submitCreatePatient; -window.toggleModalView = toggleModalView; -window.toggleModalInfo = toggleModalInfo; -window.inlineCreateGto = inlineCreateGto; -window.saveSelectedTransform = saveSelectedTransform; -window.clearTransformSelection = clearTransformSelection; -window.resetImageToOriginal = resetImageToOriginal; - -// ================================================================ -// CONFIGURAÇÕES (Trocar Usuário/Senha) -// ================================================================ - -function openSettingsModal() { - const modal = document.getElementById('settingsModal'); - if (modal) modal.style.display = 'block'; - - // Limpar form - document.getElementById('currentPassword').value = ''; - document.getElementById('newUsername').value = ''; - document.getElementById('newPassword').value = ''; -} - -function closeSettingsModal() { - const modal = document.getElementById('settingsModal'); - if (modal) modal.style.display = 'none'; -} - -async function updateCredentials(event) { - event.preventDefault(); - - const currentPassword = document.getElementById('currentPassword').value; - const newUsername = document.getElementById('newUsername').value.trim(); - const newPassword = document.getElementById('newPassword').value; - - if (!currentPassword) { - showToast('A senha atual é obrigatória.', 'error'); - return; - } - - if (!newUsername && !newPassword) { - showToast('Preencha um novo usuário ou nova senha.', 'error'); - return; - } - - const btn = document.getElementById('btnSaveSettings'); - const originalText = btn.innerHTML; - btn.innerHTML = ' Salvando...'; - btn.disabled = true; - - try { - const token = getAuthToken(); - const res = await fetch(`${API_URL}/auth/update-credentials`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }, - body: JSON.stringify({ currentPassword, newUsername, newPassword }) - }); - - const data = await res.json(); - - if (!res.ok) { - throw new Error(data.error || 'Erro ao atualizar credenciais.'); - } - - // Sucesso - showToast('Credenciais atualizadas com sucesso!', 'success'); - - // Atualizar token no localStorage se tiver um novo - if (data.token) { - localStorage.setItem('auth_token', data.token); - } - - closeSettingsModal(); - - } catch (e) { - console.error(e); - showToast(e.message, 'error'); - } finally { - btn.innerHTML = originalText; - btn.disabled = false; - } -} - -// Global scope -window.openSettingsModal = openSettingsModal; -window.closeSettingsModal = closeSettingsModal; -window.updateCredentials = updateCredentials; - -// ================================================================ -// CONFIGURAÇÕES DE PLUGINS (Wasabi Storage) -// ================================================================ - -function openPluginsModal() { - const modal = document.getElementById('pluginsModal'); - if (modal) modal.style.display = 'block'; - - // Limpar campos - document.getElementById('wasabiAccessKey').value = ''; - document.getElementById('wasabiSecretKey').value = ''; - document.getElementById('wasabiBucket').value = ''; - document.getElementById('wasabiRegion').value = ''; - document.getElementById('wasabiEndpoint').value = ''; - - const alertBox = document.getElementById('pluginsStatusAlert'); - alertBox.style.display = 'none'; - - // Carregar configurações atuais - loadPluginsConfig(); -} - -function closePluginsModal() { - const modal = document.getElementById('pluginsModal'); - if (modal) modal.style.display = 'none'; -} - -async function loadPluginsConfig() { - try { - const res = await fetchWithAuth('/api/system/storage-config'); - if (!res.ok) throw new Error('Falha ao carregar configurações'); - - const config = await res.json(); - - document.getElementById('wasabiAccessKey').value = config.wasabiAccessKey || ''; - document.getElementById('wasabiSecretKey').value = config.wasabiSecretKey || ''; - document.getElementById('wasabiBucket').value = config.wasabiBucket || ''; - document.getElementById('wasabiRegion').value = config.wasabiRegion || ''; - document.getElementById('wasabiEndpoint').value = config.wasabiEndpoint || ''; - - const alertBox = document.getElementById('pluginsStatusAlert'); - if (config.enabled) { - alertBox.className = 'alert-success'; - alertBox.style.background = '#eafaf1'; - alertBox.style.color = '#27ae60'; - alertBox.style.border = '1px solid #2ecc7133'; - alertBox.innerHTML = '🟢 Status: Wasabi Storage está ATIVO e conectado.'; - } else { - alertBox.className = 'alert-warning'; - alertBox.style.background = '#fffbeb'; - alertBox.style.color = '#d97706'; - alertBox.style.border = '1px solid #f59e0b33'; - alertBox.innerHTML = '🟡 Status: Wasabi Storage está DESATIVADO (utilizando armazenamento local).'; - } - alertBox.style.display = 'block'; - } catch (e) { - console.error(e); - showToast('Erro ao carregar configurações do Wasabi.', 'error'); - } -} - -async function updatePluginsConfig(event) { - event.preventDefault(); - - const wasabiAccessKey = document.getElementById('wasabiAccessKey').value.trim(); - const wasabiSecretKey = document.getElementById('wasabiSecretKey').value; - const wasabiBucket = document.getElementById('wasabiBucket').value.trim(); - const wasabiRegion = document.getElementById('wasabiRegion').value.trim(); - const wasabiEndpoint = document.getElementById('wasabiEndpoint').value.trim(); - - const btn = document.getElementById('btnSavePlugins'); - const originalText = btn.innerHTML; - btn.innerHTML = ' Salvando...'; - btn.disabled = true; - - try { - const res = await fetchWithAuth('/api/system/storage-config', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - wasabiAccessKey, - wasabiSecretKey, - wasabiBucket, - wasabiRegion, - wasabiEndpoint - }) - }); - - if (!res.ok) { - const err = await res.json(); - throw new Error(err.error || 'Erro ao salvar configurações.'); - } - - showToast('Configurações do Wasabi salvas com sucesso!', 'success'); - - // Recarregar o status - await loadPluginsConfig(); - - // Fechar após 1 segundo - setTimeout(closePluginsModal, 1200); - - } catch (e) { - console.error(e); - showToast(e.message, 'error'); - } finally { - btn.innerHTML = originalText; - btn.disabled = false; - } -} - -async function triggerSync() { - showToast('🔄 Enviando sinal de sincronização para dispositivos...', 'info'); - try { - const res = await fetchWithAuth('/api/system/trigger-sync', { - method: 'POST', - headers: { 'Content-Type': 'application/json' } - }); - - if (!res.ok) { - const err = await res.json(); - throw new Error(err.error || 'Erro ao acionar sincronização.'); - } - - const data = await res.json(); - if (data.devicesTriggered > 0) { - showToast(`✅ Sinal enviado para ${data.devicesTriggered} dispositivo(s) conectado(s)!`, 'success'); - } else { - showToast('⚠️ Nenhum dispositivo Windows conectado no momento.', 'warning'); - } - } catch (e) { - console.error(e); - showToast(e.message, 'error'); - } -} - -// Global scope -window.openPluginsModal = openPluginsModal; -window.closePluginsModal = closePluginsModal; -window.updatePluginsConfig = updatePluginsConfig; -window.triggerSync = triggerSync; - -// ================================================================ -// ÁREA DE SINCRONIZAÇÃO E UPLOAD MANUAL -// ================================================================ - -let selectedUploadFiles = []; - -function openSyncModal() { - const modal = document.getElementById('syncModal'); - if (!modal) return; - - modal.classList.add('active'); - modal.style.display = 'flex'; - - // Resetar abas - switchSyncTab('devices'); - - // Limpar formulário de upload - resetManualUploadForm(); - - // Carregar dados - loadSyncDevices(); - refreshUploadPatients(); - - // Escuta de clique fora para fechar modal - modal.onclick = (e) => { - if (e.target === modal) closeSyncModal(); - }; -} - -function closeSyncModal() { - const modal = document.getElementById('syncModal'); - if (!modal) return; - - modal.classList.remove('active'); - modal.style.display = 'none'; -} - -function switchSyncTab(tabName) { - const btnDevices = document.getElementById('tabBtnDevices'); - const btnUpload = document.getElementById('tabBtnUpload'); - const tabDevices = document.getElementById('tab-devices'); - const tabUpload = document.getElementById('tab-upload'); - - if (!btnDevices || !btnUpload || !tabDevices || !tabUpload) return; - - if (tabName === 'devices') { - btnDevices.classList.add('active'); - btnUpload.classList.remove('active'); - tabDevices.classList.add('active'); - tabUpload.classList.remove('active'); - loadSyncDevices(); - } else { - btnDevices.classList.remove('active'); - btnUpload.classList.add('active'); - tabDevices.classList.remove('active'); - tabUpload.classList.add('active'); - } -} - -async function loadSyncDevices() { - const loader = document.getElementById('syncDevicesLoading'); - const empty = document.getElementById('syncDevicesEmpty'); - const list = document.getElementById('syncDeviceList'); - - if (!loader || !empty || !list) return; - - loader.style.display = 'block'; - empty.style.display = 'none'; - list.style.display = 'none'; - - try { - const res = await fetchWithAuth('/api/socket/status'); - if (!res.ok) throw new Error('Falha ao obter status dos dispositivos.'); - const data = await res.json(); - - list.replaceChildren(); - - const windowsClients = (data.identified || []).filter(c => c.type === 'client' || c.type === 'windows'); - - if (windowsClients.length === 0) { - loader.style.display = 'none'; - empty.style.display = 'block'; - list.style.display = 'none'; - return; - } - - windowsClients.forEach(client => { - const card = document.createElement('div'); - card.className = 'sync-device-card'; - - const left = document.createElement('div'); - left.className = 'device-card-left'; - - const title = document.createElement('div'); - title.className = 'device-card-title'; - title.textContent = `💻 ${client.name || 'Dispositivo Sem Nome'}`; - - const subtitle = document.createElement('div'); - subtitle.className = 'device-card-subtitle'; - subtitle.textContent = `SO: ${client.system || 'Windows'} | Versão: ${client.version || 'N/A'} | ID: ${client.socketId}`; - - left.appendChild(title); - left.appendChild(subtitle); - - const badge = document.createElement('span'); - badge.className = 'device-card-badge badge-connected'; - badge.textContent = 'ONLINE'; - - card.appendChild(left); - card.appendChild(badge); - - list.appendChild(card); - }); - - loader.style.display = 'none'; - empty.style.display = 'none'; - list.style.display = 'flex'; - - } catch (e) { - console.error(e); - loader.style.display = 'none'; - empty.style.display = 'block'; - const p = empty.querySelector('p'); - if (p) p.textContent = 'Erro ao carregar dispositivos: ' + e.message; - } -} - -async function triggerSyncFromModal() { - const btn = document.getElementById('btnTriggerSyncModal'); - if (!btn) return; - - const originalText = btn.innerHTML; - btn.disabled = true; - btn.innerHTML = '🔄 Enviando sinal...'; - - try { - const res = await fetchWithAuth('/api/system/trigger-sync', { - method: 'POST', - headers: { 'Content-Type': 'application/json' } - }); - - if (!res.ok) { - const err = await res.json(); - throw new Error(err.error || 'Erro ao acionar sincronização.'); - } - - const data = await res.json(); - if (data.devicesTriggered > 0) { - showToast(`✅ Sinal enviado para ${data.devicesTriggered} dispositivo(s) conectado(s)!`, 'success'); - } else { - showToast('⚠️ Nenhum dispositivo Windows conectado para sincronizar.', 'warning'); - } - } catch (e) { - console.error(e); - showToast(e.message, 'error'); - } finally { - btn.disabled = false; - btn.innerHTML = originalText; - } -} - -async function refreshUploadPatients() { - const select = document.getElementById('uploadPatientSelect'); - if (!select) return; - - // Manter as primeiras duas opções - while (select.options.length > 2) { - select.remove(2); - } - - try { - const res = await fetchWithAuth('/api/images/patients?disabled=false&search='); - if (!res.ok) throw new Error('Erro ao listar pacientes.'); - - const patients = await res.json(); - patients.forEach(p => { - if (!p || !p.patient_name) return; - const opt = document.createElement('option'); - opt.value = p.patient_name; - opt.textContent = p.patient_name; - select.appendChild(opt); - }); - } catch (e) { - console.error(e); - showToast('Erro ao carregar lista de pacientes.', 'error'); - } -} - -function toggleNewPatientFields() { - const select = document.getElementById('uploadPatientSelect'); - const fields = document.getElementById('newPatientFields'); - - if (!select || !fields) return; - - if (select.value === 'NEW_PATIENT') { - fields.style.display = 'block'; - document.getElementById('uploadNewPatientFirstName').required = true; - document.getElementById('uploadNewPatientLastName').required = true; - } else { - fields.style.display = 'none'; - document.getElementById('uploadNewPatientFirstName').required = false; - document.getElementById('uploadNewPatientLastName').required = false; - } -} - -function resetManualUploadForm() { - selectedUploadFiles = []; - - const form = document.getElementById('manualUploadForm'); - if (form) form.reset(); - - const previews = document.getElementById('uploadPreviewsContainer'); - if (previews) previews.replaceChildren(); - - const progressContainer = document.getElementById('uploadProgressContainer'); - if (progressContainer) progressContainer.style.display = 'none'; - - const fields = document.getElementById('newPatientFields'); - if (fields) fields.style.display = 'none'; - - const btnSubmit = document.getElementById('btnSubmitManualUpload'); - if (btnSubmit) btnSubmit.disabled = true; -} - -// Inicializar Drag and Drop -function initDragAndDrop() { - const zone = document.getElementById('uploadDragDropZone'); - const fileInput = document.getElementById('uploadFileInput'); - - if (!zone || !fileInput) return; - - // Evento de clique na zona abre o seletor de arquivos - zone.addEventListener('click', () => fileInput.click()); - - ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { - zone.addEventListener(eventName, e => { - e.preventDefault(); - e.stopPropagation(); - }, false); - }); - - ['dragenter', 'dragover'].forEach(eventName => { - zone.addEventListener(eventName, () => { - zone.classList.add('drag-active'); - }, false); - }); - - ['dragleave', 'drop'].forEach(eventName => { - zone.addEventListener(eventName, () => { - zone.classList.remove('drag-active'); - }, false); - }); - - zone.addEventListener('drop', e => { - const dt = e.dataTransfer; - if (dt) handleUploadFiles(dt.files); - }); - - fileInput.addEventListener('change', e => { - if (e.target && e.target.files) handleUploadFiles(e.target.files); - }); -} - -function handleUploadFiles(files) { - if (!files || files.length === 0) return; - - for (let i = 0; i < files.length; i++) { - const file = files[i]; - - if (!file.type.match('image.*')) { - showToast(`O arquivo "${file.name}" não é uma imagem válida!`, 'warning'); - continue; - } - - if (file.size > 10 * 1024 * 1024) { - showToast(`A imagem "${file.name}" excede o limite de tamanho de 10MB!`, 'warning'); - continue; - } - - if (selectedUploadFiles.some(f => f.name === file.name && f.size === file.size)) { - continue; - } - - selectedUploadFiles.push(file); - } - - renderUploadPreviews(); -} - -function renderUploadPreviews() { - const container = document.getElementById('uploadPreviewsContainer'); - const btnSubmit = document.getElementById('btnSubmitManualUpload'); - if (!container) return; - - container.replaceChildren(); - - selectedUploadFiles.forEach((file, index) => { - const item = document.createElement('div'); - item.className = 'preview-item'; - - const img = document.createElement('img'); - img.alt = file.name; - - const reader = new FileReader(); - reader.onload = e => { - if (e.target && e.target.result) img.src = e.target.result; - }; - reader.readAsDataURL(file); - - const btnRemove = document.createElement('button'); - btnRemove.type = 'button'; - btnRemove.className = 'preview-remove'; - btnRemove.innerHTML = '×'; - btnRemove.title = 'Remover Imagem'; - btnRemove.onclick = (e) => { - e.stopPropagation(); - removeUploadPreview(index); - }; - - item.appendChild(img); - item.appendChild(btnRemove); - container.appendChild(item); - }); - - if (btnSubmit) { - btnSubmit.disabled = selectedUploadFiles.length === 0; - } -} - -function removeUploadPreview(index) { - selectedUploadFiles.splice(index, 1); - renderUploadPreviews(); -} - -function readFileAsBase64(file) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result); - reader.onerror = error => reject(error); - reader.readAsDataURL(file); - }); -} - -async function handleManualUploadSubmit(event) { - event.preventDefault(); - - if (selectedUploadFiles.length === 0) { - showToast('Selecione pelo menos uma imagem para enviar.', 'warning'); - return; - } - - const select = document.getElementById('uploadPatientSelect'); - const remarkInput = document.getElementById('uploadRemark'); - const btnSubmit = document.getElementById('btnSubmitManualUpload'); - - if (!select || !btnSubmit) return; - - let patientName = ''; - let doctor = ''; - - if (select.value === 'NEW_PATIENT') { - const firstName = document.getElementById('uploadNewPatientFirstName').value.trim(); - const lastName = document.getElementById('uploadNewPatientLastName').value.trim(); - doctor = document.getElementById('uploadNewPatientDoctor').value.trim(); - - if (!firstName || !lastName) { - showToast('Nome e sobrenome são obrigatórios para novo paciente.', 'warning'); - return; - } - - patientName = `${firstName} ${lastName}`; - } else { - patientName = select.value; - } - - const remark = remarkInput ? remarkInput.value.trim() : ''; - - const progressContainer = document.getElementById('uploadProgressContainer'); - const progressText = document.getElementById('uploadProgressText'); - const progressBarFill = document.getElementById('uploadProgressBarFill'); - const progressPercent = document.getElementById('uploadProgressPercent'); - - if (progressContainer) progressContainer.style.display = 'block'; - btnSubmit.disabled = true; - - const totalFiles = selectedUploadFiles.length; - let successCount = 0; - - for (let i = 0; i < totalFiles; i++) { - const file = selectedUploadFiles[i]; - - if (progressText) progressText.textContent = `Enviando imagem ${i + 1} de ${totalFiles}...`; - if (progressBarFill) { - const pct = Math.round((i / totalFiles) * 100); - progressBarFill.style.width = `${pct}%`; - if (progressPercent) progressPercent.textContent = `${pct}%`; - } - - try { - const base64 = await readFileAsBase64(file); - - const response = await fetchWithAuth('/api/images/upload', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - imageBase64: base64, - filename: file.name, - patientName: patientName, - doctor: doctor || null, - remark: remark || null - }) - }); - - if (!response.ok) { - const err = await response.json(); - throw new Error(err.error || `Erro HTTP ${response.status}`); - } - - successCount++; - } catch (e) { - console.error(`Erro ao enviar arquivo ${file.name}:`, e); - showToast(`Falha ao enviar a imagem ${file.name}: ${e.message}`, 'error'); - } - } - - if (progressBarFill) { - progressBarFill.style.width = '100%'; - if (progressPercent) progressPercent.textContent = '100%'; - } - if (progressText) progressText.textContent = `Finalizado! ${successCount} de ${totalFiles} imagens enviadas.`; - - setTimeout(async () => { - closeSyncModal(); - showToast(`✅ ${successCount} imagem(ns) enviada(s) com sucesso!`, 'success'); - - await loadPatients(); - if (window.loadClientsList) { - await window.loadClientsList(); - } - }, 1000); -} - -// Vinculando ao window -window.openSyncModal = openSyncModal; -window.closeSyncModal = closeSyncModal; -window.switchSyncTab = switchSyncTab; -window.triggerSyncFromModal = triggerSyncFromModal; -window.refreshUploadPatients = refreshUploadPatients; -window.toggleNewPatientFields = toggleNewPatientFields; -window.handleManualUploadSubmit = handleManualUploadSubmit; -window.initDragAndDrop = initDragAndDrop; -window.deletePatientImages = deletePatientImages; -window.adjustFineTune = adjustFineTune; -window.resetFineTune = resetFineTune; -// A aplicação é inicializada no evento DOMContentLoaded acima - -function logout() { localStorage.removeItem('token'); window.location.href = '/login.html'; } diff --git a/dental-server/public/public/clients.html b/dental-server/public/public/clients.html deleted file mode 100644 index 7934046..0000000 --- a/dental-server/public/public/clients.html +++ /dev/null @@ -1,484 +0,0 @@ - - - - - - Clientes Conectados - Dental Server - - - - -
-
-
-

🖥️ Clientes Conectados

-

Monitoramento em tempo real dos clientes Socket.IO

-
-
- ← Voltar - - -
-
- -
- -
- -
-
-
Carregando...
-
-
-
-
- - - - - - diff --git a/dental-server/public/public/favicon.svg b/dental-server/public/public/favicon.svg deleted file mode 100644 index 62bde5a..0000000 --- a/dental-server/public/public/favicon.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - diff --git a/dental-server/public/public/index.html b/dental-server/public/public/index.html deleted file mode 100644 index d235c6c..0000000 --- a/dental-server/public/public/index.html +++ /dev/null @@ -1,480 +0,0 @@ - - - - - - Gerenciador de Imagens Dentais - - - - - -
- - - - -
- -
-
-
- -

Galeria de Imagens

-
-
- - -
-
- - - -
- - -
- - -
-
-

Carregando...

-
- - - - - -
- -
-
-
- - - - - - - - - - - -
Operação realizada com sucesso!
- - - - - - - - - - - - - - - diff --git a/dental-server/public/public/install.html b/dental-server/public/public/install.html deleted file mode 100644 index 85ae77d..0000000 --- a/dental-server/public/public/install.html +++ /dev/null @@ -1,441 +0,0 @@ - - - - - - Instalação - Dental Image Server - - - -
-

🦷 Instalação do Sistema

-

Configure o banco de dados e crie o usuário administrador

- -
-
- Verificando status da instalação... - -
-
- -
- -
-
-
- 1 - Configuração do Banco de Dados -
-
- - -
-
- - -
-
- - -
O banco será criado automaticamente se não existir
-
-
- - -
-
- - -
-
- -
-
- 2 - Usuário Administrador -
-
- - -
-
- - -
-
- - -
Mínimo de 8 caracteres
-
-
- - -
- -
-
-

Instalando... Por favor aguarde.

-
- -
-
-

Instalação Concluída!

-

O sistema foi configurado com sucesso.

- -
-
- - - - - diff --git a/dental-server/public/public/reset.html b/dental-server/public/public/reset.html deleted file mode 100644 index ba2e157..0000000 --- a/dental-server/public/public/reset.html +++ /dev/null @@ -1,204 +0,0 @@ - - - - - - Resetar Sistema - DentalSys - - - - - - - -
-
-

⚠️ Apagar Tudo

-

Preparação para Servidor de Produção

-
- -
- Atenção: Esta ação é irreversível. Ela irá apagar todos os pacientes, GTOs, histórico e todas as imagens (físicas e do banco). O seu usuário e senha de acesso serão mantidos. -
- -
-
- - -
- - - -
-
- -
Mensagem
- - - - diff --git a/dental-server/public/public/style.css b/dental-server/public/public/style.css deleted file mode 100644 index 8d40961..0000000 --- a/dental-server/public/public/style.css +++ /dev/null @@ -1,1311 +0,0 @@ -/* ================================================================ - DENTAL IMAGE MANAGER - STYLES (Mobile-First) - ================================================================ */ - -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); - -:root { - --primary-color: #667eea; - --primary-dark: #5568d3; - --secondary-color: #764ba2; - --success-color: #51cf66; - --danger-color: #ff6b6b; - --warning-color: #ffd43b; - --info-color: #339af0; - --dark-color: #1a202c; - --light-color: #f0f4ff; - --border-color: #e2e8f0; - --text-secondary: #718096; - --card-bg: #ffffff; - --shadow: 0 2px 8px rgba(102, 126, 234, 0.1); - --shadow-lg: 0 10px 30px rgba(102, 126, 234, 0.15); - --radius: 14px; - --radius-sm: 8px; - --header-h: 60px; - --bg-color: var(--light-color); - --text-color: var(--dark-color); -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; - -webkit-tap-highlight-color: transparent; -} - -html { - scroll-behavior: smooth; -} - -body { - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - background: var(--light-color); - color: var(--dark-color); - line-height: 1.6; - min-height: 100vh; - -webkit-font-smoothing: antialiased; -} - -/* ================================================================ - LAYOUT CONTAINER - ================================================================ */ - -.container { - width: 100%; - max-width: 1400px; - margin: 0 auto; - padding: 12px; -} - -@media (min-width: 768px) { - .container { - padding: 24px; - } -} - -/* ================================================================ - HEADER - ================================================================ */ - -.header { - background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); - color: white; - padding: 18px 16px; - border-radius: var(--radius); - box-shadow: var(--shadow-lg); - margin-bottom: 16px; - position: sticky; - top: 8px; - z-index: 100; -} - -@media (min-width: 768px) { - .header { - padding: 28px 30px; - margin-bottom: 28px; - position: static; - } -} - -.header-content { - display: flex; - flex-direction: column; - gap: 12px; -} - -@media (min-width: 768px) { - .header-content { - flex-direction: row; - align-items: center; - justify-content: space-between; - } -} - -.header-left { - display: flex; - align-items: center; - gap: 12px; - min-width: 0; -} - -.header h1 { - font-size: 1.3rem; - font-weight: 700; - letter-spacing: -0.3px; - white-space: nowrap; -} - -@media (min-width: 768px) { - .header h1 { - font-size: 1.8rem; - } -} - -/* Back button */ -.btn-back { - background: rgba(255,255,255,0.15); - color: white; - border: 1px solid rgba(255,255,255,0.3); - padding: 8px 14px; - font-size: 0.85rem; - border-radius: 50px; - white-space: nowrap; - flex-shrink: 0; -} - -.btn-back:hover { - background: rgba(255,255,255,0.28); - transform: none; - box-shadow: none; -} - -/* Patient sub-header */ -.patient-header { - margin-top: 14px; - padding-top: 14px; - border-top: 1px solid rgba(255,255,255,0.15); -} - -.patient-header-name { - font-size: 1.15rem; - font-weight: 700; - color: white; -} - -.patient-header-meta { - font-size: 0.82rem; - color: rgba(255,255,255,0.7); - margin-top: 2px; -} - -.header-actions { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 8px; -} - -@media (min-width: 768px) { - .header-actions { - display: flex; - flex-direction: row; - gap: 12px; - align-items: center; - } -} - -.client-filter { - grid-column: span 2; - padding: 10px 14px; - border: none; - border-radius: var(--radius-sm); - font-size: 0.9rem; - font-family: inherit; - background: rgba(255,255,255,0.95); - color: var(--dark-color); - cursor: pointer; - width: 100%; - font-weight: 500; - box-shadow: 0 2px 8px rgba(0,0,0,0.1); -} - -@media (min-width: 768px) { - .client-filter { - min-width: 220px; - width: auto; - grid-column: auto; - padding: 12px 18px; - font-size: 1rem; - } -} - -.client-filter:focus { - outline: 3px solid rgba(255,255,255,0.4); -} - -.search-input { - grid-column: span 2; - padding: 10px 14px; - border: none; - border-radius: var(--radius-sm); - font-size: 0.9rem; - font-family: inherit; - background: rgba(255,255,255,0.95); - width: 100%; - box-shadow: 0 2px 8px rgba(0,0,0,0.1); -} - -@media (min-width: 768px) { - .search-input { - flex: 1; - grid-column: auto; - min-width: 220px; - padding: 12px 18px; - font-size: 1rem; - } -} - -.search-input:focus { - outline: 3px solid rgba(255,255,255,0.4); -} - -/* ================================================================ - BUTTONS - ================================================================ */ - -.btn { - padding: 10px 16px; - border: none; - border-radius: var(--radius-sm); - font-size: 0.875rem; - font-weight: 600; - font-family: inherit; - cursor: pointer; - transition: all 0.2s ease; - white-space: nowrap; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 6px; - touch-action: manipulation; -} - -@media (min-width: 768px) { - .btn { - padding: 12px 22px; - font-size: 0.95rem; - } -} - -.btn:hover { - transform: translateY(-1px); - box-shadow: var(--shadow); -} - -.btn:active { - transform: translateY(0); -} - -.btn-primary { - background: var(--primary-color); - color: white; -} - -.btn-primary:hover { - background: var(--primary-dark); -} - -.btn-secondary { - background: rgba(255,255,255,0.2); - color: white; - backdrop-filter: blur(10px); - border: 1px solid rgba(255,255,255,0.3); -} - -.btn-secondary:hover { - background: rgba(255,255,255,0.3); -} - -.btn-success { - background: var(--success-color); - color: white; -} - -.btn-success:hover { - background: #43b558; -} - -.btn-danger { - background: var(--danger-color); - color: white; -} - -.btn-danger:hover { - background: #f03e3e; -} - -.btn-info { - background: var(--info-color); - color: white; - text-decoration: none; -} - -.btn-info:hover { - background: #228be6; -} - -.btn-block { - width: 100%; - margin-top: 10px; -} - -.btn-small { - padding: 7px 12px; - font-size: 0.8rem; -} - -/* ================================================================ - IMAGES GRID - ================================================================ */ - -.images-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 12px; - margin-bottom: 40px; -} - -@media (min-width: 480px) { - .images-grid { - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 14px; - } -} - -@media (min-width: 768px) { - .images-grid { - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 20px; - } -} - -/* ================================================================ - IMAGE CARD - ================================================================ */ - -.image-card { - background: var(--card-bg); - border-radius: var(--radius); - overflow: hidden; - box-shadow: var(--shadow); - transition: transform 0.2s ease, box-shadow 0.2s ease; - cursor: pointer; - position: relative; - border: 1px solid var(--border-color); - display: flex; - flex-direction: column; -} - -.image-card:hover { - transform: translateY(-3px); - box-shadow: var(--shadow-lg); -} - -.image-card:active { - transform: translateY(-1px); -} - -.image-card.disabled { - opacity: 0.65; - border: 2px dashed var(--warning-color); -} - -.image-preview { - width: 100%; - height: 160px; - min-height: 160px; - object-fit: cover; - background: linear-gradient(135deg, #e2e8f0, #cbd5e0); - flex-shrink: 0; -} - -@media (min-width: 480px) { - .image-preview { - height: 200px; - min-height: 200px; - } -} - -@media (min-width: 768px) { - .image-preview { - height: 240px; - min-height: 240px; - } -} - -.image-info { - padding: 12px; -} - -@media (min-width: 768px) { - .image-info { - padding: 15px; - } -} - -.image-patient-name { - font-weight: 700; - margin-bottom: 3px; - font-size: 0.95rem; - color: var(--dark-color); - line-height: 1.3; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -@media (min-width: 768px) { - .image-patient-name { - font-size: 1.05rem; - } -} - -.image-guid { - font-size: 0.72rem; - color: var(--text-secondary); - margin-bottom: 8px; - word-break: break-all; - line-height: 1.4; - /* Show only 2 lines */ - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.image-meta { - display: flex; - justify-content: space-between; - font-size: 0.78rem; - color: #888; - flex-wrap: wrap; - gap: 4px; -} - -.image-doctor-remark { - margin-top: 6px; - padding-top: 6px; - border-top: 1px dashed #edf2f7; - font-size: 0.75rem; - color: #4a5568; - line-height: 1.5; -} - -.image-doctor-remark div { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.image-actions { - display: flex; - gap: 6px; - margin-top: 10px; - flex-wrap: wrap; -} - -.image-actions .btn { - flex: 1; - min-width: 0; - padding: 7px 8px; - font-size: 0.75rem; -} - -@media (min-width: 480px) { - .image-actions .btn { - font-size: 0.8rem; - padding: 8px 10px; - } -} - -.image-badge { - position: absolute; - top: 8px; - right: 8px; - background: var(--danger-color); - color: white; - padding: 4px 8px; - border-radius: 20px; - font-size: 0.72rem; - font-weight: 700; - letter-spacing: 0.3px; -} - -/* Patient count badge (in thumbnail overlay) */ -.patient-count-badge { - position: absolute; - bottom: 10px; - left: 10px; - background: rgba(0,0,0,0.65); - color: white; - padding: 4px 10px; - border-radius: 20px; - font-size: 0.78rem; - font-weight: 600; - backdrop-filter: blur(4px); -} - -/* Patient card image-preview needs relative positioning */ -.patient-card .image-preview { - position: relative; -} - -/* Subtle scale on patient card click */ -.patient-card:active { - transform: scale(0.98); -} - -/* ================================================================ - LOADING & EMPTY STATES - ================================================================ */ - -.loading { - text-align: center; - padding: 60px 20px; -} - -.spinner { - border: 3px solid var(--border-color); - border-top: 3px solid var(--primary-color); - border-radius: 50%; - width: 44px; - height: 44px; - animation: spin 0.8s linear infinite; - margin: 0 auto 20px; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -.empty-state { - text-align: center; - padding: 60px 20px; -} - -.empty-icon { - font-size: 3.5rem; - margin-bottom: 16px; -} - -/* ================================================================ - MODAL - ================================================================ */ - -.modal { - display: none; - position: fixed; - z-index: 1000; - left: 0; - top: 0; - width: 100%; - height: 100%; - background: rgba(0,0,0,0.65); - backdrop-filter: blur(4px); - overflow-y: auto; - -webkit-overflow-scrolling: touch; -} - -.modal.active { - display: flex; - align-items: center; - justify-content: center; - padding: 0; -} - -.modal-content { - background: white; - border-radius: 0; - width: 100vw; - height: 100vh; - max-height: 100vh; - max-width: 100vw; - overflow-y: auto; - -webkit-overflow-scrolling: touch; - box-shadow: none; - animation: slideUp 0.3s ease; - display: flex; - flex-direction: column; -} - -.modal-large { - width: 90vw; - height: 98vh; - max-width: 90vw; - max-height: 98vh; - border-radius: 16px; - box-shadow: 0 24px 80px rgba(0,0,0,0.45); - animation: modalAppear 0.25s ease; -} - -@keyframes modalAppear { - from { opacity: 0; transform: scale(0.95); } - to { opacity: 1; transform: scale(1); } -} - -@keyframes slideUp { - from { transform: translateY(100%); } - to { transform: translateY(0); } -} - -@keyframes slideDown { - from { opacity: 0; transform: translateY(-30px); } - to { opacity: 1; transform: translateY(0); } -} - -.modal-header { - padding: 18px 20px; - border-bottom: 1px solid var(--border-color); - display: flex; - justify-content: space-between; - align-items: center; - position: sticky; - top: 0; - background: white; - z-index: 1; -} - -.modal-header h2 { - font-size: 1.1rem; - color: var(--dark-color); - font-weight: 700; -} - -@media (min-width: 600px) { - .modal-header { - padding: 24px 28px; - } - .modal-header h2 { - font-size: 1.4rem; - } -} - -.modal-close { - font-size: 1.8rem; - cursor: pointer; - color: #888; - transition: color 0.2s; - line-height: 1; - padding: 4px; - border-radius: 50%; - width: 36px; - height: 36px; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; -} - -.modal-close:hover { - color: var(--danger-color); - background: #fff0f0; -} - -.modal-body { - padding: 20px; - flex: 1; - display: flex; - flex-direction: column; - overflow-y: auto; -} - -@media (min-width: 600px) { - .modal-body { - padding: 28px; - } -} - -/* ================================================================ - TRANSFORM GRID — Adaptativo por orientação da imagem - ================================================================ - - HORIZONTAL (original paisagem): - - Original → coluna 1, 2 linhas - - Flip H + Flip V → coluna 2, empilhados (um sobre o outro) - - +90° e -90° → colunas 3 e 4, lado a lado - - VERTICAL (original retrato): - - Original → coluna 1, 2 linhas - - Flip H + Flip V → colunas 2 e 3, lado a lado - - +90° e -90° → coluna 4, empilhados (um sobre o outro) - ================================================================ */ - -.transform-grid { - display: grid; - gap: 12px; - height: 100%; - align-items: stretch; -} - -/* ---- HORIZONTAL (paisagem) ---- */ -.transform-grid.grid-landscape { - grid-template-columns: 2fr 1.3fr 1fr 1fr; - grid-template-rows: 1fr 1fr; -} -/* Original: col 1, 2 linhas */ -.transform-grid.grid-landscape .transform-option:nth-child(1) { grid-column: 1; grid-row: 1 / span 2; } -/* Flip H: col 2, linha 1 */ -.transform-grid.grid-landscape .transform-option:nth-child(2) { grid-column: 2; grid-row: 1; } -/* Flip V: col 2, linha 2 (empilhado sob Flip H) */ -.transform-grid.grid-landscape .transform-option:nth-child(3) { grid-column: 2; grid-row: 2; } -/* +90°: col 3, 2 linhas (lado a lado com -90°) */ -.transform-grid.grid-landscape .transform-option:nth-child(4) { grid-column: 3; grid-row: 1 / span 2; } -/* -90°: col 4, 2 linhas (lado a lado com +90°) */ -.transform-grid.grid-landscape .transform-option:nth-child(5) { grid-column: 4; grid-row: 1 / span 2; } - -/* ---- VERTICAL (retrato) ---- */ -.transform-grid.grid-portrait { - grid-template-columns: 1fr 1fr 1fr 1.6fr; - grid-template-rows: 1fr 1fr; -} -/* Original: col 1, 2 linhas */ -.transform-grid.grid-portrait .transform-option:nth-child(1) { grid-column: 1; grid-row: 1 / span 2; } -/* Flip H: col 2, 2 linhas (lado a lado com Flip V) */ -.transform-grid.grid-portrait .transform-option:nth-child(2) { grid-column: 2; grid-row: 1 / span 2; } -/* Flip V: col 3, 2 linhas (lado a lado com Flip H) */ -.transform-grid.grid-portrait .transform-option:nth-child(3) { grid-column: 3; grid-row: 1 / span 2; } -/* +90°: col 4, linha 1 (empilhado sobre -90°) */ -.transform-grid.grid-portrait .transform-option:nth-child(4) { - grid-column: 4; - grid-row: 1; -} - -/* Modo de Seleção Ativa (Exibe apenas 2 imagens) */ -.selection-mode { - display: flex !important; - justify-content: center; - gap: 30px; - align-items: center; - flex-wrap: wrap; -} -.selection-mode .transform-option { - width: 45%; - max-width: 450px; - grid-column: auto !important; - grid-row: auto !important; -} -.selection-mode .transform-option img { - height: 35vh !important; - max-height: 400px; - object-fit: contain; -} -/* -90°: col 4, linha 2 (empilhado sob +90°) */ -.transform-grid.grid-portrait .transform-option:nth-child(5) { grid-column: 4; grid-row: 2; } - -/* ---- Fallback (sem classe de orientação ainda) ---- */ -.transform-grid:not(.grid-landscape):not(.grid-portrait) { - grid-template-columns: 1fr 1fr 1fr 1fr; - grid-template-rows: 1fr 1fr; -} -.transform-grid:not(.grid-landscape):not(.grid-portrait) .transform-option:nth-child(1) { grid-column: 1; grid-row: 1 / span 2; } -.transform-grid:not(.grid-landscape):not(.grid-portrait) .transform-option:nth-child(2) { grid-column: 2; grid-row: 1 / span 2; } -.transform-grid:not(.grid-landscape):not(.grid-portrait) .transform-option:nth-child(3) { grid-column: 3; grid-row: 1 / span 2; } -.transform-grid:not(.grid-landscape):not(.grid-portrait) .transform-option:nth-child(4) { grid-column: 4; grid-row: 1; } -.transform-grid:not(.grid-landscape):not(.grid-portrait) .transform-option:nth-child(5) { grid-column: 4; grid-row: 2; } - -.transform-option { - border: 2px solid var(--border-color); - border-radius: var(--radius-sm); - padding: 10px; - cursor: pointer; - transition: all 0.2s ease; - text-align: center; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - min-height: 0; - overflow: hidden; -} - -.transform-option:hover { - border-color: var(--primary-color); - transform: scale(1.02); -} - -.transform-option.selected { - border-color: var(--success-color); - background: rgba(81, 207, 102, 0.08); -} - -.transform-option img { - max-width: 100%; - flex: 1; - min-height: 0; - object-fit: contain; - border-radius: 4px; - margin-bottom: 10px; -} - -.transform-name { - font-size: 0.9rem; - font-weight: 600; - color: var(--dark-color); - flex-shrink: 0; -} - - - -/* ================================================================ - FORMS - ================================================================ */ - -.form-group { - margin-bottom: 18px; -} - -.form-group label { - display: block; - margin-bottom: 7px; - font-weight: 600; - font-size: 0.9rem; - color: var(--dark-color); -} - -.form-group input, -.form-group select { - width: 100%; - padding: 12px 14px; - border: 2px solid var(--border-color); - border-radius: var(--radius-sm); - font-size: 1rem; - font-family: inherit; - transition: border-color 0.2s; - background: white; - -webkit-appearance: none; - appearance: none; -} - -.form-group input:focus, -.form-group select:focus { - outline: none; - border-color: var(--primary-color); - box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15); -} - -.form-group small { - display: block; - margin-top: 5px; - color: var(--text-secondary); - font-size: 0.8rem; -} - -.form-actions { - margin-top: 20px; -} - -/* ================================================================ - TOAST NOTIFICATION - ================================================================ */ - -.toast { - position: fixed; - bottom: 20px; - left: 50%; - transform: translateX(-50%) translateY(120px); - padding: 13px 22px; - border-radius: 50px; - color: white; - font-weight: 600; - font-size: 0.9rem; - box-shadow: 0 8px 24px rgba(0,0,0,0.2); - opacity: 0; - transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); - z-index: 2000; - white-space: nowrap; - max-width: calc(100vw - 32px); - text-align: center; -} - -@media (min-width: 600px) { - .toast { - bottom: 30px; - right: 30px; - left: auto; - transform: translateX(0) translateY(120px); - border-radius: 10px; - } -} - -.toast.show { - transform: translateX(-50%) translateY(0); - opacity: 1; -} - -@media (min-width: 600px) { - .toast.show { - transform: translateX(0) translateY(0); - } -} - -.toast.success { background: var(--success-color); } -.toast.error { background: var(--danger-color); } -.toast.info { background: var(--primary-color); } - -/* ================================================================ - SIDEBAR LAYOUT - ================================================================ */ - -.app-container { - display: flex; - height: 100vh; - width: 100vw; - overflow: hidden; - background-color: var(--bg-color); -} - -.sidebar { - width: 250px; - background: #fff; - border-right: 1px solid var(--border-color); - display: flex; - flex-direction: column; - flex-shrink: 0; - transition: all 0.3s ease; - z-index: 10; -} - -.sidebar-header { - padding: 20px; - border-bottom: 1px solid rgba(255,255,255,0.15); - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -} - -.sidebar-header h2 { - margin: 0; - font-size: 1.2rem; - color: #fff; - display: flex; - align-items: center; - gap: 8px; -} - -.sidebar-nav { - flex: 1; - padding: 12px 0; - overflow-y: auto; -} - -.nav-item { - display: flex; - align-items: center; - padding: 12px 20px; - color: var(--text-color); - text-decoration: none; - font-weight: 500; - transition: all 0.2s; - gap: 10px; - border-left: 3px solid transparent; -} - -.nav-item .icon { - font-size: 1.1rem; - flex-shrink: 0; -} - -.nav-item:hover { - background-color: rgba(102, 126, 234, 0.08); - color: var(--primary-color); - border-left-color: var(--primary-color); -} - -.nav-item.active { - background-color: rgba(102, 126, 234, 0.12); - color: var(--primary-color); - border-left-color: var(--primary-color); - font-weight: 600; -} - -.nav-divider { - height: 1px; - background: var(--border-color); - margin: 10px 0; -} - -/* --------------------------------------------------------------- - MAIN CONTENT — header fixo + área interna rolável - --------------------------------------------------------------- */ -.main-content { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - min-width: 0; -} - -.main-content .header { - border-radius: 0; - margin: 0; - border-bottom: 1px solid rgba(255,255,255,0.2); - box-shadow: none; - flex-shrink: 0; -} - -/* Wrapper rolável que envolve loading/empty/grid */ -.content-scroll { - flex: 1; - overflow-y: auto; - overflow-x: hidden; - background: var(--bg-color); -} - -/* Compatibilidade */ -.app-container .container { display: none; } - -/* Header padding */ -.header-content { padding: 14px 20px; } - -/* --------------------------------------------------------------- - IMAGES GRID — grid puro sem flex tricks - --------------------------------------------------------------- */ -.app-container #imagesGrid { - display: grid !important; - grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); - gap: 20px; - padding: 24px; - align-content: start; -} - -@media (min-width: 1400px) { - .app-container #imagesGrid { - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - } -} - -@media (max-width: 900px) { - .app-container #imagesGrid { - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 14px; - padding: 16px; - } -} - -/* Image preview — altura fixa garantida */ -.app-container .image-preview { - height: 200px !important; - min-height: 200px !important; - flex-shrink: 0 !important; - background-size: cover !important; - background-position: center !important; -} - -/* --------------------------------------------------------------- - RESPONSIVE SIDEBAR - --------------------------------------------------------------- */ -@media (max-width: 768px) { - .sidebar { width: 60px; } - .sidebar-header { padding: 14px 10px; } - .sidebar-header h2 span:not(.icon) { display: none; } - .nav-item span:not(.icon) { display: none; } - .nav-item { justify-content: center; padding: 14px 0; gap: 0; } - .nav-item .icon { margin-right: 0; font-size: 1.3rem; } - #toggleDisabledBtn { font-size: 0; padding: 10px; } - #toggleDisabledBtn::before { content: '👁'; font-size: 1.1rem; } -} - -/* ================================================================ - SYNC & UPLOAD MODAL STYLES - ================================================================ */ - -.sync-tabs { - display: flex; - border-bottom: 2px solid var(--border-color); - margin-bottom: 20px; - gap: 10px; -} - -.sync-tab-btn { - padding: 12px 20px; - background: none; - border: none; - border-bottom: 3px solid transparent; - font-size: 1rem; - font-weight: 600; - color: var(--text-secondary); - cursor: pointer; - transition: all 0.25s ease; -} - -.sync-tab-btn:hover { - color: var(--primary-color); -} - -.sync-tab-btn.active { - color: var(--primary-color); - border-bottom-color: var(--primary-color); -} - -.sync-tab-content { - display: none; - animation: slideDown 0.25s ease; -} - -.sync-tab-content.active { - display: block; -} - -/* Área de Drag & Drop */ -.drag-drop-zone { - border: 3px dashed var(--border-color); - border-radius: var(--radius); - padding: 40px 20px; - text-align: center; - background: #fdfdfd; - cursor: pointer; - transition: all 0.2s ease; - margin-bottom: 20px; -} - -.drag-drop-zone:hover, -.drag-drop-zone.drag-active { - border-color: var(--primary-color); - background: rgba(102, 126, 234, 0.04); -} - -.drag-drop-icon { - font-size: 3rem; - margin-bottom: 12px; -} - -.drag-drop-text { - font-size: 1rem; - color: var(--text-color); - font-weight: 500; - margin-bottom: 6px; -} - -.drag-drop-subtext { - font-size: 0.82rem; - color: var(--text-secondary); -} - -/* Preview de arquivos selecionados */ -.upload-previews { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); - gap: 10px; - margin-bottom: 20px; -} - -.preview-item { - position: relative; - aspect-ratio: 1; - border-radius: 6px; - overflow: hidden; - border: 1px solid var(--border-color); -} - -.preview-item img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.preview-remove { - position: absolute; - top: 4px; - right: 4px; - background: rgba(220, 53, 69, 0.85); - color: white; - border: none; - border-radius: 50%; - width: 18px; - height: 18px; - font-size: 10px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - line-height: 1; -} - -.preview-remove:hover { - background: #dc3545; -} - -/* Barra de Progresso */ -.upload-progress-container { - background: #f8f9fa; - border-radius: 8px; - padding: 15px; - margin-bottom: 20px; - border: 1px solid var(--border-color); -} - -.progress-bar-label { - display: flex; - justify-content: space-between; - font-size: 0.85rem; - font-weight: 600; - margin-bottom: 8px; - color: var(--dark-color); -} - -.progress-bar-track { - background: #e2e8f0; - border-radius: 10px; - height: 8px; - overflow: hidden; -} - -.progress-bar-fill { - background: linear-gradient(90deg, var(--primary-color) 0%, #764ba2 100%); - height: 100%; - width: 0%; - transition: width 0.2s ease; -} - -/* Lista de dispositivos online */ -.sync-device-list { - display: flex; - flex-direction: column; - gap: 12px; - margin: 20px 0; -} - -.sync-device-card { - background: #f8f9fa; - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - padding: 15px; - display: flex; - justify-content: space-between; - align-items: center; - transition: all 0.2s ease; -} - -.sync-device-card:hover { - border-color: var(--primary-color); - background: #fff; - box-shadow: 0 4px 12px rgba(0,0,0,0.03); -} - -.device-card-left { - display: flex; - flex-direction: column; - gap: 4px; -} - -.device-card-title { - font-weight: 700; - font-size: 0.95rem; - color: var(--dark-color); -} - -.device-card-subtitle { - font-size: 0.78rem; - color: var(--text-secondary); -} - -.device-card-badge { - padding: 5px 12px; - border-radius: 20px; - font-size: 0.72rem; - font-weight: 700; -} - -.badge-connected { - background: rgba(81, 207, 102, 0.15); - color: var(--success-color); -} - -.badge-disconnected { - background: rgba(220, 53, 69, 0.1); - color: var(--danger-color); -} - -/* Botão de Excluir Paciente nos cards iniciais */ -.delete-patient-btn { - position: absolute; - top: 10px; - right: 10px; - background: rgba(220, 53, 69, 0.85); - color: white; - border: none; - border-radius: 50%; - width: 34px; - height: 34px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - z-index: 5; - transition: all 0.2s ease; - font-size: 1.05rem; - box-shadow: 0 2px 6px rgba(0,0,0,0.25); -} - -.delete-patient-btn:hover { - background: #dc3545; - transform: scale(1.1); -}