diff --git a/dental-server/public/app.js b/dental-server/public/app.js index f30dd25..f706ea7 100644 --- a/dental-server/public/app.js +++ b/dental-server/public/app.js @@ -264,14 +264,14 @@ function updateClientsDropdown() { // Adiciona o item "Todos os Clientes" const allItem = document.createElement('div'); - allItem.className = `submenu-item${selectedClient === '' ? ' active' : ''}`; - allItem.innerHTML = '📋 Todos os Clientes'; + allItem.className = `nav-item${selectedClient === '' ? ' active' : ''}`; + allItem.innerHTML = `📋 Todos os Clientes`; allItem.onclick = () => selectClientFilter(''); submenu.appendChild(allItem); sorted.forEach(c => { const item = document.createElement('div'); - item.className = `submenu-item${selectedClient === c.name ? ' active' : ''}`; + item.className = `nav-item${selectedClient === c.name ? ' active' : ''}`; let lbl = c.name; if (c.name === 'Upload Web') { lbl = 'Upload Web 💻'; @@ -279,12 +279,25 @@ function updateClientsDropdown() { if (c.status === 'identified') lbl += ' ✅'; else if (c.status === 'historic') lbl += ' 📚'; } - item.textContent = lbl; + item.innerHTML = `🔹 ${escapeHtml(lbl)}`; item.onclick = () => selectClientFilter(c.name); submenu.appendChild(item); }); } +window.toggleClientsSubmenu = function() { + const submenu = document.getElementById('clientsSubmenu'); + const chevron = document.getElementById('clientsChevron'); + if (!submenu) return; + if (submenu.style.display === 'none') { + submenu.style.display = 'flex'; + if (chevron) chevron.style.transform = 'rotate(180deg)'; + } else { + submenu.style.display = 'none'; + if (chevron) chevron.style.transform = 'rotate(0deg)'; + } +} + function selectClientFilter(clientName) { selectedClient = clientName; if (view === 'patients') { @@ -627,8 +640,8 @@ async function showTransformModal(imageId) { { 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);' } + { name: 'Rotação +90°', rotation: 90, flipH: false, flipV: false, css: 'transform: rotate(90deg) scale(0.65);' }, + { name: 'Rotação -90°', rotation: -90, flipH: false, flipV: false, css: 'transform: rotate(-90deg) scale(0.65);' } ]; currentTransformations = transformations; diff --git a/dental-server/public/index.html b/dental-server/public/index.html index 901c006..22dab45 100644 --- a/dental-server/public/index.html +++ b/dental-server/public/index.html @@ -6,7 +6,7 @@ Gerenciador de Imagens Dentais - +
@@ -19,12 +19,23 @@ 🖼️ Pacientes - + + + + + + + Novo Paciente - 🖥️ Dispositivos + 🖥️ Dados do Cliente ⚙️ Configurações @@ -455,6 +466,6 @@ - + diff --git a/dental-server/public/style.css b/dental-server/public/style.css index af6f841..3128f5c 100644 --- a/dental-server/public/style.css +++ b/dental-server/public/style.css @@ -828,9 +828,10 @@ body { /* Evita cortes nas rotações de 90° e -90° causados pelo comportamento de rotação 2D do CSS */ .transform-option:nth-child(4) .transform-image-wrapper img, .transform-option:nth-child(5) .transform-image-wrapper img { - width: auto !important; + max-width: 100% !important; + max-height: 100% !important; + width: 100% !important; height: 100% !important; - aspect-ratio: 1 / 1 !important; object-fit: contain !important; } @@ -1056,7 +1057,11 @@ body { } .sidebar-header { - padding: 20px; + height: 70px !important; + box-sizing: border-box; + display: flex; + align-items: center; + padding: 0 20px !important; border-bottom: 1px solid rgba(255,255,255,0.15); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } @@ -1124,6 +1129,11 @@ body { } .main-content .header { + height: 70px !important; + box-sizing: border-box; + display: flex; + align-items: center; + padding: 0 20px !important; border-radius: 0; margin: 0; border-bottom: 1px solid rgba(255,255,255,0.2); @@ -1143,7 +1153,14 @@ body { .app-container .container { display: none; } /* Header padding */ -.header-content { padding: 14px 20px; } +.main-content .header .header-content { + padding: 0 !important; + width: 100%; + margin: 0; + flex-direction: row !important; + justify-content: space-between !important; + align-items: center !important; +} /* --------------------------------------------------------------- IMAGES GRID — grid puro sem flex tricks diff --git a/dental-server/server.js b/dental-server/server.js index b370ded..6a87967 100644 --- a/dental-server/server.js +++ b/dental-server/server.js @@ -41,6 +41,8 @@ const io = socketIo(server, { maxHttpBufferSize: 5e7 // 50MB para suportar raios-x grandes }); +const connectedClients = []; + const PORT = process.env.PORT || 3000; const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(__dirname, 'uploads'); const PROCESSED_DIR = process.env.PROCESSED_DIR || path.join(__dirname, 'processed'); @@ -72,6 +74,43 @@ app.get('/status', (req, res) => { }); }); +// Verificar se o nome do computador/cliente já existe +app.get('/api/clients/check-name', async (req, res) => { + try { + const name = (req.query.name || '').trim(); + const token = req.query.token || req.headers['x-api-key'] || req.headers['authorization']?.split(' ')[1]; + const clientId = req.query.clientId || ''; + + const serverApiKey = process.env.CLIENT_API_KEY || 'rf-dental-secure-key-2026'; + if (token !== serverApiKey) { + return res.status(401).json({ success: false, error: 'Chave de API inválida' }); + } + + if (!name) { + return res.status(400).json({ success: false, error: 'Nome não fornecido' }); + } + + // 1. Verificar se existe no banco de dados na tabela images (busca case-insensitive) + const row = await db.get('SELECT COUNT(*) as count FROM images WHERE LOWER(client_name) = LOWER(?)', [name]); + const existsInDb = row && row.count > 0; + + // 2. Verificar se existe algum cliente atualmente conectado com esse nome (desconsiderando o próprio clientId) + const existsConnected = connectedClients.some(c => + c.name.toLowerCase() === name.toLowerCase() && + (!clientId || c.clientId !== clientId) + ); + + if (existsInDb || existsConnected) { + return res.json({ success: true, available: false, exists: true }); + } + + res.json({ success: true, available: true, exists: false }); + } catch (error) { + console.error('Erro ao verificar nome do cliente:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + // Debug endpoint for browser logs app.post('/api/debug-log', (req, res) => { console.log('=== BROWSER HTML ==='); @@ -454,7 +493,8 @@ async function checkInstallationMiddleware(req, res, next) { req.path.startsWith('/status') || req.path.startsWith('/socket.io') || req.path.startsWith('/api/auth') || - req.path.startsWith('/api/install')) { + req.path.startsWith('/api/install') || + req.path.startsWith('/api/clients/check-name')) { return next(); } @@ -551,8 +591,6 @@ io.use((socket, next) => { } }); -const connectedClients = []; - io.on('connection', (socket) => { console.log('📱 Cliente conectado:', socket.id); console.log('📱 Socket transport:', socket.conn.transport.name); @@ -587,18 +625,56 @@ io.on('connection', (socket) => { console.log('👤 Cliente identificado:', data?.type || 'SEM TYPE', data?.name || 'SEM NAME'); console.log('👤 Dados completos:', JSON.stringify(data, null, 2)); - if (!data || !data.type) { + if (!data || !data.type || !data.name) { console.error('❌ [client-identify] Dados inválidos:', data); socket.emit('error', { message: 'Dados de identificação inválidos' }); return; } + const clientName = data.name.trim(); + const clientId = data.clientId || ''; + + // 1. Validar unicidade em relação aos clientes já conectados + const duplicate = connectedClients.find(c => + c.name.toLowerCase() === clientName.toLowerCase() && + c.clientId !== clientId + ); + + if (duplicate) { + console.log(`❌ [client-identify] Conexão rejeitada: O nome "${clientName}" já está em uso por outro dispositivo.`); + socket.emit('error', { + message: `O nome "${clientName}" já está sendo usado por outro computador neste servidor. Escolha um nome exclusivo.` + }); + setTimeout(() => { + socket.disconnect(true); + }, 500); + return; + } + + // 2. Se for uma reconexão do mesmo clientId, desconectar e limpar os sockets antigos do mesmo cliente + const oldConnections = connectedClients.filter(c => + c.clientId === clientId && c.socketId !== socket.id + ); + for (const old of oldConnections) { + console.log(`🔄 Desconectando socket antigo ${old.socketId} para o mesmo clientId ${clientId}`); + const oldSocket = io.sockets.sockets.get(old.socketId); + if (oldSocket) { + oldSocket.emit('error', { message: 'Nova conexão estabelecida deste computador.' }); + oldSocket.disconnect(true); + } + const idx = connectedClients.indexOf(old); + if (idx > -1) { + connectedClients.splice(idx, 1); + } + } + const clientInfo = { socketId: socket.id, type: data.type || 'unknown', - name: data.name || 'Unknown', + name: clientName, version: data.version || '1.0.0', system: data.system || 'unknown', + clientId: clientId, connectedAt: new Date().toISOString() };