feat: reestruturacao de menus na sidebar, niveis de acesso e migracao para postgresql/dragonflydb
continuous-integration/webhook Deploy concluido (rx.scoreodonto.com)
continuous-integration/webhook Deploy concluido (rx.scoreodonto.com)
This commit is contained in:
@@ -1,153 +0,0 @@
|
||||
# ✅ CONVERSÃO SQLite → MySQL COMPLETA
|
||||
|
||||
## 📋 RESUMO DA CONVERSÃO
|
||||
|
||||
Todas as referências a SQLite foram convertidas para MySQL.
|
||||
|
||||
---
|
||||
|
||||
## ✅ ARQUIVOS MODIFICADOS
|
||||
|
||||
### **1. package.json** ✅
|
||||
- ❌ Removido: `sqlite3`
|
||||
- ✅ Adicionado: `mysql2`
|
||||
|
||||
### **2. database.js** ✅
|
||||
- ✅ Convertido de SQLite3 para MySQL2
|
||||
- ✅ Pool de conexões implementado
|
||||
- ✅ Sintaxe MySQL aplicada
|
||||
- ✅ Tabelas criadas corretamente
|
||||
- ✅ Índices aplicados
|
||||
|
||||
### **3. migration-v2.sql** ✅
|
||||
- ✅ Sintaxe SQLite → MySQL
|
||||
- ✅ CREATE TABLE IF NOT EXISTS
|
||||
- ✅ AUTO_INCREMENT no lugar de INTEGER PRIMARY KEY
|
||||
- ✅ TINYINT(1) no lugar de INTEGER
|
||||
- ✅ VARCHAR no lugar de TEXT (onde apropriado)
|
||||
- ✅ Índices criados
|
||||
- ⚠️ Migration complexa criada (com verificações)
|
||||
|
||||
### **4. migration-simple.sql** ✅ (NOVO)
|
||||
- ✅ Migration simplificada
|
||||
- ✅ Recomendada para novo banco
|
||||
- ✅ Cria tabelas completas de uma vez
|
||||
|
||||
### **5. ENV-CONFIG.txt** ✅
|
||||
- ✅ Variáveis SQLite removidas
|
||||
- ✅ Variáveis MySQL adicionadas:
|
||||
- `DB_HOST`
|
||||
- `DB_PORT`
|
||||
- `DB_USER`
|
||||
- `DB_PASSWORD`
|
||||
- `DB_NAME`
|
||||
|
||||
### **6. INSTALACAO-DENTAL-SERVER.md** ✅
|
||||
- ✅ Comandos SQLite → MySQL
|
||||
- ✅ Instruções de migration atualizadas
|
||||
- ✅ Troubleshooting atualizado
|
||||
|
||||
### **7. README.md** ✅
|
||||
- ✅ Referências atualizadas
|
||||
- ✅ Tecnologias atualizadas
|
||||
|
||||
### **8. INSTRUCOES-RAPIDAS.txt** ✅
|
||||
- ✅ Variáveis de ambiente atualizadas
|
||||
- ✅ Comandos de migration atualizados
|
||||
|
||||
---
|
||||
|
||||
## 🎯 PRINCIPAIS MUDANÇAS
|
||||
|
||||
### **Tipos de Dados**
|
||||
| SQLite | MySQL |
|
||||
|--------|-------|
|
||||
| INTEGER PRIMARY KEY | INT AUTO_INCREMENT PRIMARY KEY |
|
||||
| INTEGER DEFAULT 1 | TINYINT(1) DEFAULT 1 |
|
||||
| TEXT | VARCHAR(255) ou TEXT |
|
||||
| DATETIME DEFAULT CURRENT_TIMESTAMP | DATETIME DEFAULT CURRENT_TIMESTAMP ✅ |
|
||||
|
||||
### **Queries**
|
||||
| SQLite | MySQL |
|
||||
|--------|-------|
|
||||
| `db.get()` | `pool.query()` |
|
||||
| `db.all()` | `pool.query()` |
|
||||
| `db.run()` | `pool.query()` |
|
||||
| `this.lastID` | `result.insertId` |
|
||||
| `this.changes` | `result.affectedRows` |
|
||||
|
||||
### **Connection**
|
||||
| SQLite | MySQL |
|
||||
|--------|-------|
|
||||
| `sqlite3.Database(path)` | `mysql.createPool(config)` |
|
||||
| Direto | Pool de conexões |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 INSTALAÇÃO
|
||||
|
||||
### **1. Criar Banco**
|
||||
```bash
|
||||
mysql -u root -p -e "CREATE DATABASE dental_images CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
||||
```
|
||||
|
||||
### **2. Executar Migration**
|
||||
```bash
|
||||
mysql -u root -p dental_images < migration-simple.sql
|
||||
```
|
||||
|
||||
### **3. Configurar .env**
|
||||
```env
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=sua_senha
|
||||
DB_NAME=dental_images
|
||||
```
|
||||
|
||||
### **4. Instalar Dependências**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ FUNCIONALIDADES TESTADAS
|
||||
|
||||
- ✅ Conexão ao MySQL
|
||||
- ✅ Criação de tabelas
|
||||
- ✅ Criação de índices
|
||||
- ✅ Queries básicas (get, all, run)
|
||||
- ✅ Auto-increment
|
||||
- ✅ Datetime defaults
|
||||
- ✅ Pool de conexões
|
||||
|
||||
---
|
||||
|
||||
## 📦 DIFERENÇAS IMPORTANTES
|
||||
|
||||
### **Vantagens do MySQL:**
|
||||
- ✅ Pool de conexões (melhor performance)
|
||||
- ✅ Suporte a múltiplos usuários
|
||||
- ✅ Melhor para produção
|
||||
- ✅ Ferramentas de administração (phpMyAdmin, Workbench)
|
||||
|
||||
### **Desvantagens:**
|
||||
- ⚠️ Requer servidor MySQL rodando
|
||||
- ⚠️ Configuração mais complexa
|
||||
- ⚠️ Precisa de credenciais
|
||||
|
||||
---
|
||||
|
||||
## 🎉 CONCLUSÃO
|
||||
|
||||
**Conversão 100% completa!**
|
||||
|
||||
Todos os arquivos foram atualizados para MySQL. O sistema está pronto para usar com banco MySQL.
|
||||
|
||||
---
|
||||
|
||||
**Status:** ✅ CONVERSÃO COMPLETA
|
||||
**Banco:** MySQL
|
||||
**Versão:** 2.0.0
|
||||
**Data:** 2025-01-26
|
||||
@@ -1,75 +0,0 @@
|
||||
═══════════════════════════════════════════════════════════
|
||||
✅ CONVERSÃO SQLite → MySQL COMPLETA
|
||||
═══════════════════════════════════════════════════════════
|
||||
|
||||
🎉 TODOS OS ARQUIVOS FORAM CONVERTIDOS PARA MySQL!
|
||||
|
||||
═══════════════════════════════════════════════════════════
|
||||
✅ O QUE FOI MUDADO
|
||||
═══════════════════════════════════════════════════════════
|
||||
|
||||
1. package.json
|
||||
❌ Removido: sqlite3
|
||||
✅ Adicionado: mysql2
|
||||
|
||||
2. database.js
|
||||
✅ Pool de conexões MySQL
|
||||
✅ Sintaxe MySQL aplicada
|
||||
✅ Funções convertidas
|
||||
|
||||
3. migration-simple.sql (NOVO)
|
||||
✅ Migration MySQL completa
|
||||
✅ Criar banco e tabelas
|
||||
|
||||
4. ENV-CONFIG.txt
|
||||
✅ Variáveis MySQL adicionadas
|
||||
|
||||
5. Documentação
|
||||
✅ INSTALACAO-DENTAL-SERVER.md atualizado
|
||||
✅ README.md atualizado
|
||||
✅ INSTRUCOES-RAPIDAS.txt atualizado
|
||||
|
||||
═══════════════════════════════════════════════════════════
|
||||
🚀 COMO USAR COM MYSQL
|
||||
═══════════════════════════════════════════════════════════
|
||||
|
||||
1. CRIAR BANCO:
|
||||
mysql -u root -p
|
||||
CREATE DATABASE dental_images;
|
||||
exit
|
||||
|
||||
2. EXECUTAR MIGRATION:
|
||||
mysql -u root -p dental_images < migration-simple.sql
|
||||
|
||||
3. CONFIGURAR .env:
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=sua_senha
|
||||
DB_NAME=dental_images
|
||||
|
||||
4. INSTALAR:
|
||||
npm install
|
||||
|
||||
5. INICIAR:
|
||||
pm2 start server.js --name dental-server
|
||||
|
||||
═══════════════════════════════════════════════════════════
|
||||
✅ FUNCIONALIDADES TESTADAS
|
||||
═══════════════════════════════════════════════════════════
|
||||
|
||||
✅ Conexão MySQL
|
||||
✅ Criação de tabelas
|
||||
✅ Índices
|
||||
✅ Queries (get, all, run)
|
||||
✅ Auto-increment
|
||||
✅ Pool de conexões
|
||||
✅ Zero erros de linter
|
||||
|
||||
═══════════════════════════════════════════════════════════
|
||||
|
||||
Status: ✅ CONVERSÃO COMPLETA
|
||||
Banco: MySQL
|
||||
Versão: 2.0.0
|
||||
|
||||
═══════════════════════════════════════════════════════════
|
||||
@@ -83,7 +83,7 @@ function generateToken(user, expiresIn = JWT_EXPIRES_IN) {
|
||||
async function createInitialUser() {
|
||||
try {
|
||||
// Verificar se usuário já existe
|
||||
const existingUser = await db.get('SELECT * FROM users WHERE username = ?', ['rcesar']);
|
||||
const existingUser = await db.get('SELECT * FROM users WHERE username = $1', ['rcesar']);
|
||||
|
||||
if (existingUser) {
|
||||
console.log('👤 Usuário admin já existe:', existingUser.username);
|
||||
@@ -96,7 +96,7 @@ async function createInitialUser() {
|
||||
// Inserir usuário
|
||||
await db.run(
|
||||
`INSERT INTO users (username, email, password_hash, is_admin, created_at)
|
||||
VALUES (?, ?, ?, 1, NOW())`,
|
||||
VALUES ($1, $2, $3, true, CURRENT_TIMESTAMP) RETURNING id`,
|
||||
['rcesar', 'rcesar@rcesar.com', passwordHash]
|
||||
);
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ async function runBackfill() {
|
||||
await storage.saveImage(thumbFilename, thumbBuffer, img.client_name, img.patient_name, false);
|
||||
|
||||
// d. Atualizar no banco de dados
|
||||
await db.run('UPDATE images SET thumb_filename = ? WHERE id = ?', [thumbFilename, img.id]);
|
||||
await db.run('UPDATE images SET thumb_filename = $1 WHERE id = $2', [thumbFilename, img.id]);
|
||||
|
||||
console.log(`✅ [ID ${img.id}] Thumbnail salvo com sucesso: ${thumbFilename} (Paciente: ${img.patient_name})`);
|
||||
successCount++;
|
||||
|
||||
@@ -1,42 +1,8 @@
|
||||
const { Pool } = require('pg');
|
||||
require('dotenv').config();
|
||||
|
||||
let pool = null;
|
||||
|
||||
// ================================================================
|
||||
// FUNÇÕES AUXILIARES DE COMPATIBILIDADE (MYSQL -> POSTGRES)
|
||||
// ================================================================
|
||||
|
||||
function rewriteToPostgres(sql) {
|
||||
let rewritten = sql;
|
||||
|
||||
// 1. Converter MySQL NOW() para Postgres CURRENT_TIMESTAMP
|
||||
rewritten = rewritten.replace(/\bNOW\(\)/gi, 'CURRENT_TIMESTAMP');
|
||||
|
||||
// 2. Converter parâmetros de ? para $1, $2, etc.
|
||||
let paramIndex = 1;
|
||||
rewritten = rewritten.replace(/\?/g, () => `$${paramIndex++}`);
|
||||
|
||||
// 3. Adicionar RETURNING id nas inserções se não houver
|
||||
if (rewritten.trim().toUpperCase().startsWith('INSERT INTO')) {
|
||||
if (!rewritten.toUpperCase().includes('RETURNING')) {
|
||||
rewritten += ' RETURNING id';
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Reescrever ON DUPLICATE KEY UPDATE do MySQL
|
||||
if (rewritten.includes('ON DUPLICATE KEY UPDATE')) {
|
||||
// Isso é específico para a query de inserção de usuário default
|
||||
rewritten = `INSERT INTO users (username, email, password_hash, created_at)
|
||||
VALUES ($1, $2, $3, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (username) DO UPDATE SET
|
||||
email = EXCLUDED.email,
|
||||
password_hash = EXCLUDED.password_hash
|
||||
RETURNING id`;
|
||||
}
|
||||
|
||||
return rewritten;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// INICIALIZAR BANCO DE DADOS POSTGRESQL
|
||||
// ================================================================
|
||||
@@ -218,7 +184,7 @@ async function createTables() {
|
||||
function get(query, params = []) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const res = await pool.query(rewriteToPostgres(query), params);
|
||||
const res = await pool.query(query, params);
|
||||
resolve(res.rows[0] || null);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
@@ -229,7 +195,7 @@ function get(query, params = []) {
|
||||
function all(query, params = []) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const res = await pool.query(rewriteToPostgres(query), params);
|
||||
const res = await pool.query(query, params);
|
||||
resolve(res.rows || []);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
@@ -240,8 +206,7 @@ function all(query, params = []) {
|
||||
function run(query, params = []) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const rewritten = rewriteToPostgres(query);
|
||||
const res = await pool.query(rewritten, params);
|
||||
const res = await pool.query(query, params);
|
||||
|
||||
// No PostgreSQL, se adicionamos RETURNING id, o id estará no res.rows[0]
|
||||
const insertedId = (res.rows && res.rows[0] && res.rows[0].id) ? res.rows[0].id : null;
|
||||
|
||||
@@ -43,7 +43,7 @@ async function checkInstallation() {
|
||||
} else {
|
||||
// Tentar inicializar se .env tem configurações
|
||||
const envContent = fs.readFileSync(envPath, 'utf8');
|
||||
if (envContent.includes('DB_TYPE=sqlite') || (envContent.includes('DB_HOST') && envContent.includes('DB_NAME'))) {
|
||||
if (envContent.includes('DB_HOST') && envContent.includes('DB_NAME')) {
|
||||
// Recarregar .env
|
||||
require('dotenv').config({ path: envPath });
|
||||
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
/*M!999999\- enable the sandbox mode */
|
||||
-- MariaDB dump 10.19 Distrib 10.11.10-MariaDB, for Linux (x86_64)
|
||||
--
|
||||
-- Host: localhost Database: dental_images
|
||||
-- ------------------------------------------------------
|
||||
-- Server version 10.11.10-MariaDB-log
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
||||
/*!40103 SET TIME_ZONE='+00:00' */;
|
||||
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||
|
||||
--
|
||||
-- Table structure for table `images`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `images`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `images` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`image_guid` varchar(255) NOT NULL,
|
||||
`patient_name` text DEFAULT NULL,
|
||||
`client_name` varchar(100) DEFAULT NULL,
|
||||
`original_filename` text DEFAULT NULL,
|
||||
`filename` text NOT NULL,
|
||||
`enabled` tinyint(1) DEFAULT 1,
|
||||
`tooth_number` varchar(10) DEFAULT NULL,
|
||||
`tooth_side` varchar(10) DEFAULT NULL,
|
||||
`patient_name_resumed` varchar(50) DEFAULT NULL,
|
||||
`original_image_id` int(11) DEFAULT NULL,
|
||||
`rotation` int(11) DEFAULT 0,
|
||||
`flip_horizontal` int(11) DEFAULT 0,
|
||||
`flip_vertical` int(11) DEFAULT 0,
|
||||
`created_at` datetime DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_images_enabled` (`enabled`),
|
||||
KEY `idx_images_original` (`original_image_id`),
|
||||
KEY `idx_patient_name` (`patient_name`(100)),
|
||||
KEY `idx_image_guid` (`image_guid`),
|
||||
KEY `idx_created_at` (`created_at`),
|
||||
KEY `idx_client_name` (`client_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `images`
|
||||
--
|
||||
|
||||
LOCK TABLES `images` WRITE;
|
||||
/*!40000 ALTER TABLE `images` DISABLE KEYS */;
|
||||
/*!40000 ALTER TABLE `images` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `users`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `users`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `users` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(50) NOT NULL,
|
||||
`email` varchar(100) NOT NULL,
|
||||
`password_hash` text NOT NULL,
|
||||
`created_at` datetime DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `username` (`username`),
|
||||
UNIQUE KEY `email` (`email`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `users`
|
||||
--
|
||||
|
||||
LOCK TABLES `users` WRITE;
|
||||
/*!40000 ALTER TABLE `users` DISABLE KEYS */;
|
||||
INSERT INTO `users` VALUES
|
||||
(1,'rcesar','rcesar@rcesar.com','$2b$10$i0zu9BGACsu6TDTsrU1EaOGZkKnvp8zwjOek1W/3m9mP4mzbN8HkC','2025-10-31 07:16:50');
|
||||
/*!40000 ALTER TABLE `users` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Dumping routines for database 'dental_images'
|
||||
--
|
||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
|
||||
-- Dump completed on 2025-10-31 21:24:39
|
||||
@@ -1,84 +0,0 @@
|
||||
/*M!999999\- enable the sandbox mode */
|
||||
-- MariaDB dump 10.19 Distrib 10.11.10-MariaDB, for Linux (x86_64)
|
||||
--
|
||||
-- Host: localhost Database: dental_images
|
||||
-- ------------------------------------------------------
|
||||
-- Server version 10.11.10-MariaDB-log
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
||||
/*!40103 SET TIME_ZONE='+00:00' */;
|
||||
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||
|
||||
--
|
||||
-- Table structure for table `images`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `images`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `images` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`image_guid` varchar(255) NOT NULL,
|
||||
`patient_name` text DEFAULT NULL,
|
||||
`client_name` varchar(100) DEFAULT NULL,
|
||||
`original_filename` text DEFAULT NULL,
|
||||
`filename` text NOT NULL,
|
||||
`enabled` tinyint(1) DEFAULT 1,
|
||||
`tooth_number` varchar(10) DEFAULT NULL,
|
||||
`tooth_side` varchar(10) DEFAULT NULL,
|
||||
`patient_name_resumed` varchar(50) DEFAULT NULL,
|
||||
`original_image_id` int(11) DEFAULT NULL,
|
||||
`rotation` int(11) DEFAULT 0,
|
||||
`flip_horizontal` int(11) DEFAULT 0,
|
||||
`flip_vertical` int(11) DEFAULT 0,
|
||||
`created_at` datetime DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_images_enabled` (`enabled`),
|
||||
KEY `idx_images_original` (`original_image_id`),
|
||||
KEY `idx_patient_name` (`patient_name`(100)),
|
||||
KEY `idx_image_guid` (`image_guid`),
|
||||
KEY `idx_created_at` (`created_at`),
|
||||
KEY `idx_client_name` (`client_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `users`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `users`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `users` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(50) NOT NULL,
|
||||
`email` varchar(100) NOT NULL,
|
||||
`password_hash` text NOT NULL,
|
||||
`created_at` datetime DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `username` (`username`),
|
||||
UNIQUE KEY `email` (`email`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping routines for database 'dental_images'
|
||||
--
|
||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
|
||||
-- Dump completed on 2025-10-31 21:24:36
|
||||
@@ -5,6 +5,20 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Gerenciamento de Clínicas - RF Dental</title>
|
||||
|
||||
<script>
|
||||
// Redirecionar se não estiver autenticado ou não for administrador
|
||||
if (!localStorage.getItem('auth_token')) {
|
||||
window.location.replace('/login');
|
||||
} else if (localStorage.getItem('is_admin') !== 'true') {
|
||||
window.location.replace('/');
|
||||
} else {
|
||||
// Adiciona classe de admin no body para que os estilos de admin sejam exibidos
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.body.classList.add('is-admin');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Fontes -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
@@ -28,15 +42,41 @@
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<a href="/" class="nav-item">
|
||||
<div class="nav-section-title">Menu Principal</div>
|
||||
<a href="/" class="nav-item" id="menu-dashboard">
|
||||
<i class="fa-solid fa-chart-line"></i> Dashboard
|
||||
</a>
|
||||
<a href="/clients" class="nav-item">
|
||||
<i class="fa-solid fa-network-wired"></i> Conexões
|
||||
<a href="/?action=new-patient" class="nav-item" id="menu-new-patient" onclick="if(window.location.pathname === '/') { openCreatePatientModal(); return false; }">
|
||||
<i class="fa-solid fa-user-plus"></i> Novo Paciente
|
||||
</a>
|
||||
<a href="/admin-clinics" class="nav-item active">
|
||||
<i class="fa-solid fa-hospital"></i> Clínicas e Acessos
|
||||
<a href="/?action=sync-upload" class="nav-item" id="menu-upload" onclick="if(window.location.pathname === '/') { openSyncModal('upload'); return false; }">
|
||||
<i class="fa-solid fa-upload"></i> Enviar Imagens
|
||||
</a>
|
||||
<a href="/?action=settings" class="nav-item" id="menu-settings" onclick="if(window.location.pathname === '/') { openSettingsModal(); return false; }">
|
||||
<i class="fa-solid fa-user-gear"></i> Minha Conta
|
||||
</a>
|
||||
|
||||
<div class="admin-only">
|
||||
<div class="nav-section-title">Área Administrativa</div>
|
||||
<a href="/clients" class="nav-item" id="menu-clients">
|
||||
<i class="fa-solid fa-network-wired"></i> Conexões
|
||||
</a>
|
||||
<a href="/admin-clinics" class="nav-item active" id="menu-clinics">
|
||||
<i class="fa-solid fa-hospital"></i> Clínicas e Acessos
|
||||
</a>
|
||||
<a href="/?action=users" class="nav-item" id="menu-users" onclick="if(window.location.pathname === '/') { openUserManagementModal(); return false; }">
|
||||
<i class="fa-solid fa-users"></i> Gerenciar Usuários
|
||||
</a>
|
||||
<a href="/?action=sync-devices" class="nav-item" id="menu-sync" onclick="if(window.location.pathname === '/') { openSyncModal('devices'); return false; }">
|
||||
<i class="fa-solid fa-rotate"></i> Sincronização
|
||||
</a>
|
||||
<a href="/?action=plugins" class="nav-item" id="menu-plugins" onclick="if(window.location.pathname === '/') { openPluginsModal(); return false; }">
|
||||
<i class="fa-solid fa-plug"></i> Plugins / Wasabi
|
||||
</a>
|
||||
<a href="/reset" class="nav-item" id="menu-reset" style="color: #dc2626;">
|
||||
<i class="fa-solid fa-triangle-exclamation"></i> Zerar Sistema
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
|
||||
@@ -4,6 +4,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// Pegar Token JWT do Admin
|
||||
const token = localStorage.getItem('auth_token') || '';
|
||||
const isAdmin = localStorage.getItem('is_admin') === 'true';
|
||||
|
||||
// Se não tem token, redirecionar imediatamente para login
|
||||
if (!token) {
|
||||
@@ -11,6 +12,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Se não for administrador, redirecionar imediatamente para a home
|
||||
if (!isAdmin) {
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
+14
-13
@@ -78,15 +78,11 @@ async function verifyAuth() {
|
||||
}
|
||||
|
||||
function updateAdminMenuVisibility(isAdmin) {
|
||||
// Esconder links não autorizados da Sidebar
|
||||
const connectionsLink = document.querySelector('.sidebar-nav .nav-item[href="/clients"]');
|
||||
const clinicsLink = document.querySelector('.sidebar-nav .nav-item[href="/admin-clinics"]');
|
||||
if (connectionsLink) connectionsLink.style.display = isAdmin ? 'flex' : 'none';
|
||||
if (clinicsLink) clinicsLink.style.display = isAdmin ? 'flex' : 'none';
|
||||
|
||||
// Esconder botão de gerenciamento de usuários na toolbar
|
||||
const btnUserManagement = document.getElementById('btnUserManagement');
|
||||
if (btnUserManagement) btnUserManagement.style.display = isAdmin ? 'flex' : 'none';
|
||||
if (isAdmin) {
|
||||
document.body.classList.add('is-admin');
|
||||
} else {
|
||||
document.body.classList.remove('is-admin');
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchWithAuth(url, options = {}) {
|
||||
@@ -189,7 +185,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
if (action === 'new-patient') window.openCreatePatientModal();
|
||||
else if (action === 'settings') window.openSettingsModal();
|
||||
else if (action === 'plugins') window.openPluginsModal();
|
||||
else if (action === 'sync') window.openSyncModal();
|
||||
else if (action === 'sync-devices') window.openSyncModal('devices');
|
||||
else if (action === 'sync-upload') window.openSyncModal('upload');
|
||||
else if (action === 'sync') window.openSyncModal('devices');
|
||||
else if (action === 'users') window.openUserManagementModal();
|
||||
|
||||
// Remove querystring to prevent reopening on reload
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
@@ -1505,7 +1504,7 @@ window.triggerSync = triggerSync;
|
||||
|
||||
let selectedUploadFiles = [];
|
||||
|
||||
function openSyncModal() {
|
||||
function openSyncModal(defaultTab = 'devices') {
|
||||
const modal = document.getElementById('syncModal');
|
||||
if (!modal) return;
|
||||
|
||||
@@ -1513,13 +1512,15 @@ function openSyncModal() {
|
||||
modal.style.display = 'flex';
|
||||
|
||||
// Resetar abas
|
||||
switchSyncTab('devices');
|
||||
switchSyncTab(defaultTab);
|
||||
|
||||
// Limpar formulário de upload
|
||||
resetManualUploadForm();
|
||||
|
||||
// Carregar dados
|
||||
loadSyncDevices();
|
||||
if (defaultTab === 'devices') {
|
||||
loadSyncDevices();
|
||||
}
|
||||
refreshUploadPatients();
|
||||
|
||||
// Escuta de clique fora para fechar modal
|
||||
|
||||
@@ -179,9 +179,16 @@
|
||||
|
||||
</style>
|
||||
<script>
|
||||
// Redirecionar se não estiver autenticado
|
||||
// Redirecionar se não estiver autenticado ou não for administrador
|
||||
if (!localStorage.getItem('auth_token')) {
|
||||
window.location.href = '/login';
|
||||
window.location.replace('/login');
|
||||
} else if (localStorage.getItem('is_admin') !== 'true') {
|
||||
window.location.replace('/');
|
||||
} else {
|
||||
// Adiciona classe de admin no body para que os estilos de admin sejam exibidos
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.body.classList.add('is-admin');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
@@ -196,15 +203,41 @@
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<a href="/" class="nav-item">
|
||||
<div class="nav-section-title">Menu Principal</div>
|
||||
<a href="/" class="nav-item" id="menu-dashboard">
|
||||
<i class="fa-solid fa-chart-line"></i> Dashboard
|
||||
</a>
|
||||
<a href="/clients" class="nav-item active">
|
||||
<i class="fa-solid fa-network-wired"></i> Conexões
|
||||
<a href="/?action=new-patient" class="nav-item" id="menu-new-patient" onclick="if(window.location.pathname === '/') { openCreatePatientModal(); return false; }">
|
||||
<i class="fa-solid fa-user-plus"></i> Novo Paciente
|
||||
</a>
|
||||
<a href="/admin-clinics" class="nav-item">
|
||||
<i class="fa-solid fa-hospital"></i> Clínicas e Acessos
|
||||
<a href="/?action=sync-upload" class="nav-item" id="menu-upload" onclick="if(window.location.pathname === '/') { openSyncModal('upload'); return false; }">
|
||||
<i class="fa-solid fa-upload"></i> Enviar Imagens
|
||||
</a>
|
||||
<a href="/?action=settings" class="nav-item" id="menu-settings" onclick="if(window.location.pathname === '/') { openSettingsModal(); return false; }">
|
||||
<i class="fa-solid fa-user-gear"></i> Minha Conta
|
||||
</a>
|
||||
|
||||
<div class="admin-only">
|
||||
<div class="nav-section-title">Área Administrativa</div>
|
||||
<a href="/clients" class="nav-item active" id="menu-clients">
|
||||
<i class="fa-solid fa-network-wired"></i> Conexões
|
||||
</a>
|
||||
<a href="/admin-clinics" class="nav-item" id="menu-clinics">
|
||||
<i class="fa-solid fa-hospital"></i> Clínicas e Acessos
|
||||
</a>
|
||||
<a href="/?action=users" class="nav-item" id="menu-users" onclick="if(window.location.pathname === '/') { openUserManagementModal(); return false; }">
|
||||
<i class="fa-solid fa-users"></i> Gerenciar Usuários
|
||||
</a>
|
||||
<a href="/?action=sync-devices" class="nav-item" id="menu-sync" onclick="if(window.location.pathname === '/') { openSyncModal('devices'); return false; }">
|
||||
<i class="fa-solid fa-rotate"></i> Sincronização
|
||||
</a>
|
||||
<a href="/?action=plugins" class="nav-item" id="menu-plugins" onclick="if(window.location.pathname === '/') { openPluginsModal(); return false; }">
|
||||
<i class="fa-solid fa-plug"></i> Plugins / Wasabi
|
||||
</a>
|
||||
<a href="/reset" class="nav-item" id="menu-reset" style="color: #dc2626;">
|
||||
<i class="fa-solid fa-triangle-exclamation"></i> Zerar Sistema
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
|
||||
@@ -21,15 +21,41 @@
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<a href="/" class="nav-item active">
|
||||
<div class="nav-section-title">Menu Principal</div>
|
||||
<a href="/" class="nav-item active" id="menu-dashboard" onclick="if(window.location.pathname === '/') { showView('gallery'); return false; }">
|
||||
<i class="fa-solid fa-chart-line"></i> Dashboard
|
||||
</a>
|
||||
<a href="/clients" class="nav-item">
|
||||
<i class="fa-solid fa-network-wired"></i> Conexões
|
||||
<a href="#" class="nav-item" id="menu-new-patient" onclick="openCreatePatientModal(); return false;">
|
||||
<i class="fa-solid fa-user-plus"></i> Novo Paciente
|
||||
</a>
|
||||
<a href="/admin-clinics" class="nav-item">
|
||||
<i class="fa-solid fa-hospital"></i> Clínicas e Acessos
|
||||
<a href="#" class="nav-item" id="menu-upload" onclick="openSyncModal('upload'); return false;">
|
||||
<i class="fa-solid fa-upload"></i> Enviar Imagens
|
||||
</a>
|
||||
<a href="#" class="nav-item" id="menu-settings" onclick="openSettingsModal(); return false;">
|
||||
<i class="fa-solid fa-user-gear"></i> Minha Conta
|
||||
</a>
|
||||
|
||||
<div class="admin-only">
|
||||
<div class="nav-section-title">Área Administrativa</div>
|
||||
<a href="/clients" class="nav-item" id="menu-clients">
|
||||
<i class="fa-solid fa-network-wired"></i> Conexões
|
||||
</a>
|
||||
<a href="/admin-clinics" class="nav-item" id="menu-clinics">
|
||||
<i class="fa-solid fa-hospital"></i> Clínicas e Acessos
|
||||
</a>
|
||||
<a href="#" class="nav-item" id="menu-users" onclick="openUserManagementModal(); return false;">
|
||||
<i class="fa-solid fa-users"></i> Gerenciar Usuários
|
||||
</a>
|
||||
<a href="#" class="nav-item" id="menu-sync" onclick="openSyncModal('devices'); return false;">
|
||||
<i class="fa-solid fa-rotate"></i> Sincronização
|
||||
</a>
|
||||
<a href="#" class="nav-item" id="menu-plugins" onclick="openPluginsModal(); return false;">
|
||||
<i class="fa-solid fa-plug"></i> Plugins / Wasabi
|
||||
</a>
|
||||
<a href="/reset" class="nav-item" id="menu-reset" style="color: #dc2626;">
|
||||
<i class="fa-solid fa-triangle-exclamation"></i> Zerar Sistema
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
@@ -75,8 +101,8 @@
|
||||
<!-- Scrollable content area -->
|
||||
<div class="content-scroll">
|
||||
|
||||
<!-- Toolbar de Filtros e Ações Rápidas (Migradas da Sidebar) -->
|
||||
<div class="toolbar glass-card" style="margin-bottom: 24px; padding: 16px; display: flex; gap: 16px; align-items: center; justify-content: space-between; flex-wrap: wrap; background: rgba(255, 255, 255, 0.4); border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 12px; box-shadow: var(--shadow-sm);">
|
||||
<!-- Toolbar de Filtros e Ações Rápidas -->
|
||||
<div class="toolbar glass-card" style="margin-bottom: 24px; padding: 16px; display: flex; gap: 16px; align-items: center; background: rgba(255, 255, 255, 0.4); border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 12px; box-shadow: var(--shadow-sm);">
|
||||
<div class="toolbar-left" style="display: flex; gap: 12px; align-items: center; flex-wrap: wrap;">
|
||||
<!-- Filtro de Clientes -->
|
||||
<select id="clientFilter" class="client-filter" style="padding: 10px 16px; border-radius: 10px; border: 1px solid var(--border-color); background: var(--bg-surface); color: var(--text-primary); font-size: 0.95rem; font-weight: 500; outline: none; min-width: 200px; cursor: pointer;">
|
||||
@@ -87,27 +113,6 @@
|
||||
👁️ <span id="toggleText">Ocultas</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-right" style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
|
||||
<button class="btn btn-primary" onclick="openCreatePatientModal()" style="padding: 10px 16px; border-radius: 10px; font-weight: 600; font-size: 0.95rem; background: var(--primary-color); color: white; border: none; cursor: pointer; display: flex; align-items: center; gap: 6px; box-shadow: 0 4px 10px rgba(79, 70, 229, 0.2);">
|
||||
➕ Novo Paciente
|
||||
</button>
|
||||
<button id="btnUserManagement" class="btn btn-secondary" onclick="openUserManagementModal()" title="Gerenciar Usuários" style="display: none; padding: 10px 16px; border-radius: 10px; font-weight: 500; font-size: 0.95rem; border: 1px solid var(--border-color); background: var(--bg-surface); color: var(--text-primary); cursor: pointer; align-items: center; gap: 6px;">
|
||||
👥 Gerenciar Usuários
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="openSettingsModal()" title="Configurações" style="padding: 10px 16px; border-radius: 10px; font-weight: 500; font-size: 0.95rem; border: 1px solid var(--border-color); background: var(--bg-surface); color: var(--text-primary); cursor: pointer; display: flex; align-items: center; gap: 6px;">
|
||||
⚙️ Configurações
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="openPluginsModal()" title="Plugins / Wasabi" style="padding: 10px 16px; border-radius: 10px; font-weight: 500; font-size: 0.95rem; border: 1px solid var(--border-color); background: var(--bg-surface); color: var(--text-primary); cursor: pointer; display: flex; align-items: center; gap: 6px;">
|
||||
🔌 Plugins / Wasabi
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="openSyncModal()" title="Sincronização" style="padding: 10px 16px; border-radius: 10px; font-weight: 500; font-size: 0.95rem; border: 1px solid var(--border-color); background: var(--bg-surface); color: var(--text-primary); cursor: pointer; display: flex; align-items: center; gap: 6px;">
|
||||
🔄 Sincronização
|
||||
</button>
|
||||
<a href="/reset" class="btn btn-danger" title="Zerar Sistema" style="padding: 10px 16px; border-radius: 10px; font-weight: 500; font-size: 0.95rem; background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; text-decoration: none; cursor: pointer; display: inline-flex; align-items: center; gap: 6px;">
|
||||
⚠️ Zerar Sistema
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
|
||||
@@ -942,3 +942,41 @@ body {
|
||||
max-height: 95vh;
|
||||
}
|
||||
}
|
||||
|
||||
/* RF Dental Premium Sidebar Menus Additions */
|
||||
.nav-section-title {
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.2px;
|
||||
font-weight: 700;
|
||||
color: var(--text-secondary);
|
||||
padding: 12px 16px 4px 16px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Ocultação inicial de elementos administrativos */
|
||||
.admin-only {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Exibição quando autenticado como admin */
|
||||
body.is-admin .admin-only {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Responsividade mobile para o agrupamento admin */
|
||||
@media (max-width: 768px) {
|
||||
.nav-section-title {
|
||||
display: none !important;
|
||||
}
|
||||
body.is-admin .admin-only {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
gap: 16px !important;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ router.post('/', requireAdmin, async (req, res) => {
|
||||
|
||||
try {
|
||||
// Verificar se email já existe
|
||||
const existing = await db.get(`SELECT id FROM clinics_devices WHERE email = ?`, [email]);
|
||||
const existing = await db.get(`SELECT id FROM clinics_devices WHERE email = $1`, [email]);
|
||||
if (existing) {
|
||||
return res.status(400).json({ success: false, message: 'Email já cadastrado para outra clínica' });
|
||||
}
|
||||
@@ -52,7 +52,7 @@ router.post('/', requireAdmin, async (req, res) => {
|
||||
|
||||
await db.run(`
|
||||
INSERT INTO clinics_devices (clinic_name, email, password_hash, pc_name, machine_token)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
VALUES ($1, $2, $3, $4, $5) RETURNING id
|
||||
`, [clinic_name, email, password_hash, pc_name, machine_token]);
|
||||
|
||||
res.json({
|
||||
@@ -74,7 +74,7 @@ router.put('/:id/status', requireAdmin, async (req, res) => {
|
||||
const { is_active } = req.body;
|
||||
|
||||
try {
|
||||
await db.run(`UPDATE clinics_devices SET is_active = ? WHERE id = ?`, [is_active ? 1 : 0, id]);
|
||||
await db.run(`UPDATE clinics_devices SET is_active = $1 WHERE id = $2`, [is_active ? 1 : 0, id]);
|
||||
res.json({ success: true, message: 'Status atualizado com sucesso' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: 'Erro interno' });
|
||||
@@ -88,7 +88,7 @@ router.delete('/:id', requireAdmin, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
try {
|
||||
await db.run(`DELETE FROM clinics_devices WHERE id = ?`, [id]);
|
||||
await db.run(`DELETE FROM clinics_devices WHERE id = $1`, [id]);
|
||||
res.json({ success: true, message: 'Clínica excluída com sucesso' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: 'Erro interno' });
|
||||
|
||||
@@ -19,7 +19,7 @@ router.post('/login', async (req, res) => {
|
||||
|
||||
// Buscar usuário
|
||||
const user = await db.get(
|
||||
'SELECT * FROM users WHERE username = ? OR email = ?',
|
||||
'SELECT * FROM users WHERE username = $1 OR email = $2',
|
||||
[username, username]
|
||||
);
|
||||
|
||||
@@ -77,7 +77,7 @@ router.get('/verify', async (req, res) => {
|
||||
const jwt = require('jsonwebtoken');
|
||||
const decoded = jwt.verify(token, require('../auth').JWT_SECRET);
|
||||
|
||||
const user = await db.get('SELECT * FROM users WHERE id = ?', [decoded.id]);
|
||||
const user = await db.get('SELECT * FROM users WHERE id = $1', [decoded.id]);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'Usuário não encontrado' });
|
||||
@@ -131,7 +131,7 @@ router.post('/create-user', authenticateToken, async (req, res) => {
|
||||
|
||||
// Verificar se usuário já existe
|
||||
const existing = await db.get(
|
||||
'SELECT * FROM users WHERE username = ? OR email = ?',
|
||||
'SELECT * FROM users WHERE username = $1 OR email = $2',
|
||||
[username, email]
|
||||
);
|
||||
|
||||
@@ -147,7 +147,7 @@ router.post('/create-user', authenticateToken, async (req, res) => {
|
||||
// Criar usuário
|
||||
const result = await db.run(
|
||||
`INSERT INTO users (username, email, password_hash, clinic_name, pc_name, is_admin, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, NOW())`,
|
||||
VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP) RETURNING id`,
|
||||
[username, email, passwordHash, clinic_name || null, pc_name || null, !!is_admin]
|
||||
);
|
||||
|
||||
@@ -188,7 +188,7 @@ router.delete('/users/:id', authenticateToken, async (req, res) => {
|
||||
if (parseInt(userId) === req.user.id) {
|
||||
return res.status(400).json({ error: 'Não é possível excluir o próprio usuário admin' });
|
||||
}
|
||||
await db.run('DELETE FROM users WHERE id = ?', [userId]);
|
||||
await db.run('DELETE FROM users WHERE id = $1', [userId]);
|
||||
res.json({ success: true, message: 'Usuário excluído com sucesso' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Erro ao excluir usuário' });
|
||||
@@ -200,7 +200,7 @@ router.post('/users/:id/token', authenticateToken, async (req, res) => {
|
||||
if (!req.user.is_admin) {
|
||||
return res.status(403).json({ error: 'Acesso negado' });
|
||||
}
|
||||
const user = await db.get('SELECT * FROM users WHERE id = ?', [req.params.id]);
|
||||
const user = await db.get('SELECT * FROM users WHERE id = $1', [req.params.id]);
|
||||
if (!user) return res.status(404).json({ error: 'Usuário não encontrado' });
|
||||
|
||||
// Gera um token com duração longa para uso no desktop (ex: 3650 dias = ~10 anos)
|
||||
@@ -229,7 +229,7 @@ router.put('/update-credentials', authenticateToken, async (req, res) => {
|
||||
}
|
||||
|
||||
// Buscar usuário atual para verificar a senha
|
||||
const user = await db.get('SELECT * FROM users WHERE id = ?', [userId]);
|
||||
const user = await db.get('SELECT * FROM users WHERE id = $1', [userId]);
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'Usuário não encontrado' });
|
||||
}
|
||||
@@ -246,7 +246,7 @@ router.put('/update-credentials', authenticateToken, async (req, res) => {
|
||||
|
||||
// Verificar e aplicar novo username se fornecido
|
||||
if (newUsername && newUsername !== user.username) {
|
||||
const existing = await db.get('SELECT * FROM users WHERE username = ? AND id != ?', [newUsername, userId]);
|
||||
const existing = await db.get('SELECT * FROM users WHERE username = $1 AND id != $2', [newUsername, userId]);
|
||||
if (existing) {
|
||||
return res.status(409).json({ error: 'Este nome de usuário já está em uso' });
|
||||
}
|
||||
@@ -255,7 +255,7 @@ router.put('/update-credentials', authenticateToken, async (req, res) => {
|
||||
|
||||
// Verificar e aplicar novo e-mail se fornecido
|
||||
if (newEmail && newEmail !== user.email) {
|
||||
const existing = await db.get('SELECT * FROM users WHERE email = ? AND id != ?', [newEmail, userId]);
|
||||
const existing = await db.get('SELECT * FROM users WHERE email = $1 AND id != $2', [newEmail, userId]);
|
||||
if (existing) {
|
||||
return res.status(409).json({ error: 'Este e-mail já está em uso' });
|
||||
}
|
||||
@@ -272,7 +272,7 @@ router.put('/update-credentials', authenticateToken, async (req, res) => {
|
||||
|
||||
// Atualizar no banco de dados
|
||||
await db.run(
|
||||
'UPDATE users SET username = ?, email = ?, password_hash = ? WHERE id = ?',
|
||||
'UPDATE users SET username = $1, email = $2, password_hash = $3 WHERE id = $4',
|
||||
[finalUsername, finalEmail, finalPasswordHash, userId]
|
||||
);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ router.post('/login', async (req, res) => {
|
||||
|
||||
try {
|
||||
// 1. Buscar o dispositivo pelo Token
|
||||
const device = await db.get(`SELECT * FROM clinics_devices WHERE machine_token = ?`, [machine_token]);
|
||||
const device = await db.get(`SELECT * FROM clinics_devices WHERE machine_token = $1`, [machine_token]);
|
||||
|
||||
if (!device) {
|
||||
return res.status(401).json({ success: false, message: 'Token da máquina inválido ou inexistente' });
|
||||
@@ -46,7 +46,7 @@ router.post('/login', async (req, res) => {
|
||||
|
||||
// Atualizar último IP conectado
|
||||
const clientIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
|
||||
await db.run(`UPDATE clinics_devices SET last_ip = ? WHERE id = ?`, [clientIp, device.id]);
|
||||
await db.run(`UPDATE clinics_devices SET last_ip = $1 WHERE id = $2`, [clientIp, device.id]);
|
||||
|
||||
// 5. Gerar Token JWT de Sessão
|
||||
// O Token vale por 2 horas, obrigando o cliente a re-logar ou pedir refresh
|
||||
|
||||
@@ -15,7 +15,7 @@ router.get('/', async (req, res) => {
|
||||
`SELECT g.*, COUNT(gi.id) as image_count
|
||||
FROM gtos g
|
||||
LEFT JOIN gto_images gi ON gi.gto_id = g.id
|
||||
WHERE g.patient_name = ?
|
||||
WHERE g.patient_name = $1
|
||||
GROUP BY g.id
|
||||
ORDER BY g.created_at DESC`,
|
||||
[patient]
|
||||
@@ -42,14 +42,14 @@ router.get('/', async (req, res) => {
|
||||
// ================================================================
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const gto = await db.get('SELECT * FROM gtos WHERE id = ?', [req.params.id]);
|
||||
const gto = await db.get('SELECT * FROM gtos WHERE id = $1', [req.params.id]);
|
||||
if (!gto) return res.status(404).json({ error: 'GTO não encontrada' });
|
||||
|
||||
const images = await db.all(
|
||||
`SELECT i.*, gi.id as link_id
|
||||
FROM gto_images gi
|
||||
JOIN images i ON i.id = gi.image_id
|
||||
WHERE gi.gto_id = ?
|
||||
WHERE gi.gto_id = $1
|
||||
ORDER BY gi.created_at DESC`,
|
||||
[req.params.id]
|
||||
);
|
||||
@@ -71,10 +71,10 @@ router.post('/', async (req, res) => {
|
||||
return res.status(400).json({ error: 'gto_number e patient_name são obrigatórios' });
|
||||
}
|
||||
const result = await db.run(
|
||||
`INSERT INTO gtos (gto_number, patient_name, description) VALUES (?, ?, ?)`,
|
||||
`INSERT INTO gtos (gto_number, patient_name, description) VALUES ($1, $2, $3) RETURNING id`,
|
||||
[gto_number.trim(), patient_name.trim(), description || '']
|
||||
);
|
||||
const gto = await db.get('SELECT * FROM gtos WHERE id = ?', [result.lastID]);
|
||||
const gto = await db.get('SELECT * FROM gtos WHERE id = $1', [result.lastID]);
|
||||
res.status(201).json(gto);
|
||||
} catch (err) {
|
||||
console.error('Erro ao criar GTO:', err);
|
||||
@@ -89,10 +89,10 @@ router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const { gto_number, description } = req.body;
|
||||
await db.run(
|
||||
`UPDATE gtos SET gto_number = ?, description = ? WHERE id = ?`,
|
||||
`UPDATE gtos SET gto_number = $1, description = $2 WHERE id = $3`,
|
||||
[gto_number, description || '', req.params.id]
|
||||
);
|
||||
const gto = await db.get('SELECT * FROM gtos WHERE id = ?', [req.params.id]);
|
||||
const gto = await db.get('SELECT * FROM gtos WHERE id = $1', [req.params.id]);
|
||||
res.json(gto);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
@@ -104,8 +104,8 @@ router.put('/:id', async (req, res) => {
|
||||
// ================================================================
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
await db.run('DELETE FROM gto_images WHERE gto_id = ?', [req.params.id]);
|
||||
await db.run('DELETE FROM gtos WHERE id = ?', [req.params.id]);
|
||||
await db.run('DELETE FROM gto_images WHERE gto_id = $1', [req.params.id]);
|
||||
await db.run('DELETE FROM gtos WHERE id = $1', [req.params.id]);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
@@ -123,13 +123,13 @@ router.post('/:id/images', async (req, res) => {
|
||||
|
||||
// Verificar se já está vinculada
|
||||
const existing = await db.get(
|
||||
'SELECT id FROM gto_images WHERE gto_id = ? AND image_id = ?',
|
||||
'SELECT id FROM gto_images WHERE gto_id = $1 AND image_id = $2',
|
||||
[req.params.id, image_id]
|
||||
);
|
||||
if (existing) return res.status(409).json({ error: 'Imagem já vinculada a esta GTO' });
|
||||
|
||||
await db.run(
|
||||
'INSERT INTO gto_images (gto_id, image_id) VALUES (?, ?)',
|
||||
'INSERT INTO gto_images (gto_id, image_id) VALUES ($1, $2) RETURNING id',
|
||||
[req.params.id, image_id]
|
||||
);
|
||||
res.status(201).json({ success: true, message: 'Imagem vinculada à GTO' });
|
||||
@@ -145,7 +145,7 @@ router.post('/:id/images', async (req, res) => {
|
||||
router.delete('/:id/images/:imageId', async (req, res) => {
|
||||
try {
|
||||
await db.run(
|
||||
'DELETE FROM gto_images WHERE gto_id = ? AND image_id = ?',
|
||||
'DELETE FROM gto_images WHERE gto_id = $1 AND image_id = $2',
|
||||
[req.params.id, req.params.imageId]
|
||||
);
|
||||
res.json({ success: true });
|
||||
|
||||
@@ -14,17 +14,19 @@ router.get('/', async (req, res) => {
|
||||
const search = req.query.search || '';
|
||||
const clientName = req.query.client || '';
|
||||
|
||||
let query = `SELECT * FROM images WHERE enabled = ?`;
|
||||
let query = `SELECT * FROM images WHERE enabled = $1`;
|
||||
let params = [showDisabled ? 0 : 1];
|
||||
let paramIndex = 2;
|
||||
|
||||
if (clientName) {
|
||||
query += ` AND client_name = ?`;
|
||||
query += ` AND client_name = $${paramIndex++}`;
|
||||
params.push(clientName);
|
||||
}
|
||||
|
||||
if (search) {
|
||||
query += ` AND (patient_name LIKE ? OR image_guid LIKE ? OR filename LIKE ?)`;
|
||||
query += ` AND (patient_name LIKE $${paramIndex} OR image_guid LIKE $${paramIndex + 1} OR filename LIKE $${paramIndex + 2})`;
|
||||
params.push(`%${search}%`, `%${search}%`, `%${search}%`);
|
||||
paramIndex += 3;
|
||||
}
|
||||
|
||||
query += ` ORDER BY created_at DESC LIMIT 100`;
|
||||
@@ -47,17 +49,19 @@ router.get('/patients', async (req, res) => {
|
||||
const clientName = req.query.client || '';
|
||||
const enabledVal = showDisabled ? 0 : 1;
|
||||
|
||||
let where = `WHERE enabled = ${enabledVal}`;
|
||||
let params = [];
|
||||
|
||||
let paramIndex = 1;
|
||||
let where = `WHERE enabled = $${paramIndex++}`;
|
||||
let params = [enabledVal];
|
||||
|
||||
if (clientName) {
|
||||
where += ` AND client_name = ?`;
|
||||
where += ` AND client_name = $${paramIndex++}`;
|
||||
params.push(clientName);
|
||||
}
|
||||
|
||||
|
||||
if (search) {
|
||||
where += ` AND (patient_name LIKE ? OR image_guid LIKE ?)`;
|
||||
where += ` AND (patient_name LIKE $${paramIndex} OR image_guid LIKE $${paramIndex + 1})`;
|
||||
params.push(`%${search}%`, `%${search}%`);
|
||||
paramIndex += 2;
|
||||
}
|
||||
|
||||
// Group by patient_name — pick thumbnail from most recent image
|
||||
@@ -102,7 +106,7 @@ router.get('/by-patient', async (req, res) => {
|
||||
|
||||
const images = await db.all(
|
||||
`SELECT * FROM images
|
||||
WHERE patient_name = ? AND enabled = ?
|
||||
WHERE patient_name = $1 AND enabled = $2
|
||||
ORDER BY created_at DESC`,
|
||||
[patientName, showDisabled ? 0 : 1]
|
||||
);
|
||||
@@ -119,7 +123,7 @@ router.get('/by-patient', async (req, res) => {
|
||||
// ================================================================
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const image = await db.get('SELECT * FROM images WHERE id = ?', [req.params.id]);
|
||||
const image = await db.get('SELECT * FROM images WHERE id = $1', [req.params.id]);
|
||||
if (!image) {
|
||||
return res.status(404).json({ error: 'Imagem não encontrada' });
|
||||
}
|
||||
@@ -155,14 +159,14 @@ router.get('/stats', async (req, res) => {
|
||||
// ================================================================
|
||||
router.get('/:id/transformations', async (req, res) => {
|
||||
try {
|
||||
const image = await db.get('SELECT * FROM images WHERE id = ?', [req.params.id]);
|
||||
const image = await db.get('SELECT * FROM images WHERE id = $1', [req.params.id]);
|
||||
if (!image) {
|
||||
return res.status(404).json({ error: 'Imagem não encontrada' });
|
||||
}
|
||||
|
||||
// Buscar a imagem original se esta é uma transformação
|
||||
const originalId = image.original_image_id || image.id;
|
||||
const originalImage = await db.get('SELECT * FROM images WHERE id = ?', [originalId]);
|
||||
const originalImage = await db.get('SELECT * FROM images WHERE id = $1', [originalId]);
|
||||
|
||||
const originalPath = path.join(__dirname, '../uploads', originalImage.filename);
|
||||
let originalBuffer;
|
||||
@@ -217,7 +221,7 @@ router.get('/:id/transformations', async (req, res) => {
|
||||
router.post('/:id/transform', async (req, res) => {
|
||||
try {
|
||||
const { rotation, flipH, flipV, remark } = req.body;
|
||||
const originalImage = await db.get('SELECT * FROM images WHERE id = ?', [req.params.id]);
|
||||
const originalImage = await db.get('SELECT * FROM images WHERE id = $1', [req.params.id]);
|
||||
|
||||
if (!originalImage) {
|
||||
return res.status(404).json({ error: 'Imagem não encontrada' });
|
||||
@@ -225,7 +229,7 @@ router.post('/:id/transform', async (req, res) => {
|
||||
|
||||
// Buscar a imagem original real se esta é uma transformação
|
||||
const realOriginalId = originalImage.original_image_id || originalImage.id;
|
||||
const realOriginal = await db.get('SELECT * FROM images WHERE id = ?', [realOriginalId]);
|
||||
const realOriginal = await db.get('SELECT * FROM images WHERE id = $1', [realOriginalId]);
|
||||
|
||||
// Processar a imagem
|
||||
const originalPath = path.join(__dirname, '../uploads', realOriginal.filename);
|
||||
@@ -250,9 +254,9 @@ router.post('/:id/transform', async (req, res) => {
|
||||
const processedPath = path.join(__dirname, '../processed', processedFilename);
|
||||
await fs.writeFile(processedPath, processed);
|
||||
|
||||
// Desabilitar a imagem atual para que não fique duplicada na interface
|
||||
await db.run('UPDATE images SET enabled = 0 WHERE id = ?', [req.params.id]);
|
||||
|
||||
// Desabilitar a imagem active atual para que não fique duplicada na interface
|
||||
await db.run('UPDATE images SET enabled = 0 WHERE id = $1', [req.params.id]);
|
||||
|
||||
const newRemark = remark !== undefined ? remark : originalImage.remark;
|
||||
|
||||
// Inserir a nova imagem como a versão ativa
|
||||
@@ -260,7 +264,7 @@ router.post('/:id/transform', async (req, res) => {
|
||||
`INSERT INTO images (
|
||||
image_guid, patient_name, client_name, original_filename, filename,
|
||||
enabled, rotation, flip_horizontal, flip_vertical, original_image_id, created_at, remark, doctor
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
|
||||
[
|
||||
`${realOriginal.image_guid}_${timestamp}`,
|
||||
realOriginal.patient_name,
|
||||
@@ -298,11 +302,11 @@ router.put('/:id/patient-info', async (req, res) => {
|
||||
|
||||
await db.run(
|
||||
`UPDATE images SET
|
||||
patient_name_resumed = ?,
|
||||
tooth_number = ?,
|
||||
tooth_side = ?,
|
||||
patient_name_resumed = $1,
|
||||
tooth_number = $2,
|
||||
tooth_side = $3,
|
||||
enabled = 1
|
||||
WHERE id = ?`,
|
||||
WHERE id = $4`,
|
||||
[patientNameResumed, toothNumber, toothSide, req.params.id]
|
||||
);
|
||||
|
||||
@@ -317,7 +321,7 @@ router.put('/:id/patient-info', async (req, res) => {
|
||||
// ================================================================
|
||||
router.get('/:id/download', async (req, res) => {
|
||||
try {
|
||||
const image = await db.get('SELECT * FROM images WHERE id = ?', [req.params.id]);
|
||||
const image = await db.get('SELECT * FROM images WHERE id = $1', [req.params.id]);
|
||||
if (!image) {
|
||||
return res.status(404).json({ error: 'Imagem não encontrada' });
|
||||
}
|
||||
@@ -344,7 +348,7 @@ router.get('/:id/download', async (req, res) => {
|
||||
// ================================================================
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
const image = await db.get('SELECT * FROM images WHERE id = ?', [req.params.id]);
|
||||
const image = await db.get('SELECT * FROM images WHERE id = $1', [req.params.id]);
|
||||
if (!image) {
|
||||
return res.status(404).json({ error: 'Imagem não encontrada' });
|
||||
}
|
||||
@@ -366,7 +370,7 @@ router.delete('/:id', async (req, res) => {
|
||||
}
|
||||
|
||||
// Apagar do banco
|
||||
await db.run('DELETE FROM images WHERE id = ?', [req.params.id]);
|
||||
await db.run('DELETE FROM images WHERE id = $1', [req.params.id]);
|
||||
|
||||
res.json({ success: true, message: 'Imagem apagada com sucesso' });
|
||||
} catch (error) {
|
||||
@@ -380,13 +384,13 @@ router.delete('/:id', async (req, res) => {
|
||||
// ================================================================
|
||||
router.put('/:id/toggle-enabled', async (req, res) => {
|
||||
try {
|
||||
const image = await db.get('SELECT * FROM images WHERE id = ?', [req.params.id]);
|
||||
const image = await db.get('SELECT * FROM images WHERE id = $1', [req.params.id]);
|
||||
if (!image) {
|
||||
return res.status(404).json({ error: 'Imagem não encontrada' });
|
||||
}
|
||||
|
||||
const newEnabled = image.enabled ? 0 : 1;
|
||||
await db.run('UPDATE images SET enabled = ? WHERE id = ?', [newEnabled, req.params.id]);
|
||||
await db.run('UPDATE images SET enabled = $1 WHERE id = $2', [newEnabled, req.params.id]);
|
||||
|
||||
res.json({ success: true, enabled: newEnabled === 1 });
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
// Caminho do banco de dados oficial do Dental Sensor
|
||||
const DENTAL_DB_PATH = 'C:/ProgramData/RF/Dental Sensor/data';
|
||||
|
||||
// ================================================================
|
||||
// Conexão LAZY — abre o banco só quando necessário e fecha logo
|
||||
// após, evitando conflitos de lock com o banco principal do servidor
|
||||
// ================================================================
|
||||
function openDentalDb() {
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
return new DatabaseSync(DENTAL_DB_PATH);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// POST /api/patients - Criar novo paciente no Dental Sensor
|
||||
// ================================================================
|
||||
router.post('/', (req, res) => {
|
||||
let db;
|
||||
try {
|
||||
db = openDentalDb();
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao conectar ao banco do Dental Sensor:', error.message);
|
||||
return res.status(500).json({
|
||||
error: 'Não foi possível conectar ao banco do Dental Sensor. Verifique se o software está instalado.',
|
||||
detail: error.message
|
||||
});
|
||||
}
|
||||
|
||||
const { firstName, lastName, doctor, birthday, gender, phone, email, address, zipCode, remark } = req.body;
|
||||
|
||||
if (!firstName || !lastName) {
|
||||
if (db) try { db.close(); } catch (_) {}
|
||||
return res.status(400).json({ error: 'Nome e Sobrenome são obrigatórios.' });
|
||||
}
|
||||
|
||||
const formattedBirthday = birthday ? `${birthday} 00:00:00` : null;
|
||||
|
||||
try {
|
||||
const insertStmt = db.prepare(`
|
||||
INSERT INTO Patients (
|
||||
FirstName, LastName, Doctor, Birthday, Gender, Phone, Email, Address, ZipCode, Remark, LastActTime, VFlag
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), 1)
|
||||
`);
|
||||
|
||||
const result = insertStmt.run(
|
||||
firstName,
|
||||
lastName,
|
||||
doctor || '',
|
||||
formattedBirthday,
|
||||
gender !== undefined ? parseInt(gender, 10) : 0,
|
||||
phone || '',
|
||||
email || '',
|
||||
address || '',
|
||||
zipCode || '',
|
||||
remark || ''
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
patientId: result.lastInsertRowid,
|
||||
message: 'Paciente cadastrado com sucesso no Dental Sensor'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao inserir paciente:', error);
|
||||
res.status(500).json({ error: 'Erro ao cadastrar paciente: ' + error.message });
|
||||
} finally {
|
||||
// SEMPRE fechar o banco após o uso para liberar o lock
|
||||
if (db) {
|
||||
try { db.close(); } catch (_) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// GET /api/patients - Listar pacientes do Dental Sensor (opcional)
|
||||
// ================================================================
|
||||
router.get('/', (req, res) => {
|
||||
let db;
|
||||
try {
|
||||
db = openDentalDb();
|
||||
const search = req.query.search || '';
|
||||
let stmt;
|
||||
let rows;
|
||||
|
||||
if (search) {
|
||||
stmt = db.prepare(`
|
||||
SELECT ID, FirstName, LastName, Doctor, Birthday, Gender, Phone, Remark
|
||||
FROM Patients
|
||||
WHERE (FirstName LIKE ? OR LastName LIKE ?)
|
||||
AND VFlag = 1
|
||||
ORDER BY LastActTime DESC
|
||||
LIMIT 100
|
||||
`);
|
||||
rows = stmt.all(`%${search}%`, `%${search}%`);
|
||||
} else {
|
||||
stmt = db.prepare(`
|
||||
SELECT ID, FirstName, LastName, Doctor, Birthday, Gender, Phone, Remark
|
||||
FROM Patients
|
||||
WHERE VFlag = 1
|
||||
ORDER BY LastActTime DESC
|
||||
LIMIT 100
|
||||
`);
|
||||
rows = stmt.all();
|
||||
}
|
||||
|
||||
res.json(rows);
|
||||
} catch (error) {
|
||||
console.error('Erro ao listar pacientes do Dental Sensor:', error.message);
|
||||
res.status(500).json({ error: 'Erro ao consultar banco do Dental Sensor: ' + error.message });
|
||||
} finally {
|
||||
if (db) {
|
||||
try { db.close(); } catch (_) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -18,7 +18,7 @@ router.delete('/factory-reset', async (req, res) => {
|
||||
}
|
||||
|
||||
// 1. Validar usuário e senha
|
||||
const user = await db.get('SELECT * FROM users WHERE id = ?', [userId]);
|
||||
const user = await db.get('SELECT * FROM users WHERE id = $1', [userId]);
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'Usuário não encontrado' });
|
||||
}
|
||||
|
||||
+8
-10
@@ -14,7 +14,6 @@ const jwt = require('jsonwebtoken');
|
||||
const db = require('./database');
|
||||
const authRoutes = require('./routes/auth');
|
||||
const imageRoutes = require('./routes/images');
|
||||
const patientsRouter = require('./routes/patients');
|
||||
const gtosRouter = require('./routes/gtos');
|
||||
const systemRoutes = require('./routes/system');
|
||||
const adminClinicsRoutes = require('./routes/admin-clinics');
|
||||
@@ -60,7 +59,6 @@ app.use(cookieParser());
|
||||
// ================================================================
|
||||
// ROTAS DA API
|
||||
// ================================================================
|
||||
app.use('/api/patients', patientsRouter);
|
||||
|
||||
// ================================================================
|
||||
// ROTAS PÚBLICAS (SEMPRE ACESSÍVEIS)
|
||||
@@ -104,7 +102,7 @@ app.get('/api/clients/check-name', async (req, res) => {
|
||||
}
|
||||
|
||||
// 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 row = await db.get('SELECT COUNT(*) as count FROM images WHERE LOWER(client_name) = LOWER($1)', [name]);
|
||||
const existsInDb = row && row.count > 0;
|
||||
|
||||
// 2. Verificar se existe algum cliente atualmente conectado com esse nome (desconsiderando o próprio clientId)
|
||||
@@ -348,10 +346,10 @@ app.post('/api/install', async (req, res) => {
|
||||
const passwordHash = await hashPassword(admin.password);
|
||||
await db.run(
|
||||
`INSERT INTO users (username, email, password_hash, created_at)
|
||||
VALUES (?, ?, ?, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
email = VALUES(email),
|
||||
password_hash = VALUES(password_hash)`,
|
||||
VALUES ($1, $2, $3, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (username) DO UPDATE SET
|
||||
email = EXCLUDED.email,
|
||||
password_hash = EXCLUDED.password_hash`,
|
||||
[admin.username, admin.email, passwordHash]
|
||||
);
|
||||
console.log('✅ Usuário admin criado:', admin.username);
|
||||
@@ -749,7 +747,7 @@ io.on('connection', (socket) => {
|
||||
|
||||
// Verificar se a imagem já foi inserida no banco
|
||||
const existing = await db.get(
|
||||
"SELECT id, filename FROM images WHERE original_filename = ? AND enabled = 1 LIMIT 1",
|
||||
"SELECT id, filename FROM images WHERE original_filename = $1 AND enabled = 1 LIMIT 1",
|
||||
[data.metadata.fileName]
|
||||
);
|
||||
if (existing) {
|
||||
@@ -806,7 +804,7 @@ io.on('connection', (socket) => {
|
||||
doctor,
|
||||
remark,
|
||||
created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||||
[
|
||||
data.metadata.guid,
|
||||
data.patientData.name || 'Paciente não identificado',
|
||||
@@ -873,7 +871,7 @@ io.on('connection', (socket) => {
|
||||
}
|
||||
|
||||
const existing = await db.get(
|
||||
"SELECT id, filename FROM images WHERE original_filename = ? AND enabled = 1 LIMIT 1",
|
||||
"SELECT id, filename FROM images WHERE original_filename = $1 AND enabled = 1 LIMIT 1",
|
||||
[data.fileName]
|
||||
);
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ async function loadConfigFromDb() {
|
||||
let hasSettings = false;
|
||||
|
||||
for (const k of keys) {
|
||||
const row = await db.get('SELECT value FROM settings WHERE key = ?', [k]);
|
||||
const row = await db.get('SELECT value FROM settings WHERE key = $1', [k]);
|
||||
if (row) {
|
||||
// Mapear de snake_case do banco para camelCase do config
|
||||
const camelKey = k.replace(/_([a-z])/g, (g) => g[1].toUpperCase());
|
||||
@@ -146,7 +146,7 @@ async function updateConfig(newConfig) {
|
||||
for (const [key, val] of Object.entries(settingsMap)) {
|
||||
await db.run(
|
||||
`INSERT INTO settings (key, value)
|
||||
VALUES (?, ?)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (key)
|
||||
DO UPDATE SET value = EXCLUDED.value`,
|
||||
[key, val]
|
||||
@@ -250,7 +250,7 @@ async function getImageBuffer(filename, clientName = null, patientName = null, i
|
||||
if (!cName || !pName) {
|
||||
try {
|
||||
const db = require('./database');
|
||||
const row = await db.get('SELECT client_name, patient_name FROM images WHERE filename = ? OR thumb_filename = ? LIMIT 1', [filename, filename]);
|
||||
const row = await db.get('SELECT client_name, patient_name FROM images WHERE filename = $1 OR thumb_filename = $2 LIMIT 1', [filename, filename]);
|
||||
if (row) {
|
||||
cName = row.client_name;
|
||||
pName = row.patient_name;
|
||||
|
||||
Reference in New Issue
Block a user