Implement client authentication and admin clinics panel

This commit is contained in:
VPS 4 Builder
2026-05-26 04:17:12 +02:00
parent 13175f93aa
commit ec3f23c9ea
11 changed files with 2642 additions and 18 deletions
+238
View File
@@ -0,0 +1,238 @@
document.addEventListener('DOMContentLoaded', () => {
// Configurações
const API_URL = '/api/admin/clinics';
// Pegar Token JWT do Admin (salvo no localStorage ou cookies)
// Assumindo que a sessão já está lidada por cookies ou podemos buscar da rota,
// mas usaremos credenciais padrão do fetch se usar cookie de sessão.
// Se usar JWT localStorage:
const token = localStorage.getItem('token') || '';
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);
if (!response.ok) {
if (response.status === 401 || response.status === 403) {
window.location.href = '/login';
return;
}
throw new Error('Falha ao buscar clínicas');
}
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;">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);
}
});