diff --git a/dental-server-deploy.tar.gz b/dental-server-deploy.tar.gz new file mode 100644 index 0000000..bb9c9e3 Binary files /dev/null and b/dental-server-deploy.tar.gz differ diff --git a/dental-server/auth.js b/dental-server/auth.js index cee2822..d015d0d 100644 --- a/dental-server/auth.js +++ b/dental-server/auth.js @@ -60,14 +60,15 @@ async function verifyPassword(password, hash) { // GERAR TOKEN JWT // ================================================================ -function generateToken(user) { +function generateToken(user, expiresIn = JWT_EXPIRES_IN) { return jwt.sign( { id: user.id, - username: user.username + username: user.username, + is_admin: user.is_admin }, JWT_SECRET, - { expiresIn: JWT_EXPIRES_IN } + { expiresIn: expiresIn } ); } @@ -90,8 +91,8 @@ async function createInitialUser() { // Inserir usuário await db.run( - `INSERT INTO users (username, email, password_hash, created_at) - VALUES (?, ?, ?, NOW())`, + `INSERT INTO users (username, email, password_hash, is_admin, created_at) + VALUES (?, ?, ?, 1, NOW())`, ['rcesar', 'rcesar@rcesar.com', passwordHash] ); diff --git a/dental-server/database.js b/dental-server/database.js index 9a4235c..8da67c8 100644 --- a/dental-server/database.js +++ b/dental-server/database.js @@ -1,4 +1,4 @@ -const { Pool } = require('pg'); +const mysql = require('mysql2/promise'); const fs = require('fs').promises; const path = require('path'); @@ -14,15 +14,18 @@ function rewriteQuery(sql) { // Replace NOW() with datetime('now') query = query.replace(/\bNOW\(\)/gi, "datetime('now')"); + // Rewrite ON DUPLICATE KEY UPDATE for users + if (query.includes('ON DUPLICATE KEY UPDATE')) { + query = `INSERT INTO users (username, email, password_hash, created_at) + VALUES (?, ?, ?, datetime('now')) + ON CONFLICT(username) DO UPDATE SET + email = excluded.email, + password_hash = excluded.password_hash`; + } + return query; } -// Convert MySQL ? to Postgres $1, $2 -function convertSqlForPg(sql) { - let i = 1; - return sql.replace(/\?/g, () => '$' + (i++)); -} - // ================================================================ // INICIALIZAR BANCO DE DADOS // ================================================================ @@ -41,18 +44,13 @@ async function initSQLite() { username TEXT UNIQUE NOT NULL, email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, + clinic_name TEXT, + pc_name TEXT, + is_admin INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); `); - sqliteDb.exec(` - CREATE TABLE IF NOT EXISTS settings ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL - ); - `); - console.log('✅ Tabela settings SQLite criada/verificada'); - sqliteDb.exec(` CREATE TABLE IF NOT EXISTS images ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -71,7 +69,6 @@ async function initSQLite() { flip_vertical INTEGER DEFAULT 0, doctor TEXT, remark TEXT, - thumb_filename TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); `); @@ -100,16 +97,24 @@ async function initSQLite() { // Migrações para adicionar colunas se o banco já existia try { sqliteDb.exec("ALTER TABLE images ADD COLUMN doctor TEXT"); - } catch (e) {} + } catch (e) { + // Coluna já existe + } + try { sqliteDb.exec("ALTER TABLE images ADD COLUMN remark TEXT"); - } catch (e) {} - try { - sqliteDb.exec("ALTER TABLE images ADD COLUMN thumb_filename TEXT"); - } catch (e) {} + } catch (e) { + // Coluna já existe + } console.log('✅ Tabelas SQLite users e images criadas/verificadas'); + // Migrações para adicionar colunas em users se o banco já existia + try { sqliteDb.exec("ALTER TABLE users ADD COLUMN clinic_name TEXT"); } catch (e) {} + try { sqliteDb.exec("ALTER TABLE users ADD COLUMN pc_name TEXT"); } catch (e) {} + try { sqliteDb.exec("ALTER TABLE users ADD COLUMN is_admin INTEGER DEFAULT 0"); } catch (e) {} + + // Criar administrador padrão se não houver usuários try { const stmtCount = sqliteDb.prepare('SELECT COUNT(*) as count FROM users'); const userCount = stmtCount.get().count; @@ -118,10 +123,10 @@ async function initSQLite() { const bcrypt = require('bcrypt'); const passwordHash = bcrypt.hashSync('admin1234', 10); const stmtInsert = sqliteDb.prepare(` - INSERT INTO users (username, email, password_hash) - VALUES (?, ?, ?) + INSERT INTO users (username, email, password_hash, clinic_name, pc_name, is_admin) + VALUES (?, ?, ?, ?, ?, 1) `); - stmtInsert.run('admin', 'admin@dental.local', passwordHash); + stmtInsert.run('admin', 'admin@dental.local', passwordHash, 'Admin Clinic', 'Admin PC'); console.log('✅ Usuário admin padrão criado com sucesso!'); } } catch (error) { @@ -138,185 +143,250 @@ async function initDatabase() { try { const dbConfig = { host: process.env.DB_HOST || 'localhost', - port: process.env.DB_PORT || 5432, - user: process.env.DB_USER || 'postgres', + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER || 'root', password: process.env.DB_PASSWORD || '', }; const dbName = process.env.DB_NAME || 'dental_images'; - const tempPool = new Pool({ + // Criar pool temporário SEM database para poder criar o banco + const tempPool = mysql.createPool({ ...dbConfig, - database: 'postgres', + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 }); + console.log('✅ Pool de conexões MySQL criado'); + + // Verificar se banco existe, se não criar try { - const res = await tempPool.query(`SELECT 1 FROM pg_database WHERE datname = $1`, [dbName]); - if (res.rowCount === 0) { + const [databases] = await tempPool.query( + `SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?`, + [dbName] + ); + + if (databases.length === 0) { console.log(`⚠️ Banco ${dbName} não encontrado. Criando...`); - await tempPool.query(`CREATE DATABASE "${dbName}"`); + await tempPool.query(`CREATE DATABASE ${dbName} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`); console.log(`✅ Banco ${dbName} criado!`); } else { console.log(`✅ Banco ${dbName} já existe`); } } catch (error) { - console.error('❌ Erro ao verificar/criar banco no PostgreSQL:', error.message); - } finally { - await tempPool.end(); + console.error('❌ Erro ao verificar/criar banco:', error); + throw error; } - pool = new Pool({ + // Fechar pool temporário + await tempPool.end(); + + // Criar pool definitivo COM database + pool = mysql.createPool({ ...dbConfig, database: dbName, + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0, + enableKeepAlive: true, + keepAliveInitialDelay: 0 }); - const client = await pool.connect(); - console.log('✅ Conectado ao PostgreSQL'); - client.release(); + // Testar conexão + const connection = await pool.getConnection(); + console.log('✅ Conectado ao MySQL'); + connection.release(); + // Criar tabelas await createTables(); } catch (error) { - console.error('❌ Erro ao conectar ao PostgreSQL:', error.message); + console.error('❌ Erro ao conectar ao MySQL:', error.message); + console.error('Verifique:'); + console.error('1. MySQL está rodando? (sudo systemctl status mysql)'); + console.error('2. Credenciais corretas no .env?'); + console.error('3. Usuário tem permissão para criar databases?'); throw error; } } // ================================================================ -// CRIAR TABELAS POSTGRESQL +// CRIAR TABELAS MYSQL // ================================================================ async function createTables() { - const client = await pool.connect(); + const connection = await pool.getConnection(); try { // Tabela de usuários - await client.query(` + await connection.query(` CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY, + id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, email VARCHAR(100) UNIQUE NOT NULL, password_hash TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) + clinic_name VARCHAR(100) NULL, + pc_name VARCHAR(50) NULL, + is_admin TINYINT(1) DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); - - // Tabela de configurações - await client.query(` - CREATE TABLE IF NOT EXISTS settings ( - key VARCHAR(100) PRIMARY KEY, - value TEXT NOT NULL - ) - `); - console.log('✅ Tabela settings PostgreSQL criada/verificada'); console.log('✅ Tabela users criada/verificada'); // Tabela de imagens - await client.query(` + await connection.query(` CREATE TABLE IF NOT EXISTS images ( - id SERIAL PRIMARY KEY, + id INT AUTO_INCREMENT PRIMARY KEY, image_guid VARCHAR(255) NOT NULL, patient_name TEXT, client_name VARCHAR(100), original_filename TEXT, filename TEXT NOT NULL, - enabled SMALLINT DEFAULT 1, + enabled TINYINT(1) DEFAULT 1, tooth_number VARCHAR(10), tooth_side VARCHAR(10), patient_name_resumed VARCHAR(50), original_image_id INT, rotation INT DEFAULT 0, - flip_horizontal SMALLINT DEFAULT 0, - flip_vertical SMALLINT DEFAULT 0, + flip_horizontal INT DEFAULT 0, + flip_vertical INT DEFAULT 0, doctor VARCHAR(255) NULL, remark TEXT NULL, - thumb_filename VARCHAR(500) NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_images_enabled (enabled), + INDEX idx_images_original (original_image_id), + INDEX idx_patient_name (patient_name(100)), + INDEX idx_image_guid (image_guid), + INDEX idx_client_name (client_name), + INDEX idx_created_at (created_at) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); - - await client.query(`CREATE INDEX IF NOT EXISTS idx_images_enabled ON images (enabled)`); - await client.query(`CREATE INDEX IF NOT EXISTS idx_images_original ON images (original_image_id)`); - await client.query(`CREATE INDEX IF NOT EXISTS idx_patient_name ON images (patient_name)`); - await client.query(`CREATE INDEX IF NOT EXISTS idx_image_guid ON images (image_guid)`); - await client.query(`CREATE INDEX IF NOT EXISTS idx_client_name ON images (client_name)`); - await client.query(`CREATE INDEX IF NOT EXISTS idx_created_at ON images (created_at)`); - console.log('✅ Tabela images criada/verificada'); // Tabela GTOs - await client.query(` + await connection.query(` CREATE TABLE IF NOT EXISTS gtos ( - id SERIAL PRIMARY KEY, + id INT AUTO_INCREMENT PRIMARY KEY, gto_number VARCHAR(100) NOT NULL, patient_name VARCHAR(255) NOT NULL, description TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_gtos_patient (patient_name) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); - await client.query(`CREATE INDEX IF NOT EXISTS idx_gtos_patient ON gtos (patient_name)`); console.log('✅ Tabela gtos criada/verificada'); // Tabela de vínculo GTO-Images - await client.query(` + await connection.query(` CREATE TABLE IF NOT EXISTS gto_images ( - id SERIAL PRIMARY KEY, + id INT AUTO_INCREMENT PRIMARY KEY, gto_id INT NOT NULL, image_id INT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (gto_id) REFERENCES gtos(id) ON DELETE CASCADE, - FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE - ) + FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE, + UNIQUE INDEX idx_gto_image (gto_id, image_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); - await client.query(`CREATE UNIQUE INDEX IF NOT EXISTS idx_gto_image ON gto_images (gto_id, image_id)`); console.log('✅ Tabela gto_images criada/verificada'); + // Verificar e adicionar colunas se não existirem (migração) try { - await client.query(`ALTER TABLE images ADD COLUMN client_name VARCHAR(100)`); - await client.query(`CREATE INDEX idx_client_name ON images (client_name)`); - console.log('✅ Coluna client_name adicionada'); - } catch (e) { /* já existe */ } - - try { - await client.query(`ALTER TABLE images ADD COLUMN doctor VARCHAR(255)`); - } catch (e) { /* já existe */ } - - try { - await client.query(`ALTER TABLE images ADD COLUMN remark TEXT`); - } catch (e) { /* já existe */ } - - try { - await client.query(`ALTER TABLE images ADD COLUMN thumb_filename VARCHAR(500)`); - console.log('✅ Coluna thumb_filename adicionada'); - } catch (e) { /* já existe */ } - - // Inserir usuários padrões se a tabela estiver vazia - try { - const userCountRes = await client.query('SELECT COUNT(*) as count FROM users'); - const userCount = parseInt(userCountRes.rows[0].count, 10); - if (userCount === 0) { - console.log('⚠️ Nenhum usuário encontrado no PostgreSQL. Criando usuários padrões...'); - const bcrypt = require('bcrypt'); - const hash1 = await bcrypt.hash('Rc362514', 10); - const hash2 = await bcrypt.hash('admin1234', 10); - - await client.query( - `INSERT INTO users (username, email, password_hash) VALUES ($1, $2, $3)`, - ['rcesar', 'rcesar@rcesar.com', hash1] - ); - await client.query( - `INSERT INTO users (username, email, password_hash) VALUES ($1, $2, $3)`, - ['admin', 'admin@dental.local', hash2] - ); - console.log('✅ Usuários padrões (rcesar e admin) criados com sucesso no PostgreSQL!'); + const columns = await connection.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'images' + AND COLUMN_NAME = 'client_name' + `); + + if (columns[0].length === 0) { + console.log('⚠️ Coluna client_name não encontrada. Adicionando...'); + await connection.query(` + ALTER TABLE images + ADD COLUMN client_name VARCHAR(100) NULL + AFTER patient_name + `); + await connection.query(` + ALTER TABLE images + ADD INDEX idx_client_name (client_name) + `); + console.log('✅ Coluna client_name adicionada'); } - } catch (e) { - console.error('⚠️ Erro ao inicializar usuários padrões no PostgreSQL:', e.message); + } catch (error) { + console.warn('⚠️ Erro ao verificar/adicionar coluna client_name:', error.message); + } + + // Migração para coluna doctor + try { + const columnsDoctor = await connection.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'images' + AND COLUMN_NAME = 'doctor' + `); + if (columnsDoctor[0].length === 0) { + console.log('⚠️ Coluna doctor não encontrada. Adicionando...'); + await connection.query(` + ALTER TABLE images + ADD COLUMN doctor VARCHAR(255) NULL + AFTER client_name + `); + } + } catch (error) { + console.warn('⚠️ Erro ao verificar/adicionar coluna doctor:', error.message); } + // Migração para coluna remark + try { + const columnsRemark = await connection.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'images' + AND COLUMN_NAME = 'remark' + `); + if (columnsRemark[0].length === 0) { + console.log('⚠️ Coluna remark não encontrada. Adicionando...'); + await connection.query(` + ALTER TABLE images + ADD COLUMN remark TEXT NULL + AFTER doctor + `); + } + } catch (error) { + console.warn('⚠️ Erro ao verificar/adicionar coluna remark:', error.message); + } + + // Migração para colunas da tabela users (clinic_name, pc_name, is_admin) + try { + const columnsUsers = await connection.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'users' + AND COLUMN_NAME = 'clinic_name' + `); + if (columnsUsers[0].length === 0) { + console.log('⚠️ Colunas de clínica em users não encontradas. Adicionando...'); + await connection.query(` + ALTER TABLE users + ADD COLUMN clinic_name VARCHAR(100) NULL AFTER password_hash, + ADD COLUMN pc_name VARCHAR(50) NULL AFTER clinic_name, + ADD COLUMN is_admin TINYINT(1) DEFAULT 0 AFTER pc_name + `); + // O primeiro usuário (admin padrão) deve virar admin se já existe + await connection.query(`UPDATE users SET is_admin = 1 ORDER BY id ASC LIMIT 1`); + } + } catch (error) { + console.warn('⚠️ Erro ao verificar/adicionar colunas de clínica em users:', error.message); + } + } catch (error) { console.error('Erro ao criar tabelas:', error); } finally { - client.release(); + connection.release(); } } @@ -339,9 +409,8 @@ function get(query, params = []) { return new Promise(async (resolve, reject) => { try { - const pgQuery = convertSqlForPg(query); - const res = await pool.query(pgQuery, params); - resolve(res.rows[0] || null); + const [rows] = await pool.query(query, params); + resolve(rows[0] || null); } catch (error) { reject(error); } @@ -363,9 +432,8 @@ function all(query, params = []) { return new Promise(async (resolve, reject) => { try { - const pgQuery = convertSqlForPg(query); - const res = await pool.query(pgQuery, params); - resolve(res.rows); + const [rows] = await pool.query(query, params); + resolve(rows); } catch (error) { reject(error); } @@ -390,25 +458,10 @@ function run(query, params = []) { return new Promise(async (resolve, reject) => { try { - let pgQuery = convertSqlForPg(query); - - // Auto-append RETURNING id for INSERT if not present (except for settings table) - if (pgQuery.trim().toUpperCase().startsWith('INSERT') && - !pgQuery.toUpperCase().includes('RETURNING') && - !pgQuery.toUpperCase().includes('SETTINGS')) { - pgQuery += ' RETURNING id'; - } - - const res = await pool.query(pgQuery, params); - - let lastID = null; - if (res.rows && res.rows.length > 0 && res.rows[0].id) { - lastID = res.rows[0].id; - } - + const [result] = await pool.query(query, params); resolve({ - lastID: lastID, - changes: res.rowCount + lastID: result.insertId, + changes: result.affectedRows }); } catch (error) { reject(error); diff --git a/dental-server/package-lock.json b/dental-server/package-lock.json index 8c0bc80..fef42e3 100644 --- a/dental-server/package-lock.json +++ b/dental-server/package-lock.json @@ -16,7 +16,7 @@ "express": "^4.18.2", "express-session": "^1.17.3", "jsonwebtoken": "^9.0.2", - "pg": "^8.11.3", + "mysql2": "^3.6.0", "sharp": "^0.32.6", "socket.io": "^4.7.2" }, @@ -157,6 +157,15 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/b4a": { "version": "1.7.3", "resolved": "https://registry.npmmirror.com/b4a/-/b4a-1.7.3.tgz", @@ -596,6 +605,15 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "license": "MIT" }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", @@ -991,6 +1009,15 @@ "node": ">=10" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1226,6 +1253,12 @@ "node": ">=8" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -1317,6 +1350,36 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/lru.min": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-3.1.0.tgz", @@ -1501,6 +1564,54 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmmirror.com/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz", @@ -1656,122 +1767,6 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, - "node_modules/pg": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", - "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==", - "dependencies": { - "pg-connection-string": "^2.13.0", - "pg-pool": "^3.14.0", - "pg-protocol": "^1.14.0", - "pg-types": "2.2.0", - "pgpass": "1.0.5" - }, - "engines": { - "node": ">= 16.0.0" - }, - "optionalDependencies": { - "pg-cloudflare": "^1.4.0" - }, - "peerDependencies": { - "pg-native": ">=3.0.1" - }, - "peerDependenciesMeta": { - "pg-native": { - "optional": true - } - } - }, - "node_modules/pg-cloudflare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", - "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz", - "integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==" - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-pool": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", - "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz", - "integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pgpass": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", - "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", - "dependencies": { - "split2": "^4.1.0" - } - }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", - "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmmirror.com/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -2024,6 +2019,11 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmmirror.com/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz", @@ -2322,12 +2322,13 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", "engines": { - "node": ">= 10.x" + "node": ">= 0.6" } }, "node_modules/statuses": { @@ -2588,14 +2589,6 @@ } } }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "engines": { - "node": ">=0.4" - } - }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", diff --git a/dental-server/package.json b/dental-server/package.json index 921def9..149b0e0 100644 --- a/dental-server/package.json +++ b/dental-server/package.json @@ -19,15 +19,13 @@ "socket.io": "^4.7.2", "express": "^4.18.2", "sharp": "^0.32.6", - "pg": "^8.11.3", + "mysql2": "^3.6.0", "dotenv": "^16.3.1", "cors": "^2.8.5", "bcrypt": "^5.1.1", "jsonwebtoken": "^9.0.2", "cookie-parser": "^1.4.6", - "express-session": "^1.17.3", - "@aws-sdk/client-s3": "^3.500.0", - "@aws-sdk/lib-storage": "^3.500.0" + "express-session": "^1.17.3" }, "engines": { "node": ">=16.0.0", diff --git a/dental-server/public/app.js b/dental-server/public/app.js index f706ea7..ef08e9c 100644 --- a/dental-server/public/app.js +++ b/dental-server/public/app.js @@ -39,6 +39,7 @@ function getAuthToken() { } let authVerificationInProgress = false; +let currentUser = null; async function verifyAuth() { if (authVerificationInProgress) return false; @@ -52,7 +53,14 @@ async function verifyAuth() { if (!response.ok) { localStorage.removeItem('auth_token'); window.location.href = '/login'; return false; } const data = await response.json(); authVerificationInProgress = false; - if (data.valid) return true; + if (data.valid) { + currentUser = data.user; + if (currentUser && currentUser.is_admin) { + const navAdmin = document.getElementById('navAdminClinics'); + if (navAdmin) navAdmin.style.display = 'flex'; + } + return true; + } localStorage.removeItem('auth_token'); window.location.href = '/login'; return false; } catch { localStorage.removeItem('auth_token'); @@ -98,14 +106,13 @@ let clientsList = []; let selectedClient = ''; let showDisabled = false; let currentTransformations = []; -let fineTuneAngle = 0; // ================================================================ // DOM REFS // ================================================================ const searchInput = document.getElementById('searchInput'); -const clientFilter = document.getElementById('clientFilter'); // Pode ser null se removido do header +const clientFilter = document.getElementById('clientFilter'); const toggleDisabledBtn = document.getElementById('toggleDisabledBtn'); const toggleText = document.getElementById('toggleText'); const imagesGrid = document.getElementById('imagesGrid'); @@ -150,7 +157,6 @@ document.addEventListener('DOMContentLoaded', async () => { await loadClientsList(); loadPatients(); setupEventListeners(); - initDragAndDrop(); setInterval(loadClientsList, 5000); } catch (error) { _log.error('INIT', 'Erro fatal na inicialização:', error.message, error); @@ -170,12 +176,10 @@ function setupEventListeners() { else loadPatientImages(selectedPatient.patient_name); }, 400)); - if (clientFilter) { - clientFilter.addEventListener('change', e => { - selectedClient = e.target.value; - if (view === 'patients') loadPatients(); - }); - } + clientFilter.addEventListener('change', e => { + selectedClient = e.target.value; + if (view === 'patients') loadPatients(); + }); toggleDisabledBtn.addEventListener('click', () => { showDisabled = !showDisabled; @@ -188,14 +192,10 @@ function setupEventListeners() { // Transform modal close on backdrop transformModal.addEventListener('click', e => { - if (e.target === transformModal) { - transformModal.classList.remove('active'); - if (typeof clearTransformSelection === 'function') clearTransformSelection(); - } + if (e.target === transformModal) transformModal.classList.remove('active'); }); document.getElementById('closeTransformModal').addEventListener('click', () => { transformModal.classList.remove('active'); - if (typeof clearTransformSelection === 'function') clearTransformSelection(); }); } @@ -227,85 +227,22 @@ async function loadClientsList() { } function updateClientsDropdown() { - // Se por acaso o elemento clientFilter ainda existir no DOM em alguma outra tela, atualiza ele - if (clientFilter) { - 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; - } - - // Renderizar o submenu dinâmico na sidebar - const submenu = document.getElementById('clientsSubmenu'); - if (!submenu) return; - + 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); }); - - submenu.innerHTML = ''; - - // Adiciona o item "Todos os Clientes" - const allItem = document.createElement('div'); - 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 = `nav-item${selectedClient === c.name ? ' active' : ''}`; + const opt = document.createElement('option'); + opt.value = c.name; let lbl = c.name; - if (c.name === 'Upload Web') { - lbl = 'Upload Web 💻'; - } else { - if (c.status === 'identified') lbl += ' ✅'; - else if (c.status === 'historic') lbl += ' 📚'; - } - item.innerHTML = `🔹 ${escapeHtml(lbl)}`; - item.onclick = () => selectClientFilter(c.name); - submenu.appendChild(item); + if (c.status === 'identified') lbl += ' ✅'; + else if (c.status === 'historic') lbl += ' 📚'; + opt.textContent = lbl; + clientFilter.appendChild(opt); }); -} - -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') { - loadPatients(); - } else { - showPatientsView(); - } - updateClientsDropdown(); + clientFilter.value = current || selectedClient; } // ================================================================ @@ -317,11 +254,6 @@ function showPatientsView() { selectedPatient = null; backBtn.style.display = 'none'; patientHeader.style.display = 'none'; - - // Restaurar título do header - const mainTitle = document.getElementById('mainTitle'); - if (mainTitle) mainTitle.textContent = 'Galeria de Imagens'; - loadPatients(); } @@ -417,7 +349,6 @@ function renderPatientCards() { return `
-
${count} imagem${count !== 1 ? 'ns' : ''}
@@ -442,7 +373,7 @@ function renderPatientCards() { cardW: cs.width, cardH: cs.height, previewH: pcs ? pcs.height : 'n/a', previewBg: pcs ? pcs.backgroundImage.substring(0, 60) : 'n/a' - }); + }); } else { _log.error('RENDER_CARDS', '❌ Nenhum card encontrado no DOM após render!'); } @@ -451,38 +382,6 @@ function renderPatientCards() { }); } -// Exclui todas as imagens de um determinado paciente (S3 e Local) -async function deletePatientImages(patientName) { - if (!patientName) return; - - // TODO(security): Usando confirm() para manter coerência com o restante das confirmações do painel do projeto - const ok = confirm(`⚠️ ATENÇÃO: Tem certeza que deseja excluir permanentemente todas as imagens do paciente "${patientName}"?\n\nEsta ação apagará os arquivos locais, do Wasabi S3 e do banco de dados, e NÃO poderá ser desfeita.`); - if (!ok) return; - - showToast('Excluindo imagens do paciente...', 'info'); - - try { - const res = await fetchWithAuth(`/api/images/by-patient?name=${encodeURIComponent(patientName)}`, { - method: 'DELETE' - }); - - if (!res.ok) { - const err = await res.json(); - throw new Error(err.error || 'Erro ao excluir imagens do paciente.'); - } - - showToast('✅ Todas as imagens do paciente foram excluídas!', 'success'); - - // Atualizar listagens - await loadPatients(); - await loadClientsList(); - - } catch (e) { - console.error('Erro na exclusão do paciente:', e); - showToast(`Falha ao excluir imagens: ${e.message}`, 'error'); - } -} - // ================================================================ // VIEW: PATIENT IMAGES // ================================================================ @@ -491,18 +390,9 @@ function openPatient(patient) { selectedPatient = patient; view = 'patient-images'; backBtn.style.display = 'inline-flex'; - - // Ocultar o patientHeader para manter o header na mesma altura de 70px - patientHeader.style.display = 'none'; + patientHeader.style.display = 'block'; - // Mudar o título principal para o nome do paciente - dentista const fullName = patient.patient_name || 'Sem nome'; - const doctorName = patient.doctor ? ` - ${patient.doctor}` : ''; - const mainTitle = document.getElementById('mainTitle'); - if (mainTitle) { - mainTitle.textContent = `${fullName}${doctorName}`; - } - patientTitle.textContent = fullName; patientMeta.textContent = `${patient.image_count || 0} imagem(ns) · ${patient.doctor || '—'}`; @@ -553,11 +443,9 @@ async function loadPatientImages(patientName) { } function renderPatientImages() { - imagesGrid.innerHTML = patientImages.map(image => { - const thumbUrl = image.thumb_filename ? `/uploads/${image.thumb_filename}` : `/uploads/${image.filename}`; - return ` + imagesGrid.innerHTML = patientImages.map(image => `
-
+
📅 ${formatDate(image.created_at)}
${escapeHtml(image.image_guid || image.filename)}
@@ -571,8 +459,8 @@ function renderPatientImages() {
${!image.enabled ? '
Desabilitada
' : ''} -
`; - }).join(''); +
+ `).join(''); } // ================================================================ @@ -626,32 +514,19 @@ async function showTransformModal(imageId) { `; try { - const image = patientImages.find(img => img.id === imageId); - if (!image) { - throw new Error('Imagem não encontrada localmente'); + const res = await fetchWithAuth(`/api/images/${imageId}/transformations`); + if (!res.ok) { + if (res.status === 401 || res.status === 403) { localStorage.removeItem('auth_token'); window.location.href = '/login'; return; } + throw new Error('Erro ao gerar transformações'); } - - if (typeof clearTransformSelection === 'function') clearTransformSelection(); - - // 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) scale(0.65);' }, - { name: 'Rotação -90°', rotation: -90, flipH: false, flipV: false, css: 'transform: rotate(-90deg) scale(0.65);' } - ]; + const transformations = await res.json(); currentTransformations = transformations; const container = document.getElementById('transformOptions'); container.innerHTML = transformations.map((t, i) => `
-
- ${escapeHtml(t.name)} -
+ ${escapeHtml(t.name)}
${escapeHtml(t.name)}
`).join(''); @@ -659,21 +534,23 @@ async function showTransformModal(imageId) { // --------------------------------------------------------------- // 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; + if (transformations.length > 0 && transformations[0].base64) { + const probe = new Image(); + probe.onload = () => { + const orientation = probe.naturalWidth >= probe.naturalHeight ? 'landscape' : 'portrait'; + container.classList.remove('grid-landscape', 'grid-portrait'); + container.classList.add(`grid-${orientation}`); + _log.info('TRANSFORM', `Orientação detectada: ${orientation} (${probe.naturalWidth}×${probe.naturalHeight})`); + }; + probe.onerror = () => { + _log.warn('TRANSFORM', 'Não foi possível detectar orientação — usando fallback'); + }; + probe.src = transformations[0].base64; + } } catch (e) { _log.error('TRANSFORM', '❌ Exceção:', e.message, e); - showToast('Erro ao carregar variações', 'error'); + showToast('Erro ao gerar variações', 'error'); transformModal.classList.remove('active'); } } @@ -702,24 +579,6 @@ function selectTransform(index) { el.classList.add('selected'); - // Reset fine tune angle when selecting a new transform - fineTuneAngle = 0; - updateFineTuneDisplay(); - - // Mover a legenda do card selecionado para o placeholder de ajuste fino - const nameEl = el.querySelector('.transform-name'); - const placeholder = document.getElementById('fineTuneLabelPlaceholder'); - if (nameEl && placeholder) { - placeholder.appendChild(nameEl); - } - - // Mover o contêiner de ajuste fino para dentro do card selecionado - const ftContainer = document.getElementById('fineTuneContainer'); - if (ftContainer) { - el.appendChild(ftContainer); - ftContainer.style.display = 'flex'; - } - // Show action area document.getElementById('transformActionArea').style.display = 'block'; @@ -734,48 +593,11 @@ function clearTransformSelection() { const container = document.getElementById('transformOptions'); container.classList.remove('selection-mode'); - // Devolver a legenda ativa para o card selecionado - const ftPlaceholder = document.getElementById('fineTuneLabelPlaceholder'); - if (ftPlaceholder) { - const activeLabel = ftPlaceholder.querySelector('.transform-name'); - const selectedEl = document.querySelector('.transform-option.selected'); - if (activeLabel && selectedEl) { - selectedEl.appendChild(activeLabel); - } - } - - // Mover o contêiner de ajuste fino de volta para o Wrapper e esconder - const ftContainer = document.getElementById('fineTuneContainer'); - if (ftContainer) { - const viewWrapper = document.getElementById('transformViewWrapper'); - if (viewWrapper) { - viewWrapper.appendChild(ftContainer); - } else { - document.body.appendChild(ftContainer); - } - ftContainer.style.display = 'none'; - } - - // Restore original transform styles on all images document.querySelectorAll('.transform-option').forEach(option => { option.classList.remove('selected'); option.style.display = 'block'; - - const optIndex = parseInt(option.getAttribute('data-index')); - const transform = currentTransformations[optIndex]; - const imgEl = option.querySelector('img'); - if (imgEl && transform) { - imgEl.style.transform = transform.css; - } - const nameEl = option.querySelector('.transform-name'); - if (nameEl && transform) { - nameEl.innerText = transform.name; - } }); - fineTuneAngle = 0; - updateFineTuneDisplay(); - document.getElementById('transformActionArea').style.display = 'none'; document.getElementById('newImageRemark').value = ''; } @@ -798,7 +620,7 @@ async function saveSelectedTransform() { try { const payload = { - rotation: transform.rotation + fineTuneAngle, + rotation: transform.rotation, flipH: transform.flipH, flipV: transform.flipV, remark: remark @@ -807,12 +629,11 @@ async function saveSelectedTransform() { const res = await fetchWithAuth(`/api/images/${selectedImageId}/transform`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) + body: JSON.stringify({ rotation: transform.rotation, flipH: transform.flipH, flipV: transform.flipV }) }); if (!res.ok) throw new Error('Erro ao salvar'); transformModal.classList.remove('active'); - clearTransformSelection(); showToast('Orientação salva!', 'success'); if (selectedPatient) loadPatientImages(selectedPatient.patient_name); } catch (e) { @@ -821,53 +642,6 @@ async function saveSelectedTransform() { } } -function adjustFineTune(offset) { - const selectedEl = document.querySelector('.transform-option.selected'); - if (!selectedEl) return; - - const index = parseInt(selectedEl.getAttribute('data-index')); - const transform = currentTransformations[index]; - - fineTuneAngle += offset; - updateFineTuneDisplay(); - - // Update the image style using the combined transform - const imgEl = selectedEl.querySelector('img'); - if (imgEl) { - let totalRotation = transform.rotation + fineTuneAngle; - let transformStr = `rotate(${totalRotation}deg)`; - if (transform.flipH) transformStr += ` scaleX(-1)`; - if (transform.flipV) transformStr += ` scaleY(-1)`; - imgEl.style.transform = transformStr; - } - - // Update display name inside the card - const nameEl = selectedEl.querySelector('.transform-name'); - if (nameEl && transform) { - if (fineTuneAngle === 0) { - nameEl.innerText = transform.name; - } else { - 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' }); @@ -1280,598 +1054,166 @@ window.closeSettingsModal = closeSettingsModal; window.updateCredentials = updateCredentials; // ================================================================ -// CONFIGURAÇÕES DE PLUGINS (Wasabi Storage) +// GERENCIAMENTO DE CLÍNICAS (ADMIN) // ================================================================ -function openPluginsModal() { - const modal = document.getElementById('pluginsModal'); - if (modal) modal.style.display = 'block'; +function showAdminClinics() { + view = 'admin-clinics'; + document.querySelector('.content-scroll').scrollTop = 0; - // Limpar campos - document.getElementById('wasabiAccessKey').value = ''; - document.getElementById('wasabiSecretKey').value = ''; - document.getElementById('wasabiBucket').value = ''; - document.getElementById('wasabiRegion').value = ''; - document.getElementById('wasabiEndpoint').value = ''; + // Esconder outras visões + imagesGrid.style.display = 'none'; + emptyState.style.display = 'none'; + patientHeader.style.display = 'none'; + backBtn.style.display = 'none'; - const alertBox = document.getElementById('pluginsStatusAlert'); - alertBox.style.display = 'none'; - - // Carregar configurações atuais - loadPluginsConfig(); + // Mostrar painel admin + document.getElementById('adminClinicsState').style.display = 'block'; + + // Atualizar menu + document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active')); + document.getElementById('navAdminClinics').classList.add('active'); + + loadAdminClinics(); } -function closePluginsModal() { - const modal = document.getElementById('pluginsModal'); - if (modal) modal.style.display = 'none'; -} +// Override para voltar aos pacientes e esconder o admin state +const originalShowPatientsView = showPatientsView; +showPatientsView = function() { + document.getElementById('adminClinicsState').style.display = 'none'; + document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active')); + document.querySelector('a[href="/"]').classList.add('active'); + originalShowPatientsView(); +}; +window.showPatientsView = showPatientsView; -async function loadPluginsConfig() { +async function loadAdminClinics() { + const tbody = document.getElementById('clinicsTableBody'); + tbody.innerHTML = 'Carregando...'; + try { - const res = await fetchWithAuth('/api/system/storage-config'); - if (!res.ok) throw new Error('Falha ao carregar configurações'); + const res = await fetchWithAuth(`${API_URL}/auth/users`); + if (!res.ok) throw new Error('Erro ao carregar clínicas'); + const data = await res.json(); - const 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).'; + if (data.users.length === 0) { + tbody.innerHTML = 'Nenhuma clínica cadastrada.'; + return; } - alertBox.style.display = 'block'; + + tbody.innerHTML = data.users.map(u => ` + + #${u.id} + ${escapeHtml(u.username)}${u.is_admin ? ' Admin' : ''} + ${escapeHtml(u.clinic_name || '—')} + ${escapeHtml(u.email)} + ${escapeHtml(u.pc_name || '—')} + + + ${u.id !== currentUser.id ? `` : ''} + + + `).join(''); } catch (e) { - console.error(e); - showToast('Erro ao carregar configurações do Wasabi.', 'error'); + tbody.innerHTML = 'Erro ao carregar dados.'; } } -async function updatePluginsConfig(event) { +function openCreateClinicModal() { + document.getElementById('createClinicModal').style.display = 'block'; +} + +function closeCreateClinicModal() { + document.getElementById('createClinicModal').style.display = 'none'; + document.getElementById('createClinicForm').reset(); +} + +async function submitCreateClinic(event) { event.preventDefault(); - const 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 btn = event.target.querySelector('button[type="submit"]'); const originalText = btn.innerHTML; - btn.innerHTML = ' Salvando...'; + btn.innerHTML = 'Salvando...'; btn.disabled = true; + const payload = { + username: document.getElementById('clinicUsername').value.trim(), + email: document.getElementById('clinicEmail').value.trim(), + password: document.getElementById('clinicPassword').value, + clinic_name: document.getElementById('clinicName').value.trim(), + pc_name: document.getElementById('clinicPcName').value.trim() + }; + try { - const res = await fetchWithAuth('/api/system/storage-config', { + const res = await fetchWithAuth(`${API_URL}/auth/create-user`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - wasabiAccessKey, - wasabiSecretKey, - wasabiBucket, - wasabiRegion, - wasabiEndpoint - }) + body: JSON.stringify(payload) }); - 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); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Erro ao criar'); + showToast('Clínica cadastrada com sucesso!', 'success'); + closeCreateClinicModal(); + loadAdminClinics(); } catch (e) { - console.error(e); - showToast(e.message, 'error'); + alert(e.message); } finally { btn.innerHTML = originalText; btn.disabled = false; } } -async function triggerSync() { - showToast('🔄 Enviando sinal de sincronização para dispositivos...', 'info'); +async function deleteClinic(id, username) { + if (!confirm(`Tem certeza que deseja excluir permanentemente o acesso da clínica "${username}"?`)) return; + try { - const res = await fetchWithAuth('/api/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'); - } + const res = await fetchWithAuth(`${API_URL}/auth/users/${id}`, { method: 'DELETE' }); + if (!res.ok) throw new Error('Erro ao excluir'); + showToast('Clínica excluída', 'success'); + loadAdminClinics(); } catch (e) { - 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'; - +async function generateClinicToken(id) { try { - const res = await fetchWithAuth('/api/socket/status'); - if (!res.ok) throw new Error('Falha ao obter status dos dispositivos.'); + const res = await fetchWithAuth(`${API_URL}/auth/users/${id}/token`, { method: 'POST' }); + if (!res.ok) throw new Error('Erro ao gerar token'); const data = await res.json(); - 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'; - + document.getElementById('clinicTokenInput').value = data.token; + document.getElementById('tokenCopySuccess').style.display = 'none'; + document.getElementById('showTokenModal').style.display = 'block'; } 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 closeShowTokenModal() { + document.getElementById('showTokenModal').style.display = 'none'; } -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 copyTokenToClipboard() { + const input = document.getElementById('clinicTokenInput'); + input.select(); + input.setSelectionRange(0, 99999); + navigator.clipboard.writeText(input.value).then(() => { + document.getElementById('tokenCopySuccess').style.display = 'block'; }); } -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(); -} +// Admin Exports +window.showAdminClinics = showAdminClinics; +window.openCreateClinicModal = openCreateClinicModal; +window.closeCreateClinicModal = closeCreateClinicModal; +window.submitCreateClinic = submitCreateClinic; +window.deleteClinic = deleteClinic; +window.generateClinicToken = generateClinicToken; +window.closeShowTokenModal = closeShowTokenModal; +window.copyTokenToClipboard = copyTokenToClipboard; -function 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 + diff --git a/dental-server/public/cadastro.html b/dental-server/public/cadastro.html new file mode 100644 index 0000000..0051713 --- /dev/null +++ b/dental-server/public/cadastro.html @@ -0,0 +1,468 @@ + + + + + + ScoreOdonto - Cadastro de Clínica + + + + +
Texto copiado!
+ +
+ +
+
+ 🦷 +

Cadastrar Clínica

+

Crie o acesso para obter o token de conexão do notebook

+
+ +
Erro ao cadastrar. Tente novamente.
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + + +
+ + + + diff --git a/dental-server/public/index.html b/dental-server/public/index.html index b1214a4..f35dd2e 100644 --- a/dental-server/public/index.html +++ b/dental-server/public/index.html @@ -6,7 +6,7 @@ Gerenciador de Imagens Dentais - +
@@ -19,33 +19,18 @@ 🖼️ Pacientes - - - - - - - Novo Paciente - 🖥️ Dados do Cliente + 🖥️ Dispositivos + + ⚙️ Configurações - - 🔌 Plugins / Wasabi - - - 🔄 Sincronização - ⚠️ Zerar Sistema @@ -68,6 +53,9 @@

Galeria de Imagens

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

+ + +
@@ -137,30 +149,14 @@
- @@ -301,171 +297,61 @@
- -