feat(clinics): wipe cirúrgico por dispositivo — apaga banco + pasta Wasabi só da clínica; remove Zerar Sistema da sidebar
continuous-integration/webhook Deploy concluído (VPS4)

- DELETE /admin/clinics/:id/wipe: apaga images/gto_images do banco (WHERE client_name),
  lista e deleta em batch todos os objetos Wasabi com prefixo 'clinic_name/',
  e remove o registro do dispositivo
- UI: botão 'Limpar dados' (🧹) separado do 🗑️ (remove só o cadastro),
  modal com aviso, confirmação e log da operação
- Sidebar: remove link 'Zerar Sistema'
This commit is contained in:
VPS 4 Deploy Agent
2026-05-30 23:42:46 +02:00
parent e0ddb7e0ec
commit 8796558fa3
3 changed files with 173 additions and 12 deletions
@@ -90,14 +90,6 @@ export default function Sidebar({ version, onNewPatient, onUpload, onSettings, o
<button className="nav-item" onClick={onPlugins}>
<i className="fa-solid fa-plug" /> Plugins / Wasabi
</button>
<NavLink
to="/reset"
className={({ isActive }) => `nav-item${isActive ? ' active' : ''}`}
style={{ color: '#dc2626' }}
>
<i className="fa-solid fa-triangle-exclamation" /> Zerar Sistema
</NavLink>
</>
)}
</nav>
@@ -23,6 +23,9 @@ export default function AdminClinicsPage() {
const [showPlugins, setShowPlugins] = useState(false);
const [showSync, setShowSync] = useState(false);
const [showCreate, setShowCreate] = useState(false);
const [wipeClinic, setWipeClinic] = useState(null);
const [wiping, setWiping] = useState(false);
const [wipeLog, setWipeLog] = useState([]);
const loadClinics = async () => {
setLoading(true);
@@ -61,7 +64,7 @@ export default function AdminClinicsPage() {
};
const deleteClinic = async (id) => {
if (!confirm('Tem certeza que deseja excluir este dispositivo permanentemente? O Desktop perderá acesso instantaneamente.')) return;
if (!confirm('Excluir apenas o cadastro do dispositivo? As imagens e pacientes serão mantidos.')) return;
try {
await api.delete(`/admin/clinics/${id}`);
showToast('Dispositivo removido', 'success');
@@ -69,6 +72,27 @@ export default function AdminClinicsPage() {
} catch { showToast('Erro ao excluir', 'error'); }
};
const confirmWipe = (clinic) => setWipeClinic(clinic);
const handleWipe = async () => {
if (!wipeClinic) return;
setWiping(true);
setWipeLog([]);
try {
const { data } = await api.delete(`/admin/clinics/${wipeClinic.id}/wipe`);
setWipeLog(data.log || []);
showToast(`Dados de "${wipeClinic.clinic_name}" apagados com sucesso.`, 'success');
loadClinics();
} catch (e) {
const log = e.response?.data?.log || [];
setWipeLog([...log, `❌ Erro: ${e.response?.data?.message || e.message}`]);
showToast('Erro durante o wipe. Veja o log.', 'error');
} finally {
setWiping(false);
// mantém o modal aberto para mostrar o log; usuário fecha manualmente
}
};
const total = clinics.length;
const active = clinics.filter((c) => c.is_active).length;
const blocked = total - active;
@@ -156,7 +180,13 @@ export default function AdminClinicsPage() {
<input type="checkbox" checked={c.is_active} onChange={(e) => toggleStatus(c.id, e.target.checked)} />
{c.is_active ? 'Ativo' : 'Bloqueado'}
</label>
<button className="btn btn-danger btn-small" onClick={() => deleteClinic(c.id)}>🗑</button>
<button className="btn btn-danger btn-small" onClick={() => deleteClinic(c.id)} title="Remover apenas o cadastro do dispositivo">🗑</button>
<button
className="btn btn-small"
style={{ background: '#7f1d1d', color: '#fff', borderColor: '#991b1b' }}
onClick={() => confirmWipe(c)}
title="Apagar TODOS os dados desta clínica (imagens, banco e Wasabi)"
>🧹 Limpar dados</button>
</div>
</td>
</tr>
@@ -193,6 +223,56 @@ export default function AdminClinicsPage() {
</div>
</Modal>
{/* Modal de confirmação de wipe */}
<Modal
isOpen={!!wipeClinic}
onClose={() => { if (!wiping) { setWipeClinic(null); setWipeLog([]); } }}
title="🧹 Limpar Dados da Clínica"
large={false}
>
{wipeLog.length === 0 ? (
<div>
<div style={{ background: '#fee2e2', border: '1px solid #f87171', borderRadius: 10, padding: 20, marginBottom: 20 }}>
<p style={{ color: '#7f1d1d', fontWeight: 700, marginBottom: 8 }}> Ação irreversível</p>
<p style={{ color: '#991b1b', lineHeight: 1.7 }}>
Serão apagados <strong>permanentemente</strong>:<br />
Todas as imagens de <strong>"{wipeClinic?.clinic_name}"</strong> no banco de dados<br />
A pasta <code style={{ background: '#fecaca', padding: '0 4px', borderRadius: 3 }}>{wipeClinic?.clinic_name}/</code> inteira no Wasabi<br />
O cadastro do dispositivo<br /><br />
Dados de <strong>outras clínicas não são afetados</strong>.
</p>
</div>
<div style={{ display: 'flex', gap: 12, justifyContent: 'flex-end' }}>
<button className="btn btn-secondary" onClick={() => { setWipeClinic(null); setWipeLog([]); }} disabled={wiping}>
Cancelar
</button>
<button
className="btn btn-danger"
onClick={handleWipe}
disabled={wiping}
style={{ background: '#7f1d1d' }}
>
{wiping ? '⏳ Apagando...' : '🧹 Confirmar e Apagar Tudo'}
</button>
</div>
</div>
) : (
<div>
<p style={{ marginBottom: 12, fontWeight: 600, color: 'var(--text-secondary)' }}>Log da operação:</p>
<ul style={{ listStyle: 'none', padding: 0, margin: '0 0 20px', display: 'flex', flexDirection: 'column', gap: 6 }}>
{wipeLog.map((line, i) => (
<li key={i} style={{ background: '#f8f9fa', padding: '6px 12px', borderRadius: 6, fontSize: '0.9rem', fontFamily: 'monospace', color: line.startsWith('❌') ? '#dc2626' : '#166534' }}>
{line.startsWith('❌') ? line : `${line}`}
</li>
))}
</ul>
<button className="btn btn-primary" style={{ width: '100%' }} onClick={() => { setWipeClinic(null); setWipeLog([]); }}>
Fechar
</button>
</div>
)}
</Modal>
<SettingsModal isOpen={showSettings} onClose={() => setShowSettings(false)} />
<UserManagementModal isOpen={showUsers} onClose={() => setShowUsers(false)} />
<PluginsModal isOpen={showPlugins} onClose={() => setShowPlugins(false)} />