Files
rx.scoreodonto.com-client/index.js
T

445 lines
14 KiB
JavaScript

const { app, BrowserWindow, Tray, Menu, ipcMain, shell, dialog } = require('electron');
const path = require('path');
const fs = require('fs');
const { fork } = require('child_process');
const { autoUpdater } = require('electron-updater');
let tray = null;
let configWindow = null;
let monitorProcess = null;
let currentStatus = { connected: false, identified: false };
let downloadProgress = null; // Variável para controlar o progresso de download
const recentLogs = [];
// ================================================================
// AUTO-UPDATER CONFIG
// ================================================================
autoUpdater.autoDownload = true;
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.logger = require('electron').app ? console : null;
autoUpdater.on('checking-for-update', () => {
console.log('🔍 Verificando atualizações...');
});
autoUpdater.on('update-available', (info) => {
console.log('🔄 Nova versão disponível:', info.version);
downloadProgress = 0; // Inicializa o progresso
updateTrayMenu(); // Atualiza o menu para mostrar a aba "Baixando..."
if (tray) {
tray.displayBalloon({
title: 'Nova Atualização Encontrada',
content: `A versão ${info.version} está sendo baixada. Clique com botão direito no ícone para acompanhar o progresso!`,
iconType: 'info'
});
}
});
autoUpdater.on('update-not-available', () => {
console.log('✅ Aplicativo está na versão mais recente.');
});
autoUpdater.on('download-progress', (progress) => {
const p = Math.round(progress.percent);
console.log(`⬇️ Baixando atualização: ${p}%`);
if (downloadProgress !== p) {
downloadProgress = p;
// Atualiza a bandeja com throttling para não piscar demais
if (p % 5 === 0 || p === 100) {
updateTrayMenu();
}
}
});
autoUpdater.on('update-downloaded', (info) => {
console.log('✅ Atualização baixada:', info.version, '— será instalada ao fechar o app.');
downloadProgress = "Pronto";
updateTrayMenu();
// Notificar o usuário na tray
if (tray) {
tray.displayBalloon({
title: 'ScoreOdonto - Atualização Pronta',
content: `Versão ${info.version} será instalada automaticamente ao fechar o aplicativo.`,
iconType: 'info'
});
}
});
autoUpdater.on('error', (err) => {
console.error('❌ Erro no auto-updater:', err.message);
downloadProgress = null;
updateTrayMenu();
});
const appDataFolder = app.getPath('userData');
const configPath = path.join(appDataFolder, 'config.json');
// Ensure directory exists
if (!fs.existsSync(appDataFolder)) {
fs.mkdirSync(appDataFolder, { recursive: true });
}
// Default config template
const defaultConfig = {
serverUrl: "https://rx.scoreodonto.com",
monitorPath: "C:\\ProgramData\\RF\\Dental Sensor\\Images",
clientName: "DIGITE-O-NOME-DA-MAQUINA",
clientType: "windows",
apiKey: "rf-dental-secure-key-2026",
serverEmail: "",
serverPassword: "",
token: "",
adminUsername: "admin",
adminPassword: "admin"
};
function loadConfig() {
try {
if (!fs.existsSync(configPath)) {
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), 'utf8');
return defaultConfig;
}
const userConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
// Ensure required fields exist
return { ...defaultConfig, ...userConfig };
} catch (e) {
console.error("Error loading config:", e);
return defaultConfig;
}
}
function saveConfig(newConfig) {
try {
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2), 'utf8');
return true;
} catch (e) {
console.error("Error saving config:", e);
return false;
}
}
// Start background child process
function startMonitorProcess() {
if (monitorProcess) {
monitorProcess.kill();
}
const config = loadConfig();
// Don't start if it's default/placeholder
if (!config.clientName || config.clientName === defaultConfig.clientName) {
console.log("Monitor process not started: clientName not configured.");
return;
}
console.log("Starting client-monitor.js as background process...");
monitorProcess = fork(path.join(__dirname, 'client-monitor.js'), [], {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
env: {
...process.env,
DENTAL_CONFIG_DIR: appDataFolder
}
});
monitorProcess.on('message', (message) => {
if (message && message.event === 'status') {
currentStatus.connected = message.connected;
currentStatus.identified = message.identified;
updateTrayMenu();
} else if (message && message.event === 'log') {
recentLogs.push(message.log);
if (recentLogs.length > 100) recentLogs.shift();
if (configWindow && !configWindow.isDestroyed()) {
configWindow.webContents.send('new-log', message.log);
}
}
});
monitorProcess.on('exit', (code) => {
console.log(`Monitor process exited with code ${code}`);
currentStatus.connected = false;
currentStatus.identified = false;
updateTrayMenu();
});
}
function createConfigWindow() {
if (configWindow) {
configWindow.focus();
return;
}
configWindow = new BrowserWindow({
width: 600,
height: 680,
minHeight: 520,
title: "ScoreOdonto Dental Client - Configuração",
icon: path.join(__dirname, 'favicon.png'),
resizable: true,
maximizable: true,
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
});
configWindow.loadFile(path.join(__dirname, 'config-ui.html'));
configWindow.on('closed', () => {
configWindow = null;
});
}
function updateTrayMenu() {
if (!tray) return;
let statusText = "ScoreOdonto: Desconectado 🔴";
if (currentStatus.connected) {
statusText = currentStatus.identified
? "ScoreOdonto: Identificado 🟢"
: "ScoreOdonto: Conectado... 🟡";
}
const template = [
{ label: statusText, enabled: false },
{ type: 'separator' }
];
// Se houver um download em andamento, mostra a barra no menu
if (downloadProgress !== null) {
if (downloadProgress === "Pronto") {
template.push({ label: '📦 Atualização pronta para instalar (reinicie o app)', enabled: false });
} else {
template.push({ label: `⬇️ Baixando atualização: ${downloadProgress}%`, enabled: false });
}
template.push({ type: 'separator' });
}
template.push(
{
label: 'Forçar Sincronização 🔄',
click: () => {
if (monitorProcess) {
monitorProcess.send({ command: 'sync' });
} else {
startMonitorProcess();
}
}
},
{
label: 'Verificar Atualizações 🔄',
click: () => {
autoUpdater.checkForUpdatesAndNotify();
}
},
{
label: 'Configurações ⚙️',
click: () => {
createConfigWindow();
}
},
{
label: 'Abrir Pasta de Imagens 📂',
click: () => {
const config = loadConfig();
if (fs.existsSync(config.monitorPath)) {
shell.openPath(config.monitorPath);
} else {
shell.openPath(path.dirname(config.monitorPath));
}
}
},
{ type: 'separator' },
{
label: 'Sair 🚪',
click: () => {
app.isQuitting = true;
app.quit();
}
}
);
const contextMenu = Menu.buildFromTemplate(template);
tray.setContextMenu(contextMenu);
tray.setToolTip(`ScoreOdonto Dental Client\n${statusText}`);
}
function initTray() {
const iconPath = path.join(__dirname, 'favicon.png');
tray = new Tray(iconPath);
updateTrayMenu();
tray.on('double-click', () => {
createConfigWindow();
});
}
// Single instance lock
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
return;
}
app.on('second-instance', () => {
if (configWindow) {
if (configWindow.isMinimized()) configWindow.restore();
configWindow.focus();
} else {
createConfigWindow();
}
});
app.whenReady().then(() => {
initTray();
// Verificar atualizações silenciosamente ao iniciar
autoUpdater.checkForUpdatesAndNotify();
const config = loadConfig();
// If not configured, show config UI
if (!config.clientName || config.clientName === defaultConfig.clientName) {
createConfigWindow();
} else {
// Start background process
startMonitorProcess();
}
});
app.on('window-all-closed', () => {
// Keep running in tray on close
if (process.platform !== 'darwin') {
// do nothing, let it run in tray
}
});
app.on('before-quit', () => {
if (monitorProcess) {
monitorProcess.kill();
}
});
// IPC communication
ipcMain.handle('get-config', () => {
return loadConfig();
});
ipcMain.handle('get-version', () => {
return app.getVersion();
});
ipcMain.handle('get-logs', () => {
return recentLogs;
});
ipcMain.handle('save-config', (event, newConfig) => {
const success = saveConfig(newConfig);
if (success) {
// Restart child process with new settings
startMonitorProcess();
}
return success;
});
ipcMain.handle('test-server', async (event, url) => {
return new Promise((resolve) => {
try {
const checkUrl = url.endsWith('/') ? `${url}status` : `${url}/status`;
const clientModule = checkUrl.startsWith('https') ? require('https') : require('http');
const req = clientModule.get(checkUrl, { timeout: 4000 }, (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve({ success: true, message: `Conexão bem sucedida! Código: ${res.statusCode}` });
} else {
resolve({ success: false, error: `Código de status inesperado: ${res.statusCode}` });
}
});
req.on('error', (err) => {
resolve({ success: false, error: err.message });
});
req.on('timeout', () => {
req.destroy();
resolve({ success: false, error: 'Timeout de 4 segundos esgotado.' });
});
} catch (e) {
resolve({ success: false, error: e.message });
}
});
});
ipcMain.handle('verify-credentials', async (event, url, email, password, token) => {
return new Promise((resolve) => {
try {
let base = url.trim();
if (base.endsWith('/')) {
base = base.slice(0, -1);
}
const verifyUrl = `${base}/api/auth/verify-client-credentials`;
const clientModule = verifyUrl.startsWith('https') ? require('https') : require('http');
const parsedUrl = new URL(verifyUrl);
const postData = JSON.stringify({ email, password, token });
const options = {
hostname: parsedUrl.hostname,
port: parsedUrl.port || (verifyUrl.startsWith('https') ? 443 : 80),
path: parsedUrl.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData)
},
timeout: 8000
};
const req = clientModule.request(options, (res) => {
let body = '';
res.on('data', (chunk) => body += chunk);
res.on('end', () => {
try {
const data = JSON.parse(body);
if (res.statusCode >= 200 && res.statusCode < 300 && data.success) {
resolve({ success: true, clinic: data.clinic });
} else {
resolve({ success: false, error: data.error || `Erro de validação: ${res.statusCode}` });
}
} catch (e) {
resolve({ success: false, error: `Resposta inválida do servidor: ${e.message}` });
}
});
});
req.on('error', (err) => {
resolve({ success: false, error: `Falha de rede: ${err.message}` });
});
req.on('timeout', () => {
req.destroy();
resolve({ success: false, error: 'Tempo limite esgotado ao tentar validar credenciais (8s).' });
});
req.write(postData);
req.end();
} catch (e) {
resolve({ success: false, error: `Erro interno: ${e.message}` });
}
});
});
ipcMain.handle('open-folder', () => {
const config = loadConfig();
const targetPath = config.monitorPath;
if (!fs.existsSync(targetPath)) {
fs.mkdirSync(targetPath, { recursive: true });
}
shell.openPath(targetPath);
return true;
});
ipcMain.on('close-window', () => {
if (configWindow) {
configWindow.close();
}
});