chore: test deploy script automatico
continuous-integration/webhook Deploy concluido (rx.scoreodonto.com)

This commit is contained in:
VPS 4 Builder
2026-05-26 06:45:18 +02:00
parent 15156313b6
commit a34ff8ff3d
12 changed files with 5791 additions and 0 deletions
@@ -0,0 +1,616 @@
:root {
--primary: #4361ee;
--primary-hover: #3a56d4;
--primary-light: rgba(67, 97, 238, 0.1);
--success: #2ec4b6;
--success-light: rgba(46, 196, 182, 0.15);
--danger: #ef476f;
--danger-light: rgba(239, 71, 111, 0.15);
--warning: #ff9f1c;
--dark: #0b132b;
--gray-900: #1c2541;
--gray-800: #2b3a67;
--gray-200: #e2e8f0;
--light: #f8f9fa;
--white: #ffffff;
--glass-bg: rgba(255, 255, 255, 0.05);
--glass-border: rgba(255, 255, 255, 0.1);
--glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
--font-heading: 'Outfit', sans-serif;
--font-body: 'Inter', sans-serif;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-body);
background: linear-gradient(135deg, var(--dark) 0%, var(--gray-900) 100%);
color: var(--light);
min-height: 100vh;
display: flex;
overflow-x: hidden;
}
/* Glassmorphism Classes */
.glass-card {
background: var(--glass-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--glass-border);
border-radius: 16px;
box-shadow: var(--glass-shadow);
}
/* Animations */
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(67, 97, 238, 0.4); }
70% { box-shadow: 0 0 0 10px rgba(67, 97, 238, 0); }
100% { box-shadow: 0 0 0 0 rgba(67, 97, 238, 0); }
}
.pulse-animation {
animation: pulse 2s infinite;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Sidebar */
.sidebar {
width: 260px;
background: rgba(11, 19, 43, 0.8);
border-right: 1px solid var(--glass-border);
display: flex;
flex-direction: column;
padding: 24px 0;
z-index: 10;
}
.sidebar-header {
padding: 0 24px 32px;
display: flex;
align-items: center;
gap: 12px;
}
.logo-icon {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #4cc9f0, var(--primary));
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: white;
}
.sidebar-header h2 {
font-family: var(--font-heading);
font-size: 22px;
font-weight: 600;
letter-spacing: 0.5px;
}
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 8px;
padding: 0 16px;
flex: 1;
}
.nav-item {
padding: 12px 16px;
border-radius: 10px;
color: #a0aec0;
text-decoration: none;
font-weight: 500;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.3s ease;
}
.nav-item i {
font-size: 18px;
width: 20px;
text-align: center;
}
.nav-item:hover {
background: var(--glass-bg);
color: var(--white);
transform: translateX(4px);
}
.nav-item.active {
background: var(--primary-light);
color: #4cc9f0;
border-left: 3px solid #4cc9f0;
}
.sidebar-footer {
padding: 0 24px;
}
.logout-btn {
width: 100%;
padding: 12px;
background: transparent;
border: 1px solid var(--glass-border);
color: #a0aec0;
border-radius: 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-family: var(--font-body);
font-weight: 500;
transition: all 0.3s;
}
.logout-btn:hover {
background: var(--danger-light);
color: var(--danger);
border-color: var(--danger);
}
/* Main Content */
.main-content {
flex: 1;
padding: 32px 40px;
overflow-y: auto;
}
.top-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40px;
animation: fadeIn 0.5s ease-out;
}
.header-title h1 {
font-family: var(--font-heading);
font-size: 32px;
margin-bottom: 8px;
}
.header-title p {
color: #a0aec0;
font-size: 15px;
}
/* Buttons */
.primary-btn {
background: var(--primary);
color: white;
border: none;
padding: 12px 24px;
border-radius: 10px;
font-family: var(--font-body);
font-weight: 600;
font-size: 15px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(67, 97, 238, 0.3);
}
.primary-btn:hover {
background: var(--primary-hover);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(67, 97, 238, 0.4);
}
.secondary-btn {
background: transparent;
color: var(--white);
border: 1px solid var(--glass-border);
padding: 12px 24px;
border-radius: 10px;
font-family: var(--font-body);
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.secondary-btn:hover {
background: var(--glass-bg);
}
.icon-btn {
background: transparent;
border: none;
color: #a0aec0;
cursor: pointer;
padding: 6px;
border-radius: 6px;
transition: all 0.2s;
}
.icon-btn:hover {
color: var(--white);
background: var(--glass-bg);
}
.icon-btn.delete:hover {
color: var(--danger);
background: var(--danger-light);
}
/* Stats */
.dashboard-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
margin-bottom: 40px;
animation: fadeIn 0.6s ease-out;
}
.stat-card {
padding: 24px;
display: flex;
align-items: center;
gap: 20px;
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.bg-blue { background: rgba(67, 97, 238, 0.15); color: #4cc9f0; }
.bg-green { background: var(--success-light); color: var(--success); }
.bg-red { background: var(--danger-light); color: var(--danger); }
.stat-info h3 {
font-size: 14px;
color: #a0aec0;
font-weight: 500;
margin-bottom: 4px;
}
.stat-info p {
font-family: var(--font-heading);
font-size: 28px;
font-weight: 700;
}
/* Table */
.data-section {
padding: 24px;
animation: fadeIn 0.7s ease-out;
}
.table-container {
overflow-x: auto;
}
.modern-table {
width: 100%;
border-collapse: separate;
border-spacing: 0 8px;
}
.modern-table th {
text-align: left;
padding: 0 16px 12px;
color: #a0aec0;
font-weight: 500;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.modern-table td {
padding: 16px;
background: rgba(255, 255, 255, 0.02);
}
.modern-table tr td:first-child {
border-radius: 10px 0 0 10px;
}
.modern-table tr td:last-child {
border-radius: 0 10px 10px 0;
}
.modern-table tbody tr {
transition: all 0.2s;
}
.modern-table tbody tr:hover td {
background: rgba(255, 255, 255, 0.05);
}
/* Status Badge */
.status-badge {
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 6px;
}
.status-active {
background: var(--success-light);
color: var(--success);
}
.status-inactive {
background: var(--danger-light);
color: var(--danger);
}
.token-blur {
filter: blur(4px);
transition: filter 0.3s;
cursor: pointer;
font-family: monospace;
background: rgba(0,0,0,0.2);
padding: 4px 8px;
border-radius: 4px;
}
.token-blur:hover {
filter: blur(0);
}
/* Modals */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(5px);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.modal-overlay.active {
opacity: 1;
visibility: visible;
}
.modal-content {
width: 100%;
max-width: 500px;
padding: 32px;
transform: scale(0.9);
transition: all 0.3s ease;
}
.modal-overlay.active .modal-content {
transform: scale(1);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.modal-header h2 {
font-family: var(--font-heading);
}
/* Forms */
.modern-form .form-group {
margin-bottom: 20px;
}
.modern-form label {
display: block;
margin-bottom: 8px;
font-size: 14px;
color: #a0aec0;
}
.input-with-icon {
position: relative;
display: flex;
align-items: center;
}
.input-with-icon i {
position: absolute;
left: 16px;
color: #a0aec0;
}
.input-with-icon input {
width: 100%;
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--glass-border);
color: white;
padding: 14px 16px 14px 45px;
border-radius: 10px;
font-family: var(--font-body);
font-size: 15px;
transition: all 0.3s;
}
.input-with-icon input:focus {
outline: none;
border-color: var(--primary);
background: rgba(0, 0, 0, 0.4);
}
.form-group small {
display: block;
margin-top: 6px;
color: #718096;
font-size: 12px;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 16px;
margin-top: 32px;
}
/* Success Modal */
.success-modal {
text-align: center;
}
.success-icon {
font-size: 64px;
color: var(--success);
margin-bottom: 16px;
}
.success-modal h2 {
margin-bottom: 12px;
}
.success-modal p {
color: #a0aec0;
margin-bottom: 24px;
font-size: 14px;
line-height: 1.5;
}
.token-container {
background: rgba(0, 0, 0, 0.3);
padding: 16px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid var(--primary-light);
}
.token-container code {
font-family: monospace;
font-size: 16px;
color: #4cc9f0;
word-break: break-all;
}
/* Toast */
.toast {
position: fixed;
bottom: 24px;
right: 24px;
padding: 16px 24px;
background: var(--glass-bg);
backdrop-filter: blur(12px);
border-left: 4px solid var(--primary);
color: white;
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
transform: translateX(120%);
transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
z-index: 1000;
}
.toast.show {
transform: translateX(0);
}
.toast.error {
border-left-color: var(--danger);
}
.toast.success {
border-left-color: var(--success);
}
.loading-cell {
text-align: center;
padding: 40px !important;
color: #a0aec0;
}
.spinner {
width: 30px;
height: 30px;
border: 3px solid rgba(255,255,255,0.1);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Custom Switch Toggle */
.toggle-switch {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(255,255,255,0.1);
transition: .4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: #a0aec0;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--success);
}
input:checked + .slider:before {
transform: translateX(20px);
background-color: white;
}
@@ -0,0 +1,201 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gerenciamento de Clínicas - RF Dental</title>
<!-- Fontes -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<!-- Ícones -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Estilos -->
<link rel="stylesheet" href="admin-clinics.css">
</head>
<body>
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-header">
<div class="logo-icon">
<i class="fa-solid fa-tooth"></i>
</div>
<h2>RF Dental</h2>
</div>
<nav class="sidebar-nav">
<a href="/" class="nav-item">
<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>
<a href="/admin-clinics" class="nav-item active">
<i class="fa-solid fa-hospital"></i> Clínicas e Acessos
</a>
</nav>
<div class="sidebar-footer">
<button id="logoutBtn" class="logout-btn">
<i class="fa-solid fa-arrow-right-from-bracket"></i> Sair
</button>
</div>
</aside>
<!-- Main Content -->
<main class="main-content">
<header class="top-header">
<div class="header-title">
<h1>Dispositivos e Clínicas</h1>
<p>Gerencie o acesso do Aplicativo Desktop aos computadores das clínicas.</p>
</div>
<div class="header-actions">
<button id="addClinicBtn" class="primary-btn pulse-animation">
<i class="fa-solid fa-plus"></i> Novo Acesso
</button>
</div>
</header>
<div class="dashboard-stats">
<div class="stat-card glass-card">
<div class="stat-icon bg-blue">
<i class="fa-solid fa-desktop"></i>
</div>
<div class="stat-info">
<h3>Total de Dispositivos</h3>
<p id="totalDevices">0</p>
</div>
</div>
<div class="stat-card glass-card">
<div class="stat-icon bg-green">
<i class="fa-solid fa-check-circle"></i>
</div>
<div class="stat-info">
<h3>Ativos</h3>
<p id="activeDevices">0</p>
</div>
</div>
<div class="stat-card glass-card">
<div class="stat-icon bg-red">
<i class="fa-solid fa-ban"></i>
</div>
<div class="stat-info">
<h3>Bloqueados</h3>
<p id="blockedDevices">0</p>
</div>
</div>
</div>
<section class="data-section glass-card">
<div class="table-container">
<table class="modern-table">
<thead>
<tr>
<th>Status</th>
<th>Clínica</th>
<th>Computador</th>
<th>Email de Acesso</th>
<th>Último IP</th>
<th>Token de Autenticação</th>
<th>Ações</th>
</tr>
</thead>
<tbody id="clinicsTableBody">
<!-- Loading state -->
<tr>
<td colspan="7" class="loading-cell">
<div class="spinner"></div>
<p>Carregando dispositivos...</p>
</td>
</tr>
</tbody>
</table>
</div>
</section>
</main>
<!-- Modal Cadastro -->
<div id="clinicModal" class="modal-overlay">
<div class="modal-content glass-card">
<div class="modal-header">
<h2>Adicionar Novo Dispositivo</h2>
<button class="close-modal"><i class="fa-solid fa-xmark"></i></button>
</div>
<form id="clinicForm" class="modern-form">
<div class="form-group">
<label for="clinic_name">Nome da Clínica</label>
<div class="input-with-icon">
<i class="fa-solid fa-hospital"></i>
<input type="text" id="clinic_name" name="clinic_name" placeholder="Ex: Odonto Mais Centro" required>
</div>
</div>
<div class="form-group">
<label for="pc_name">Identificação do Computador</label>
<div class="input-with-icon">
<i class="fa-solid fa-desktop"></i>
<input type="text" id="pc_name" name="pc_name" placeholder="Ex: Recepção 01" required>
</div>
</div>
<div class="form-group">
<label for="email">E-mail de Login</label>
<div class="input-with-icon">
<i class="fa-solid fa-envelope"></i>
<input type="email" id="email" name="email" placeholder="recepcao@odontomais.com" required>
</div>
<small>O email que o usuário usará no aplicativo Desktop.</small>
</div>
<div class="form-group">
<label for="password">Senha de Acesso</label>
<div class="input-with-icon">
<i class="fa-solid fa-lock"></i>
<input type="password" id="password" name="password" placeholder="Mínimo 6 caracteres" required minlength="6">
</div>
</div>
<div class="form-actions">
<button type="button" class="secondary-btn close-modal">Cancelar</button>
<button type="submit" class="primary-btn" id="submitBtn">
<i class="fa-solid fa-check"></i> Salvar e Gerar Token
</button>
</div>
</form>
</div>
</div>
<!-- Modal Token Gerado -->
<div id="tokenModal" class="modal-overlay">
<div class="modal-content glass-card success-modal">
<div class="success-icon pulse-animation">
<i class="fa-solid fa-check-circle"></i>
</div>
<h2>Dispositivo Cadastrado!</h2>
<p>Copie o token abaixo e cole-o no Aplicativo Desktop junto com o email e senha para liberar o envio de raios-x.</p>
<div class="token-container">
<code id="generatedToken">8f8a3b20-1d8c-4b3f-9e7b...</code>
<button id="copyTokenBtn" class="icon-btn" title="Copiar Token">
<i class="fa-regular fa-copy"></i>
</button>
</div>
<button class="primary-btn close-token-modal" style="width: 100%; margin-top: 20px;">
Entendido
</button>
</div>
</div>
<div id="toast" class="toast">Notificação</div>
<script src="admin-clinics.js"></script>
</body>
</html>
@@ -0,0 +1,251 @@
document.addEventListener('DOMContentLoaded', () => {
// Configurações
const API_URL = '/api/admin/clinics';
// Pegar Token JWT do Admin
const token = localStorage.getItem('token') || '';
// Se não tem token, redirecionar imediatamente para login
if (!token) {
window.location.href = '/login';
return;
}
const fetchOptions = {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
};
// Elementos UI
const clinicsTableBody = document.getElementById('clinicsTableBody');
const addClinicBtn = document.getElementById('addClinicBtn');
const clinicModal = document.getElementById('clinicModal');
const tokenModal = document.getElementById('tokenModal');
const closeModals = document.querySelectorAll('.close-modal');
const closeTokenModal = document.querySelector('.close-token-modal');
const clinicForm = document.getElementById('clinicForm');
const generatedTokenElem = document.getElementById('generatedToken');
const copyTokenBtn = document.getElementById('copyTokenBtn');
// Stats
const totalDevicesElem = document.getElementById('totalDevices');
const activeDevicesElem = document.getElementById('activeDevices');
const blockedDevicesElem = document.getElementById('blockedDevices');
// Inicializar
loadClinics();
// ==========================================
// Carregar Clínicas
// ==========================================
async function loadClinics() {
try {
const response = await fetch(API_URL, fetchOptions);
// Se recebeu 401 ou 403, token expirou — vai para login
if (response.status === 401 || response.status === 403) {
localStorage.removeItem('token');
window.location.href = '/login';
return;
}
// Verificar se a resposta é realmente JSON (evita erro com HTML)
const contentType = response.headers.get('content-type') || '';
if (!contentType.includes('application/json')) {
throw new Error('Sessão expirada ou servidor indisponível. Faça login novamente.');
}
if (!response.ok) {
throw new Error(`Erro HTTP ${response.status}`);
}
const data = await response.json();
if (data.success) {
renderClinics(data.clinics);
updateStats(data.clinics);
} else {
showToast(data.message, 'error');
}
} catch (error) {
console.error(error);
clinicsTableBody.innerHTML = `<tr><td colspan="7" style="text-align:center;color:red;">${error.message || 'Erro ao carregar dados.'}</td></tr>`;
}
}
// ==========================================
// Renderizar Tabela
// ==========================================
function renderClinics(clinics) {
if (clinics.length === 0) {
clinicsTableBody.innerHTML = `<tr><td colspan="7" style="text-align:center;padding:30px;color:#a0aec0;">Nenhum dispositivo cadastrado.</td></tr>`;
return;
}
clinicsTableBody.innerHTML = '';
clinics.forEach(c => {
const isAct = c.is_active;
const statusClass = isAct ? 'status-active' : 'status-inactive';
const statusIcon = isAct ? 'fa-check' : 'fa-ban';
const statusText = isAct ? 'Ativo' : 'Bloqueado';
const tr = document.createElement('tr');
tr.innerHTML = `
<td><span class="status-badge ${statusClass}"><i class="fa-solid ${statusIcon}"></i> ${statusText}</span></td>
<td><strong>${c.clinic_name}</strong></td>
<td><i class="fa-solid fa-desktop" style="color:#a0aec0;margin-right:6px"></i>${c.pc_name || '-'}</td>
<td>${c.email}</td>
<td>${c.last_ip || 'Nunca conectou'}</td>
<td><span class="token-blur" title="Clique para copiar" onclick="navigator.clipboard.writeText('${c.machine_token}');showToast('Token copiado!')">${c.machine_token.split('-')[0]}...</span></td>
<td>
<div style="display:flex;gap:12px;align-items:center;">
<label class="toggle-switch" title="${isAct ? 'Bloquear Acesso' : 'Ativar Acesso'}">
<input type="checkbox" ${isAct ? 'checked' : ''} onchange="toggleStatus(${c.id}, this.checked)">
<span class="slider"></span>
</label>
<button class="icon-btn delete" onclick="deleteClinic(${c.id})" title="Excluir">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</td>
`;
clinicsTableBody.appendChild(tr);
});
}
function updateStats(clinics) {
const total = clinics.length;
const active = clinics.filter(c => c.is_active).length;
const blocked = total - active;
totalDevicesElem.textContent = total;
activeDevicesElem.textContent = active;
blockedDevicesElem.textContent = blocked;
}
// ==========================================
// Cadastrar Clínica
// ==========================================
clinicForm.addEventListener('submit', async (e) => {
e.preventDefault();
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Salvando...';
const formData = new FormData(clinicForm);
const bodyData = Object.fromEntries(formData.entries());
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: fetchOptions.headers,
body: JSON.stringify(bodyData)
});
const data = await response.json();
if (data.success) {
clinicModal.classList.remove('active');
clinicForm.reset();
loadClinics();
// Show Token Modal
generatedTokenElem.textContent = data.machine_token;
tokenModal.classList.add('active');
} else {
showToast(data.message, 'error');
}
} catch (error) {
showToast('Erro de conexão ao salvar', 'error');
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fa-solid fa-check"></i> Salvar e Gerar Token';
}
});
// ==========================================
// Toggle Status
// ==========================================
window.toggleStatus = async (id, isActive) => {
try {
const response = await fetch(`${API_URL}/${id}/status`, {
method: 'PUT',
headers: fetchOptions.headers,
body: JSON.stringify({ is_active: isActive })
});
const data = await response.json();
if(data.success) {
showToast(isActive ? 'Dispositivo Ativado' : 'Dispositivo Bloqueado', 'success');
loadClinics();
} else {
showToast(data.message, 'error');
loadClinics(); // revert UI
}
} catch (error) {
showToast('Erro ao alterar status', 'error');
loadClinics();
}
};
// ==========================================
// Deletar
// ==========================================
window.deleteClinic = async (id) => {
if(!confirm('Tem certeza que deseja excluir este dispositivo permanentemente? O Desktop perderá acesso instantaneamente.')) return;
try {
const response = await fetch(`${API_URL}/${id}`, {
method: 'DELETE',
headers: fetchOptions.headers
});
const data = await response.json();
if(data.success) {
showToast('Dispositivo removido', 'success');
loadClinics();
} else {
showToast(data.message, 'error');
}
} catch (error) {
showToast('Erro ao excluir', 'error');
}
};
// ==========================================
// UI Actions (Modals, Copy, Logout)
// ==========================================
addClinicBtn.addEventListener('click', () => clinicModal.classList.add('active'));
closeModals.forEach(btn => btn.addEventListener('click', () => {
clinicModal.classList.remove('active');
}));
closeTokenModal.addEventListener('click', () => {
tokenModal.classList.remove('active');
});
copyTokenBtn.addEventListener('click', () => {
navigator.clipboard.writeText(generatedTokenElem.textContent).then(() => {
showToast('Token copiado com sucesso!', 'success');
copyTokenBtn.innerHTML = '<i class="fa-solid fa-check" style="color:var(--success)"></i>';
setTimeout(() => {
copyTokenBtn.innerHTML = '<i class="fa-regular fa-copy"></i>';
}, 2000);
});
});
document.getElementById('logoutBtn').addEventListener('click', () => {
localStorage.removeItem('token');
window.location.href = '/login';
});
function showToast(message, type = 'success') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = `toast show ${type}`;
setTimeout(() => {
toast.className = 'toast';
}, 3000);
}
});
File diff suppressed because it is too large Load Diff
+484
View File
@@ -0,0 +1,484 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Clientes Conectados - Dental Server</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
color: #333;
margin-bottom: 0;
}
.header-actions {
display: flex;
gap: 10px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
}
.btn-success {
background: #48bb78;
color: white;
}
.btn-success:hover {
background: #38a169;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-link {
background: transparent;
color: #667eea;
text-decoration: none;
border: 1px solid #667eea;
}
.btn-link:hover {
background: #667eea;
color: white;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.stat-card .label {
color: #666;
font-size: 14px;
margin-bottom: 5px;
}
.stat-card .value {
font-size: 32px;
font-weight: bold;
color: #667eea;
}
.clients-list {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.client-item {
padding: 15px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.client-item:last-child {
border-bottom: none;
}
.client-info {
flex: 1;
}
.client-name {
font-weight: 600;
color: #333;
margin-bottom: 5px;
}
.client-details {
font-size: 12px;
color: #666;
line-height: 1.6;
}
.client-status {
padding: 5px 15px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
margin-left: 10px;
}
.status-connected {
background: #d4edda;
color: #155724;
}
.status-identified {
background: #cce5ff;
color: #004085;
}
.status-unknown {
background: #fff3cd;
color: #856404;
}
.status-testing {
background: #e2e3e5;
color: #383d41;
}
.empty-state {
text-align: center;
padding: 40px;
color: #666;
}
.timestamp {
font-size: 12px;
color: #999;
margin-top: 10px;
}
.test-result {
margin-top: 10px;
padding: 10px;
border-radius: 5px;
font-size: 12px;
}
.test-result.success {
background: #d4edda;
color: #155724;
}
.test-result.error {
background: #f8d7da;
color: #721c24;
}
.test-result.info {
background: #d1ecf1;
color: #0c5460;
}
.loading {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid #f3f3f3;
border-top: 2px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 5px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<script>
// Redirecionar se não estiver autenticado
if (!localStorage.getItem('auth_token')) {
window.location.href = '/login';
}
</script>
</head>
<body>
<div class="container">
<div class="header">
<div>
<h1>🖥️ Clientes Conectados</h1>
<p style="color: #666; margin-top: 5px; font-size: 14px;">Monitoramento em tempo real dos clientes Socket.IO</p>
</div>
<div class="header-actions">
<a href="/" class="btn btn-link">← Voltar</a>
<button class="btn btn-primary" onclick="loadClients()">🔄 Atualizar</button>
<button class="btn btn-success" onclick="testConnection()" id="testBtn">🧪 Testar Conexão</button>
</div>
</div>
<div class="stats" id="stats">
<!-- Stats serão carregados aqui -->
</div>
<div class="clients-list">
<div id="clientsList">
<div class="empty-state">Carregando...</div>
</div>
<div class="timestamp" id="timestamp"></div>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script>
let socket = null;
let testInProgress = false;
let testResults = {};
// Conectar ao Socket.IO
function initSocket() {
const token = localStorage.getItem('auth_token');
if (!token) {
window.location.href = '/login';
return;
}
socket = io({ auth: { token } });
socket.on('connect', () => {
console.log('✅ Conectado ao servidor');
});
socket.on('disconnect', () => {
console.log('❌ Desconectado do servidor');
});
// Listener para atualização de lista de clientes
socket.on('clients-list-updated', (clients) => {
console.log('📋 Lista de clientes atualizada:', clients);
loadClients();
});
// Listener para resposta de teste de conexão
socket.on('test-connection-ack', (data) => {
console.log('✅ Teste de conexão respondido:', data);
if (data.testId) {
testResults[data.socketId] = {
success: true,
latency: data.latency || 'N/A',
timestamp: data.timestamp
};
updateTestResults();
}
});
}
async function loadClients() {
try {
const response = await fetch('/api/socket/status');
const data = await response.json();
// Atualizar stats
document.getElementById('stats').innerHTML = `
<div class="stat-card">
<div class="label">Total de Conexões</div>
<div class="value">${data.totalConnections}</div>
</div>
<div class="stat-card">
<div class="label">Clientes Identificados</div>
<div class="value">${data.identifiedClients}</div>
</div>
<div class="stat-card">
<div class="label">Não Identificados</div>
<div class="value">${data.totalConnections - data.identifiedClients}</div>
</div>
`;
// Atualizar lista
if (data.clients.length === 0) {
document.getElementById('clientsList').innerHTML = `
<div class="empty-state">
<h2>📭 Nenhum cliente conectado</h2>
<p>Aguardando conexões...</p>
</div>
`;
} else {
let html = '';
// Mostrar identificados primeiro
data.identified.forEach(client => {
const testResult = testResults[client.socketId];
html += `
<div class="client-item">
<div class="client-info">
<div class="client-name">${client.name || 'Sem nome'}</div>
<div class="client-details">
Tipo: ${client.type} |
Sistema: ${client.system || 'N/A'} |
Versão: ${client.version || 'N/A'} |
ID: ${client.socketId}
</div>
<div class="client-details" style="margin-top: 5px;">
Conectado em: ${new Date(client.connectedAt).toLocaleString('pt-BR')}
</div>
${testResult ? `
<div class="test-result ${testResult.success ? 'success' : 'error'}">
${testResult.success ?
`✅ Teste OK - Latência: ${testResult.latency}ms` :
`${testResult.error || 'Falha no teste'}`
}
</div>
` : ''}
</div>
<div class="client-status ${testInProgress ? 'status-testing' : 'status-identified'}">
${testInProgress ? '🔄 Testando...' : '✅ Identificado'}
</div>
</div>
`;
});
// Mostrar não identificados (como abas do navegador visualizando o painel)
data.clients.filter(c => !c.isIdentified).forEach(client => {
const isLocal = client.remoteAddress === '::1' || client.remoteAddress === '127.0.0.1' || client.remoteAddress === '::ffff:127.0.0.1';
const displayName = isLocal ? 'Painel Web (Navegador Local)' : 'Conexão Web (Painel de Monitoramento)';
const displayStatus = '🌐 Apenas Visualização';
html += `
<div class="client-item">
<div class="client-info">
<div class="client-name">${displayName}</div>
<div class="client-details">
Transport: ${client.transport} |
ID: ${client.id}
</div>
<div class="client-details" style="margin-top: 5px;">
IP: ${client.remoteAddress || 'N/A'}
</div>
</div>
<div class="client-status" style="background: #e2e8f0; color: #4a5568;">
${displayStatus}
</div>
</div>
`;
});
document.getElementById('clientsList').innerHTML = html;
}
// Atualizar timestamp
document.getElementById('timestamp').textContent =
`Última atualização: ${new Date(data.timestamp).toLocaleString('pt-BR')}`;
} catch (error) {
console.error('Erro ao carregar clientes:', error);
document.getElementById('clientsList').innerHTML = `
<div class="empty-state">
<h2>❌ Erro ao carregar</h2>
<p>${error.message}</p>
</div>
`;
}
}
async function testConnection() {
if (testInProgress) {
return;
}
testInProgress = true;
testResults = {};
const testBtn = document.getElementById('testBtn');
testBtn.disabled = true;
testBtn.innerHTML = '<span class="loading"></span> Testando...';
try {
// Limpar resultados anteriores
testResults = {};
loadClients(); // Atualizar UI para mostrar estado de teste
// Chamar endpoint de teste
const response = await fetch('/api/socket/test-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const result = await response.json();
// Processar resultados
if (result.testResults && result.testResults.length > 0) {
result.testResults.forEach(test => {
testResults[test.socketId] = test;
});
}
// Atualizar interface
updateTestResults();
loadClients();
// Mostrar resumo
const successCount = result.testResults ? result.testResults.filter(r => r.success).length : 0;
const totalCount = result.testResults ? result.testResults.length : 0;
if (totalCount > 0) {
alert(`✅ Teste concluído!\n${successCount} de ${totalCount} clientes responderam com sucesso.`);
} else {
alert('⚠️ Nenhum cliente identificado para testar!');
}
} catch (error) {
console.error('Erro no teste de conexão:', error);
alert('❌ Erro ao executar teste: ' + error.message);
} finally {
testInProgress = false;
testBtn.disabled = false;
testBtn.innerHTML = '🧪 Testar Conexão';
}
}
function updateTestResults() {
// Atualizar interface com resultados
loadClients();
}
// Inicializar Socket.IO e carregar clientes
initSocket();
loadClients();
// Atualizar a cada 3 segundos
setInterval(loadClients, 3000);
</script>
</body>
</html>
+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#667eea"/>
<stop offset="100%" style="stop-color:#764ba2"/>
</linearGradient>
</defs>
<rect width="64" height="64" rx="14" fill="url(#bg)"/>
<!-- Tooth shape -->
<path d="M32 9 C24 9 18 13 16 19 C14 25 15.5 30 17 34 C18.5 38 19 41 18.5 46 C18 51 19 57 24 57 C27 57 28.5 53 28.5 50 C28.5 47 30 45 32 45 C34 45 35.5 47 35.5 50 C35.5 53 37 57 40 57 C45 57 46 51 45.5 46 C45 41 45.5 38 47 34 C48.5 30 50 25 48 19 C46 13 40 9 32 9Z"
fill="white" opacity="0.95"/>
<!-- Root hints -->
<path d="M27 46 C27 50 25 55 24 57" stroke="#764ba2" stroke-width="1.5" fill="none" opacity="0.3"/>
<path d="M37 46 C37 50 39 55 40 57" stroke="#764ba2" stroke-width="1.5" fill="none" opacity="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 894 B

+480
View File
@@ -0,0 +1,480 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Gerenciador de Imagens Dentais</title>
<meta name="description" content="Sistema de gerenciamento de imagens de raio-X dental">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/style.css">
</head>
<body>
<div class="app-container">
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-header">
<h2>🦷 DentalSys</h2>
</div>
<nav class="sidebar-nav">
<a href="/" class="nav-item active">
<span class="icon">🖼️</span> Pacientes
</a>
<a href="#" class="nav-item" onclick="openCreatePatientModal(); return false;">
<span class="icon"></span> Novo Paciente
</a>
<a href="/clients" class="nav-item">
<span class="icon">🖥️</span> Dispositivos
</a>
<a href="#" class="nav-item" onclick="openSettingsModal(); return false;">
<span class="icon">⚙️</span> Configurações
</a>
<a href="#" class="nav-item" onclick="openPluginsModal(); return false;">
<span class="icon">🔌</span> Plugins / Wasabi
</a>
<a href="#" class="nav-item" onclick="openSyncModal(); return false;">
<span class="icon">🔄</span> Sincronização
</a>
<a href="/reset" class="nav-item" style="color: #dc3545;">
<span class="icon">⚠️</span> Zerar Sistema
</a>
<a href="/admin-clinics.html" class="nav-item">
<span class="icon">🏢</span> Gerenciar Clínicas
</a>
<a href="#" class="nav-item" onclick="logout(); return false;">
<span class="icon">🚪</span> Sair
</a>
<div class="nav-divider"></div>
<div style="padding: 10px 20px;">
<select id="clientFilter" class="client-filter" style="width: 100%; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.1); color: white; border-radius: 6px; padding: 8px;">
<option value="" style="color: black;">📋 Todos os Clientes</option>
</select>
</div>
<button id="toggleDisabledBtn" class="btn btn-secondary" style="margin: 10px 20px; width: calc(100% - 40px);">
<span id="toggleText">Ocultas</span>
</button>
</nav>
<div style="padding: 16px 20px; border-top: 1px solid rgba(255,255,255,0.08); margin-top: auto;">
<div style="display: flex; align-items: center; gap: 8px; opacity: 0.5;">
<span style="font-size: 11px; color: rgba(255,255,255,0.7);">🦷 DentalSys</span>
<span style="font-size: 10px; background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.6); padding: 2px 7px; border-radius: 20px; font-family: monospace; letter-spacing: 0.5px;">Server v2.0.0</span>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="main-content">
<!-- Header -->
<header class="header">
<div class="header-content">
<div class="header-left">
<button id="backBtn" class="btn btn-back" style="display:none;">
← Voltar
</button>
<h1 id="mainTitle">Galeria de Imagens</h1>
</div>
<div class="header-actions">
<input
type="search"
id="searchInput"
class="search-input"
placeholder="🔍 Pesquisar paciente..."
>
<button class="btn btn-secondary" onclick="loadClientsList()">
<span class="icon">🔄</span>
</button>
</div>
</div>
<!-- Patient sub-header (shown in patient-images view) -->
<div id="patientHeader" class="patient-header" style="display:none;">
<div class="patient-header-info">
<div id="patientTitle" class="patient-header-name"></div>
<div id="patientMeta" class="patient-header-meta"></div>
</div>
</div>
</header>
<!-- Scrollable content area -->
<div class="content-scroll">
<!-- Loading State -->
<div id="loadingState" class="loading">
<div class="spinner"></div>
<p>Carregando...</p>
</div>
<!-- Empty State -->
<div id="emptyState" class="empty-state" style="display: none;">
<div class="empty-icon">📭</div>
<h2>Nenhum resultado encontrado</h2>
<p>As imagens aparecerão aqui quando forem enviadas pelo cliente Windows</p>
</div>
<!-- Images / Patients Grid -->
<div id="imagesGrid" class="images-grid"> </div>
</div><!-- end .content-scroll -->
</main>
</div>
<!-- Transform Modal -->
<div id="transformModal" class="modal">
<div class="modal-content modal-large">
<div class="modal-header" style="display: flex; justify-content: space-between; width: 100%; align-items: center; border-bottom: 1px solid #eee; padding-bottom: 15px;">
<div style="display: flex; align-items: center; gap: 15px; flex-wrap: wrap;">
<h2 id="transformModalTitle" style="margin: 0; font-size: 1.4rem;"><!-- Dynamic Title (Patient + Date) --></h2>
<div style="display: flex; gap: 8px;">
<button id="btnShowTransform" class="btn btn-primary" onclick="toggleModalView('transform')" style="width: 60px; height: 40px; padding: 0; display: flex; align-items: center; justify-content: center; font-size: 1.2rem;" title="Ajustar Imagem">🔄</button>
<button id="btnShowGto" class="btn btn-secondary" onclick="toggleModalView('gto')" style="width: 60px; height: 40px; padding: 0; display: flex; align-items: center; justify-content: center; font-size: 1.2rem;" title="Gerenciar GTOs">📋</button>
<button id="btnShowInfo" class="btn btn-secondary" onclick="toggleModalInfo()" style="width: 60px; height: 40px; padding: 0; display: flex; align-items: center; justify-content: center; font-size: 1.2rem;" title="Informações"></button>
<button id="btnResetImage" class="btn btn-danger" onclick="resetImageToOriginal()" style="width: 60px; height: 40px; padding: 0; display: flex; align-items: center; justify-content: center; font-size: 1.2rem; background-color: #ff6b6b; border-color: #ff6b6b; color: white;" title="Resetar para Original">↩️</button>
</div>
</div>
<span class="modal-close" id="closeTransformModal">×</span>
</div>
<div class="modal-body" style="padding-top: 15px;">
<!-- Bloco oculto de informações -->
<div id="transformExtraInfo" style="display: none; font-size: 0.95rem; color: #555; background: #f8f9fa; padding: 12px 20px; border-radius: 8px; border: 1px solid #eee; margin-bottom: 15px;">
<!-- Preenchido via JS -->
</div>
<!-- View 1: Transforms -->
<div id="transformViewWrapper" style="display: flex; flex-direction: column; flex: 1; min-height: 0; height: 100%;">
<div id="transformOptions" class="transform-grid" style="flex: 1; min-height: 0;"></div>
<div id="transformActionArea" style="display: none; margin-top: 20px; background: #f8f9fa; padding: 15px; border-radius: 8px; flex-shrink: 0; border: 1px solid #e3e8ee;">
<!-- Ajuste Fino de Rotação -->
<div style="display: flex; flex-direction: column; align-items: center; margin-bottom: 15px; border-bottom: 1px solid #e3e8ee; padding-bottom: 15px;">
<span style="font-weight: 700; font-size: 0.95rem; margin-bottom: 10px; color: var(--dark-color); display: flex; align-items: center; gap: 6px;">
📐 Ajuste Fino de Ângulo (Sensor Inclinado)
</span>
<div style="display: flex; align-items: center; gap: 8px; justify-content: center; flex-wrap: wrap;">
<button type="button" class="btn btn-secondary" style="padding: 6px 12px; font-size: 0.85rem; display: flex; align-items: center; gap: 3px;" onclick="adjustFineTune(-5)" title="Rotacionar -5°">🔄 -5°</button>
<button type="button" class="btn btn-secondary" style="padding: 6px 12px; font-size: 0.85rem; display: flex; align-items: center; gap: 3px;" onclick="adjustFineTune(-1)" title="Rotacionar -1°">🔄 -1°</button>
<div style="min-width: 100px; text-align: center; padding: 6px 12px; background: white; border: 1px solid #cbd5e0; border-radius: 6px; font-family: monospace; font-weight: 700; font-size: 1.1rem; color: var(--primary-color);" id="fineTuneValDisplay">
</div>
<button type="button" class="btn btn-secondary" style="padding: 6px 12px; font-size: 0.85rem; display: flex; align-items: center; gap: 3px;" onclick="adjustFineTune(1)" title="Rotacionar +1°">🔄 +1°</button>
<button type="button" class="btn btn-secondary" style="padding: 6px 12px; font-size: 0.85rem; display: flex; align-items: center; gap: 3px;" onclick="adjustFineTune(5)" title="Rotacionar +5°">🔄 +5°</button>
<button type="button" class="btn btn-danger btn-small" style="padding: 6px 12px; font-size: 0.85rem; margin-left: 8px; display: none; background: #e53e3e; border-color: #e53e3e; color: white;" id="btnResetFineTune" onclick="resetFineTune()" title="Resetar ajuste fino">Reset</button>
</div>
</div>
<label style="font-weight: 600; display: block; margin-bottom: 8px;">Nova Observação / Detalhes (Opcional):</label>
<textarea id="newImageRemark" class="search-input" rows="2" style="width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 6px;" placeholder="Ex: Raio-X Dente 45..."></textarea>
<div style="display: flex; gap: 10px; margin-top: 10px;">
<button class="btn btn-secondary" style="flex: 1; padding: 12px; font-size: 1.1rem;" onclick="clearTransformSelection()">Cancelar</button>
<button id="btnSaveTransform" class="btn btn-primary" style="flex: 2; padding: 12px; font-size: 1.1rem;" onclick="saveSelectedTransform()">Salvar Nova Imagem</button>
</div>
</div>
</div>
<!-- View 2: GTO List & Link -->
<div id="gtoView" style="display: none;">
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
<h3 style="margin-top: 0; font-size: 1.1rem; margin-bottom: 15px;">Cadastrar Nova GTO</h3>
<div style="display: flex; gap: 10px;">
<input type="text" id="inlineGtoNumber" class="search-input" placeholder="Número/ID da GTO" style="flex: 1; padding: 10px;">
<input type="text" id="inlineGtoDesc" class="search-input" placeholder="Descrição (Opcional)" style="flex: 2; padding: 10px;">
<button class="btn btn-primary" onclick="inlineCreateGto()">Cadastrar</button>
</div>
</div>
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 15px;">
<h3 style="margin: 0; font-size: 1.1rem;">Vincular esta imagem a uma GTO:</h3>
<button class="btn btn-success" onclick="linkImageToGto()" id="btnLinkGto">✅ Vincular Selecionada</button>
</div>
<div id="gtoListContainer" style="border: 1px solid #ddd; border-radius: 8px; overflow: hidden;">
<!-- GTO list goes here -->
</div>
</div>
</div>
</div>
</div>
<!-- Modal Novo Paciente -->
<div class="modal" id="create-patient-modal">
<div class="modal-content" style="max-width: 500px; height: auto; border-radius: var(--radius);">
<div class="modal-header">
<h2>Novo Paciente</h2>
<div class="modal-close" onclick="closeCreatePatientModal()">&times;</div>
</div>
<div class="modal-body" style="overflow-y: auto;">
<form id="create-patient-form" onsubmit="submitCreatePatient(event)">
<div class="form-group">
<label>Nome *</label>
<input type="text" id="patient-first-name" required placeholder="Ex: João">
</div>
<div class="form-group">
<label>Sobrenome *</label>
<input type="text" id="patient-last-name" required placeholder="Ex: Silva">
</div>
<div class="form-group">
<label>Dentista</label>
<input type="text" id="patient-doctor" placeholder="Nome do dentista responsável">
</div>
<div style="display: flex; gap: 12px;">
<div class="form-group" style="flex: 1;">
<label>Data de Nascimento</label>
<input type="date" id="patient-birthday">
</div>
<div class="form-group" style="flex: 1;">
<label>Gênero</label>
<select id="patient-gender">
<option value="0">Masculino</option>
<option value="1">Feminino</option>
</select>
</div>
</div>
<div class="form-group">
<label>Telefone</label>
<input type="text" id="patient-phone" placeholder="(11) 99999-9999">
</div>
<div class="form-group">
<label>Observações</label>
<textarea id="patient-remark" rows="3" placeholder="Informações adicionais..."></textarea>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 10px;">
Salvar Paciente
</button>
</form>
</div>
</div>
</div>
<!-- Modal Nova GTO -->
<div class="modal" id="create-gto-modal">
<div class="modal-content" style="max-width: 400px; height: auto; border-radius: var(--radius);">
<div class="modal-header">
<h2>Nova GTO</h2>
<div class="modal-close" onclick="closeCreateGtoModal()">&times;</div>
</div>
<div class="modal-body">
<form id="create-gto-form" onsubmit="submitCreateGto(event)">
<div class="form-group">
<label>Número/ID da GTO *</label>
<input type="text" id="gto-number" required placeholder="Ex: GTO-2023-001">
</div>
<div class="form-group">
<label>Descrição (opcional)</label>
<textarea id="gto-description" rows="3" placeholder="Informações da guia..."></textarea>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 10px;">
Criar GTO
</button>
</form>
</div>
</div>
</div>
<!-- Toast de Notificação -->
<div class="toast" id="toast">Operação realizada com sucesso!</div>
<!-- Settings Modal -->
<div id="settingsModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>⚙️ Configurações da Conta</h2>
<span class="close-modal" onclick="closeSettingsModal()">&times;</span>
</div>
<div class="modal-body">
<form id="settingsForm" onsubmit="updateCredentials(event)">
<div class="form-group">
<label for="currentPassword">Senha Atual (Obrigatório)*</label>
<input type="password" id="currentPassword" class="form-control" placeholder="Digite sua senha atual para autorizar" required>
</div>
<hr style="margin: 20px 0; border: none; border-top: 1px solid #eee;">
<p style="margin-bottom: 15px; color: #666; font-size: 0.9rem;">Preencha apenas os campos que deseja alterar:</p>
<div class="form-group">
<label for="newUsername">Novo Nome de Usuário</label>
<input type="text" id="newUsername" class="form-control" placeholder="Deixe em branco para não alterar">
</div>
<div class="form-group">
<label for="newPassword">Nova Senha</label>
<input type="password" id="newPassword" class="form-control" placeholder="Deixe em branco para não alterar">
</div>
<div class="form-actions" style="margin-top: 25px; display: flex; justify-content: flex-end; gap: 10px;">
<button type="button" class="btn btn-secondary" onclick="closeSettingsModal()">Cancelar</button>
<button type="submit" class="btn btn-primary" id="btnSaveSettings">Salvar Alterações</button>
</div>
</form>
</div>
</div>
</div>
<!-- Plugins / Wasabi Modal -->
<div id="pluginsModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>🔌 Configuração do Wasabi Storage</h2>
<span class="modal-close" onclick="closePluginsModal()">&times;</span>
</div>
<div class="modal-body">
<form id="pluginsForm" onsubmit="updatePluginsConfig(event)">
<div style="background: #f8f9fa; padding: 12px 18px; border-radius: 8px; border: 1px solid #eee; margin-bottom: 20px; font-size: 0.95rem; color: #555;">
Configure as credenciais e bucket do **Wasabi S3** para habilitar o armazenamento na nuvem. Se desativado, o sistema usará o armazenamento local do servidor de forma automática.
</div>
<div class="form-group">
<label for="wasabiAccessKey">Access Key *</label>
<input type="text" id="wasabiAccessKey" placeholder="Ex: AIPL5M4G..." required>
</div>
<div class="form-group">
<label for="wasabiSecretKey">Secret Key *</label>
<input type="password" id="wasabiSecretKey" placeholder="Sua Secret Key" required>
</div>
<div class="form-group">
<label for="wasabiBucket">Bucket Ativo *</label>
<input type="text" id="wasabiBucket" placeholder="Ex: meu-bucket-rx" required>
</div>
<div style="display: flex; gap: 12px;">
<div class="form-group" style="flex: 1;">
<label for="wasabiRegion">Região</label>
<input type="text" id="wasabiRegion" placeholder="Ex: us-east-1 (padrão)">
</div>
<div class="form-group" style="flex: 1;">
<label for="wasabiEndpoint">Endpoint</label>
<input type="text" id="wasabiEndpoint" placeholder="Ex: s3.wasabisys.com">
</div>
</div>
<div id="pluginsStatusAlert" style="display: none; padding: 10px; border-radius: 6px; font-size: 0.9rem; margin-top: 15px;"></div>
<div class="form-actions" style="margin-top: 25px; display: flex; justify-content: flex-end; gap: 10px; border-top: 1px solid #eee; padding-top: 15px;">
<button type="button" class="btn btn-secondary" onclick="closePluginsModal()">Cancelar</button>
<button type="submit" class="btn btn-primary" id="btnSavePlugins">Salvar Configurações</button>
</div>
</form>
</div>
</div>
</div>
<!-- Sync / Upload Modal -->
<div id="syncModal" class="modal">
<div class="modal-content" style="max-width: 650px; height: auto; border-radius: var(--radius);">
<div class="modal-header">
<h2>🔄 Área de Sincronização</h2>
<span class="modal-close" onclick="closeSyncModal()">&times;</span>
</div>
<div class="modal-body">
<div class="sync-tabs">
<button type="button" id="tabBtnDevices" class="sync-tab-btn active" onclick="switchSyncTab('devices')">💻 Dispositivos Conectados</button>
<button type="button" id="tabBtnUpload" class="sync-tab-btn" onclick="switchSyncTab('upload')">📤 Envio Manual</button>
</div>
<!-- Tab 1: Dispositivos -->
<div id="tab-devices" class="sync-tab-content active">
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; border: 1px solid #eee; margin-bottom: 20px; font-size: 0.95rem; color: #555; line-height: 1.5;">
Os computadores clientes com Windows instalados e ativos enviam imagens em tempo real. Você pode solicitar que eles façam uma sincronização manual forçada com o servidor enviando um sinal.
</div>
<div id="syncDevicesLoading" class="loading" style="padding: 20px 0;">
<div class="spinner" style="width: 30px; height: 30px; margin-bottom: 10px;"></div>
<p>Buscando dispositivos online...</p>
</div>
<div id="syncDevicesEmpty" style="display: none; text-align: center; padding: 30px 20px; color: #666;">
<span style="font-size: 2.5rem; display: block; margin-bottom: 10px;">📭</span>
<p>Nenhum dispositivo Windows conectado no momento.</p>
</div>
<div id="syncDeviceList" class="sync-device-list" style="display: none;">
<!-- Preenchido via Javascript -->
</div>
<div style="margin-top: 25px; border-top: 1px solid #eee; padding-top: 15px; display: flex; justify-content: flex-end; gap: 10px;">
<button type="button" class="btn btn-secondary" onclick="closeSyncModal()">Fechar</button>
<button type="button" class="btn btn-primary" onclick="triggerSyncFromModal()" id="btnTriggerSyncModal">
<span class="icon">🔄</span> Enviar Sinal de Sincronização
</button>
</div>
</div>
<!-- Tab 2: Envio Manual -->
<div id="tab-upload" class="sync-tab-content">
<form id="manualUploadForm" onsubmit="handleManualUploadSubmit(event)">
<div class="form-group">
<label for="uploadPatientSelect">Paciente *</label>
<div style="display: flex; gap: 10px;">
<select id="uploadPatientSelect" onchange="toggleNewPatientFields()" required style="flex: 1;">
<option value="">-- Selecione o Paciente --</option>
<option value="NEW_PATIENT"> Novo Paciente...</option>
</select>
<button type="button" class="btn btn-secondary" onclick="refreshUploadPatients()" title="Atualizar Lista" style="width: 44px; padding: 0; display: flex; align-items: center; justify-content: center;">
🔄
</button>
</div>
</div>
<!-- Campos para Novo Paciente (ocultos por padrão) -->
<div id="newPatientFields" style="display: none; background: #f8f9fa; padding: 15px; border-radius: 8px; border: 1px solid #eee; margin-bottom: 18px; animation: slideDown 0.2s ease;">
<h4 style="margin: 0 0 12px 0; color: var(--dark-color); font-size: 0.95rem;">Dados do Novo Paciente</h4>
<div style="display: flex; gap: 12px; margin-bottom: 12px;">
<div style="flex: 1;">
<label style="font-size: 0.82rem; font-weight: 600; display: block; margin-bottom: 5px;">Nome *</label>
<input type="text" id="uploadNewPatientFirstName" placeholder="Ex: Maria" style="width: 100%; padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px;">
</div>
<div style="flex: 1;">
<label style="font-size: 0.82rem; font-weight: 600; display: block; margin-bottom: 5px;">Sobrenome *</label>
<input type="text" id="uploadNewPatientLastName" placeholder="Ex: Oliveira" style="width: 100%; padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px;">
</div>
</div>
<div>
<label style="font-size: 0.82rem; font-weight: 600; display: block; margin-bottom: 5px;">Dentista Responsável</label>
<input type="text" id="uploadNewPatientDoctor" placeholder="Nome do dentista (opcional)" style="width: 100%; padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px;">
</div>
</div>
<div class="form-group">
<label for="uploadRemark">Observações / Detalhes (Opcional)</label>
<textarea id="uploadRemark" placeholder="Ex: Raio-X Panorâmico" rows="2" style="width: 100%; padding: 10px; border: 2px solid var(--border-color); border-radius: var(--radius-sm); font-family: inherit; font-size: 1rem; resize: vertical;"></textarea>
</div>
<!-- Zona de Drag and Drop -->
<div class="drag-drop-zone" id="uploadDragDropZone">
<div class="drag-drop-icon">📸</div>
<div class="drag-drop-text">Arraste as imagens aqui ou clique para selecionar</div>
<div class="drag-drop-subtext">Suporta arquivos JPG, JPEG e PNG (máx. 10MB por imagem)</div>
<input type="file" id="uploadFileInput" multiple accept="image/png, image/jpeg, image/jpg" style="display: none;">
</div>
<!-- Previews das Imagens Selecionadas -->
<div class="upload-previews" id="uploadPreviewsContainer"></div>
<!-- Progresso de Envio -->
<div class="upload-progress-container" id="uploadProgressContainer" style="display: none;">
<div class="progress-bar-label">
<span id="uploadProgressText">Enviando imagens...</span>
<span id="uploadProgressPercent">0%</span>
</div>
<div class="progress-bar-track">
<div class="progress-bar-fill" id="uploadProgressBarFill"></div>
</div>
</div>
<div class="form-actions" style="margin-top: 25px; border-top: 1px solid #eee; padding-top: 15px; display: flex; justify-content: flex-end; gap: 10px;">
<button type="button" class="btn btn-secondary" onclick="closeSyncModal()">Cancelar</button>
<button type="submit" class="btn btn-primary" id="btnSubmitManualUpload" disabled>Enviar Imagens</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Application Script -->
<script src="/socket.io/socket.io.js"></script>
<script src="/app.js"></script>
</body>
</html>
+441
View File
@@ -0,0 +1,441 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Instalação - Dental Image Server</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.install-container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 600px;
width: 100%;
padding: 40px;
}
h1 {
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
.step {
margin-bottom: 25px;
}
.step-title {
font-weight: 600;
color: #667eea;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.step-number {
background: #667eea;
color: white;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
font-size: 14px;
}
input, select {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}
input:focus, select:focus {
outline: none;
border-color: #667eea;
}
.form-help {
font-size: 12px;
color: #999;
margin-top: 5px;
}
.btn {
width: 100%;
padding: 14px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.3s;
margin-top: 10px;
}
.btn:hover {
background: #5568d3;
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.alert {
padding: 12px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert-info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.loading {
display: none;
text-align: center;
padding: 20px;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.success-message {
text-align: center;
padding: 20px;
display: none;
}
.success-icon {
font-size: 64px;
margin-bottom: 20px;
}
.check-status {
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 20px;
}
.status-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #e0e0e0;
}
.status-item:last-child {
border-bottom: none;
}
.status-ok {
color: #28a745;
font-weight: 600;
}
.status-fail {
color: #dc3545;
font-weight: 600;
}
</style>
</head>
<body>
<div class="install-container">
<h1>🦷 Instalação do Sistema</h1>
<p class="subtitle">Configure o banco de dados e crie o usuário administrador</p>
<div id="statusCheck" class="check-status">
<div class="status-item">
<span>Verificando status da instalação...</span>
<span id="statusLoading" class="spinner" style="width: 16px; height: 16px; display: inline-block;"></span>
</div>
</div>
<div id="alertContainer"></div>
<div id="installForm">
<div class="step">
<div class="step-title">
<span class="step-number">1</span>
<span>Configuração do Banco de Dados</span>
</div>
<div class="form-group">
<label>Host MySQL</label>
<input type="text" id="dbHost" value="localhost" placeholder="localhost">
</div>
<div class="form-group">
<label>Porta MySQL</label>
<input type="number" id="dbPort" value="3306" placeholder="3306">
</div>
<div class="form-group">
<label>Nome do Banco</label>
<input type="text" id="dbName" value="dental_images" placeholder="dental_images">
<div class="form-help">O banco será criado automaticamente se não existir</div>
</div>
<div class="form-group">
<label>Usuário MySQL</label>
<input type="text" id="dbUser" value="root" placeholder="root">
</div>
<div class="form-group">
<label>Senha MySQL</label>
<input type="password" id="dbPassword" placeholder="Digite a senha">
</div>
</div>
<div class="step">
<div class="step-title">
<span class="step-number">2</span>
<span>Usuário Administrador</span>
</div>
<div class="form-group">
<label>Nome de Usuário</label>
<input type="text" id="adminUsername" value="rcesar" placeholder="rcesar">
</div>
<div class="form-group">
<label>Email</label>
<input type="email" id="adminEmail" value="rcesar@rcesar.com" placeholder="admin@example.com">
</div>
<div class="form-group">
<label>Senha</label>
<input type="password" id="adminPassword" value="Rc362514" placeholder="Digite uma senha segura">
<div class="form-help">Mínimo de 8 caracteres</div>
</div>
</div>
<button id="installBtn" class="btn" onclick="runInstallation()">
▶ Iniciar Instalação
</button>
</div>
<div id="loading" class="loading">
<div class="spinner"></div>
<p>Instalando... Por favor aguarde.</p>
</div>
<div id="successMessage" class="success-message">
<div class="success-icon"></div>
<h2>Instalação Concluída!</h2>
<p>O sistema foi configurado com sucesso.</p>
<button class="btn" onclick="window.location.href='/login'" style="margin-top: 20px;">
Ir para Login
</button>
</div>
</div>
<script>
// Verificar status ao carregar
async function checkStatus() {
try {
const response = await fetch('/api/install/status');
const data = await response.json();
const statusHtml = `
<div class="status-item">
<span>Arquivo de instalação (.installed)</span>
<span class="${data.installed ? 'status-ok' : 'status-fail'}">
${data.installed ? '✓ OK' : '✗ Não instalado'}
</span>
</div>
<div class="status-item">
<span>Arquivo de configuração (.env)</span>
<span class="${data.hasEnvFile ? 'status-ok' : 'status-fail'}">
${data.hasEnvFile ? '✓ OK' : '✗ Não configurado'}
</span>
</div>
<div class="status-item">
<span>Banco de dados</span>
<span class="${data.databaseExists ? 'status-ok' : 'status-fail'}">
${data.databaseExists ? '✓ Conectado' : '✗ Não configurado'}
</span>
</div>
<div class="status-item">
<span>Usuário admin</span>
<span class="${data.userExists ? 'status-ok' : 'status-fail'}">
${data.userExists ? '✓ Existe' : '✗ Não existe'}
</span>
</div>
`;
document.getElementById('statusCheck').innerHTML = statusHtml;
if (data.installed && data.databaseExists && data.userExists) {
showAlert('Sistema já está instalado!', 'success');
document.getElementById('installForm').style.display = 'none';
document.getElementById('successMessage').style.display = 'block';
}
} catch (error) {
console.error('Erro ao verificar status:', error);
}
}
async function runInstallation() {
const btn = document.getElementById('installBtn');
const form = document.getElementById('installForm');
const loading = document.getElementById('loading');
const success = document.getElementById('successMessage');
// Coletar dados
const installData = {
db: {
host: document.getElementById('dbHost').value.trim(),
port: parseInt(document.getElementById('dbPort').value) || 3306,
name: document.getElementById('dbName').value.trim(),
user: document.getElementById('dbUser').value.trim(),
password: document.getElementById('dbPassword').value
},
admin: {
username: document.getElementById('adminUsername').value.trim(),
email: document.getElementById('adminEmail').value.trim(),
password: document.getElementById('adminPassword').value
}
};
// Validações
if (!installData.db.host || !installData.db.name || !installData.db.user) {
showAlert('Preencha todos os campos do banco de dados!', 'error');
return;
}
if (!installData.admin.username || !installData.admin.email || !installData.admin.password) {
showAlert('Preencha todos os campos do administrador!', 'error');
return;
}
if (installData.admin.password.length < 8) {
showAlert('A senha deve ter no mínimo 8 caracteres!', 'error');
return;
}
if (!installData.admin.email.includes('@')) {
showAlert('Email inválido!', 'error');
return;
}
// Desabilitar botão
btn.disabled = true;
btn.textContent = 'Instalando...';
// Mostrar loading
form.style.display = 'none';
loading.style.display = 'block';
try {
const response = await fetch('/api/install', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(installData)
});
const result = await response.json();
if (result.success) {
loading.style.display = 'none';
success.style.display = 'block';
showAlert('Instalação concluída com sucesso! Você será redirecionado...', 'success');
// Aguardar e redirecionar
setTimeout(() => {
window.location.href = '/login';
}, 2000);
} else {
loading.style.display = 'none';
form.style.display = 'block';
btn.disabled = false;
btn.textContent = '▶ Iniciar Instalação';
showAlert(result.error || 'Erro na instalação. Verifique os dados e tente novamente.', 'error');
}
} catch (error) {
loading.style.display = 'none';
form.style.display = 'block';
btn.disabled = false;
btn.textContent = '▶ Iniciar Instalação';
showAlert('Erro ao conectar com o servidor: ' + error.message, 'error');
}
}
function showAlert(message, type) {
const container = document.getElementById('alertContainer');
container.innerHTML = `<div class="alert alert-${type}">${message}</div>`;
setTimeout(() => {
container.innerHTML = '';
}, 5000);
}
// Iniciar verificação
checkStatus();
</script>
</body>
</html>
+204
View File
@@ -0,0 +1,204 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Resetar Sistema - DentalSys</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/style.css">
<style>
body {
background-color: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.reset-container {
background: white;
border-radius: 12px;
box-shadow: 0 8px 30px rgba(0,0,0,0.1);
width: 100%;
max-width: 450px;
padding: 40px;
border-top: 5px solid #dc3545;
}
.reset-header {
text-align: center;
margin-bottom: 30px;
}
.reset-header h1 {
color: #dc3545;
font-size: 1.8rem;
margin-bottom: 10px;
}
.reset-header p {
color: #666;
line-height: 1.5;
}
.warning-box {
background: #fff3cd;
color: #856404;
padding: 15px;
border-radius: 8px;
border-left: 4px solid #ffeeba;
margin-bottom: 25px;
font-size: 0.9rem;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
.form-control {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 1rem;
box-sizing: border-box;
}
.btn {
width: 100%;
padding: 12px;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
transition: opacity 0.2s;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
opacity: 0.9;
}
.btn-link {
background: none;
color: #667eea;
margin-top: 15px;
text-decoration: underline;
}
.spinner {
border: 2px solid #f3f3f3;
border-top: 2px solid white;
border-radius: 50%;
width: 16px;
height: 16px;
animation: spin 1s linear infinite;
display: inline-block;
vertical-align: middle;
margin-right: 8px;
display: none;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: #333;
color: white;
padding: 12px 24px;
border-radius: 8px;
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
z-index: 1000;
opacity: 0;
font-size: 14px;
}
.toast.show { transform: translateX(-50%) translateY(0); opacity: 1; }
.toast.error { background: #e74c3c; }
.toast.success { background: #2ecc71; }
</style>
<script>
// Proteger rota
if (!localStorage.getItem('auth_token')) {
window.location.href = '/login';
}
</script>
</head>
<body>
<div class="reset-container">
<div class="reset-header">
<h1>⚠️ Apagar Tudo</h1>
<p>Preparação para Servidor de Produção</p>
</div>
<div class="warning-box">
<strong>Atenção:</strong> Esta ação é <b>irreversível</b>. Ela irá apagar todos os pacientes, GTOs, histórico e todas as imagens (físicas e do banco). O seu usuário e senha de acesso serão mantidos.
</div>
<form id="resetForm" onsubmit="factoryReset(event)">
<div class="form-group">
<label for="password">Confirme sua senha atual:</label>
<input type="password" id="password" class="form-control" placeholder="Sua senha de administrador" required>
</div>
<button type="submit" class="btn btn-danger" id="btnReset">
<span class="spinner" id="spinner"></span> DELETAR TODOS OS DADOS
</button>
<button type="button" class="btn btn-link" onclick="window.location.href='/'">Cancelar e voltar</button>
</form>
</div>
<div id="toast" class="toast">Mensagem</div>
<script>
function showToast(msg, type = 'success') {
const t = document.getElementById('toast');
t.textContent = msg;
t.className = 'toast show ' + type;
setTimeout(() => { t.className = 'toast'; }, 4000);
}
async function factoryReset(event) {
event.preventDefault();
const password = document.getElementById('password').value;
if (!password) return;
const btn = document.getElementById('btnReset');
const spinner = document.getElementById('spinner');
btn.disabled = true;
spinner.style.display = 'inline-block';
try {
const token = localStorage.getItem('auth_token');
const res = await fetch('/api/system/factory-reset', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ password })
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Erro desconhecido');
showToast(`Sucesso! ${data.filesDeleted} arquivos foram apagados.`, 'success');
document.getElementById('password').value = '';
// Redireciona para o painel limpo após 2 segundos
setTimeout(() => {
window.location.href = '/';
}, 2000);
} catch (err) {
showToast(err.message, 'error');
btn.disabled = false;
spinner.style.display = 'none';
}
}
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff