carousel, agenda arquivados, agenda cor, agenda arquivados, grupos pacientes, pacientes arquivados - desativados, sessoes verificadas, ajuste notificações, Prontuario, Agenda Animation, Menu Profile, bagdes Profile, Offline

This commit is contained in:
Leonardo
2026-03-18 09:26:09 -03:00
parent 66f67cd40f
commit d6d2fe29d1
55 changed files with 3655 additions and 1512 deletions
+245
View File
@@ -0,0 +1,245 @@
<!-- src/components/AppOfflineOverlay.vue -->
<!-- Detecta offline via eventos nativos do browser + polling de fetch -->
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
const isOnline = ref(true) // começa como true; detecta em onMounted
const wasOffline = ref(false)
const showReconnected = ref(false)
let pollTimer = null
let reconnectedTimer = null
// ── Detecção real: tenta buscar um recurso minúsculo ──────────
async function checkConnectivity () {
try {
// favicon do próprio app (cache busted) — não depende de rede externa
await fetch('/favicon.ico?_t=' + Date.now(), {
method: 'HEAD',
cache: 'no-store',
signal: AbortSignal.timeout(4000)
})
setOnline()
} catch {
setOffline()
}
}
function setOnline () {
if (!isOnline.value && wasOffline.value) {
// acabou de reconectar
showReconnected.value = true
if (reconnectedTimer) clearTimeout(reconnectedTimer)
reconnectedTimer = setTimeout(() => { showReconnected.value = false }, 4000)
}
isOnline.value = true
}
function setOffline () {
if (isOnline.value) wasOffline.value = true
showReconnected.value = false
if (reconnectedTimer) clearTimeout(reconnectedTimer)
isOnline.value = false
}
// ── Eventos nativos do browser ────────────────────────────────
function onBrowserOffline () { setOffline() }
function onBrowserOnline () { checkConnectivity() } // confirma antes de marcar online
onMounted(() => {
window.addEventListener('offline', onBrowserOffline)
window.addEventListener('online', onBrowserOnline)
// Polling a cada 10 s — captura quedas que não disparam evento
pollTimer = setInterval(checkConnectivity, 10_000)
// Verifica estado atual ao montar (útil se já começou offline)
checkConnectivity()
})
onBeforeUnmount(() => {
window.removeEventListener('offline', onBrowserOffline)
window.removeEventListener('online', onBrowserOnline)
clearInterval(pollTimer)
clearTimeout(reconnectedTimer)
})
</script>
<template>
<Teleport to="body">
<!-- Overlay: sem internet -->
<Transition name="offline-fade">
<div
v-if="!isOnline"
class="offline-overlay"
role="alertdialog"
aria-live="assertive"
aria-label="Sem conexão com a internet"
>
<div class="offline-backdrop" />
<div class="offline-card">
<div class="offline-icon-wrap">
<span class="offline-icon-ring" />
<i class="pi pi-wifi offline-icon" />
</div>
<h2 class="offline-title">Sem conexão</h2>
<p class="offline-desc">
Verifique sua internet.<br>Tentando reconectar automaticamente
</p>
<div class="offline-pulse-bar">
<span class="offline-pulse-dot" />
<span class="offline-pulse-dot" style="animation-delay:.2s" />
<span class="offline-pulse-dot" style="animation-delay:.4s" />
</div>
</div>
</div>
</Transition>
<!-- Toast: reconectou -->
<Transition name="reconnect-toast">
<div v-if="showReconnected" class="reconnect-toast" role="status">
<i class="pi pi-check-circle" />
<span>Conexão restabelecida</span>
</div>
</Transition>
</Teleport>
</template>
<style scoped>
/* ── Overlay ─────────────────────────────────────────────── */
.offline-overlay {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.offline-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
}
/* ── Card ────────────────────────────────────────────────── */
.offline-card {
position: relative;
z-index: 1;
background: var(--surface-card, #fff);
border: 1px solid var(--surface-border, #e0e0e0);
border-radius: 20px;
padding: 40px 48px;
max-width: 380px;
width: 100%;
text-align: center;
box-shadow: 0 24px 60px rgba(0, 0, 0, .25);
}
/* ── Ícone ───────────────────────────────────────────────── */
.offline-icon-wrap {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
margin-bottom: 24px;
}
.offline-icon-ring {
position: absolute;
inset: 0;
border-radius: 50%;
border: 2px solid var(--primary-color, #6366f1);
opacity: 0.25;
animation: ring-pulse 2s ease-in-out infinite;
}
.offline-icon {
font-size: 2.2rem;
color: var(--primary-color, #6366f1);
opacity: 0.85;
position: relative;
}
@keyframes ring-pulse {
0%, 100% { transform: scale(1); opacity: 0.25; }
50% { transform: scale(1.18); opacity: 0.10; }
}
/* ── Texto ───────────────────────────────────────────────── */
.offline-title {
font-size: 1.3rem;
font-weight: 700;
color: var(--text-color, #1a1a2e);
margin: 0 0 10px;
}
.offline-desc {
font-size: 0.88rem;
color: var(--text-color-secondary, #666);
margin: 0 0 28px;
line-height: 1.6;
}
/* ── Dots de pulso ───────────────────────────────────────── */
.offline-pulse-bar {
display: flex;
justify-content: center;
gap: 7px;
}
.offline-pulse-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--primary-color, #6366f1);
opacity: 0.7;
animation: dot-bounce 1.2s ease-in-out infinite;
}
@keyframes dot-bounce {
0%, 80%, 100% { transform: scale(0.7); opacity: 0.4; }
40% { transform: scale(1); opacity: 1; }
}
/* ── Transição do overlay ────────────────────────────────── */
.offline-fade-enter-active { transition: opacity 0.3s ease; }
.offline-fade-leave-active { transition: opacity 0.4s ease; }
.offline-fade-enter-from,
.offline-fade-leave-to { opacity: 0; }
/* ── Toast de reconexão ──────────────────────────────────── */
.reconnect-toast {
position: fixed;
bottom: 28px;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
display: flex;
align-items: center;
gap: 8px;
background: #16a34a;
color: #fff;
font-size: 0.875rem;
font-weight: 600;
padding: 10px 20px;
border-radius: 999px;
box-shadow: 0 4px 20px rgba(22, 163, 74, .4);
white-space: nowrap;
}
.reconnect-toast .pi { font-size: 1rem; }
.reconnect-toast-enter-active { transition: opacity 0.3s ease, transform 0.3s ease; }
.reconnect-toast-leave-active { transition: opacity 0.4s ease, transform 0.4s ease; }
.reconnect-toast-enter-from { opacity: 0; transform: translateX(-50%) translateY(12px); }
.reconnect-toast-leave-to { opacity: 0; transform: translateX(-50%) translateY(12px); }
</style>
+2 -2
View File
@@ -98,7 +98,7 @@
</div>
<div class="text-xs text-surface-500">
Dica: Gerar usuário preenche automaticamente com dados fictícios.
Dica: "Gerar usuário" preenche automaticamente com dados fictícios.
</div>
</div>
@@ -136,7 +136,7 @@ import { supabase } from '@/lib/supabase/client'
const { canSee } = useRoleGuard()
/**
* Lista curada de pensadores influentes na psicanálise e seu entorno.
* Lista "curada" de pensadores influentes na psicanálise e seu entorno.
* Usada para geração rápida de dados fictícios.
*/
const PSICANALISE_PENSADORES = Object.freeze([
@@ -134,7 +134,7 @@ onMounted(load)
</div>
<div v-else>
<!-- Resumo tipo cards -->
<!-- Resumo tipo "cards" -->
<div class="grid grid-cols-12 gap-3 mb-4">
<div class="col-span-12 md:col-span-4 p-3 rounded-xl border border-[var(--surface-border)]">
<div class="text-600 text-sm">Tipo de slots</div>
@@ -158,7 +158,7 @@ onMounted(load)
<div class="flex items-center justify-between">
<div class="text-900 font-medium">Jornada do dia</div>
<div class="text-600 text-sm">
(Isso vem das suas janelas semanais)
(Isso vem das suas "janelas semanais")
</div>
</div>
+2 -2
View File
@@ -76,7 +76,7 @@ function normalizeIntervals(list) {
return merged
}
// retorna sobras de [s,e] depois de remover intervalos ocupados
// retorna "sobras" de [s,e] depois de remover intervalos ocupados
function subtractIntervals(s, e, occupiedMerged) {
let segments = [{ s, e }]
for (const occ of occupiedMerged) {
@@ -127,7 +127,7 @@ function addPauseSmart({ label, inicio, fim }) {
fim: minToHHMM(seg.e)
}))
// se houve recorte, avisa
// se houve "recorte", avisa
if (segments.length !== 1 || (segments[0].s !== s || segments[0].e !== e)) {
toast.add({
severity: 'info',
+1 -1
View File
@@ -122,7 +122,7 @@
<div class="text-gray-900 mb-2 text-3xl font-semibold">Joséphine Miller</div>
<span class="text-gray-600 text-2xl">Peak Interactive</span>
<p class="text-gray-900 sm:line-height-2 md:line-height-4 text-2xl mt-6" style="max-width: 800px">
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
</p>
<img src="/demo/images/landing/peak-logo.svg" class="mt-6" alt="Company logo" />
</div>
@@ -103,7 +103,7 @@ function goToHistory () {
<!-- Empty state -->
<div v-else class="notification-drawer__empty">
<i class="pi pi-bell-slash notification-drawer__empty-icon" />
<p class="notification-drawer__empty-text">Tudo em dia</p>
<p class="notification-drawer__empty-text">Tudo em dia por aqui 🎉</p>
<p class="notification-drawer__empty-sub">Nenhuma notificação{{ filter === 'unread' ? ' não lida' : '' }}.</p>
</div>
</div>
@@ -113,7 +113,7 @@ function goToHistory () {
<div class="notification-drawer__footer">
<button class="notification-drawer__history-link" @click="goToHistory">
<i class="pi pi-history" />
Ver histórico completo
Ver histórico completo
</button>
</div>
</template>
@@ -4,41 +4,38 @@ import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { formatDistanceToNow } from 'date-fns'
import { ptBR } from 'date-fns/locale'
import { useNotificationStore } from '@/stores/notificationStore'
const props = defineProps({
item: {
type: Object,
required: true
}
item: { type: Object, required: true }
})
const emit = defineEmits(['read', 'archive'])
const router = useRouter()
const store = useNotificationStore()
const typeIconMap = {
new_scheduling: { icon: 'pi-inbox', color: 'text-red-500' },
new_patient: { icon: 'pi-user-plus', color: 'text-sky-500' },
recurrence_alert: { icon: 'pi-refresh', color: 'text-amber-500' },
session_status: { icon: 'pi-calendar-times', color: 'text-orange-500' }
const typeMap = {
new_scheduling: { icon: 'pi-inbox', border: 'border-red-500', },
new_patient: { icon: 'pi-user-plus', border: 'border-sky-500', },
recurrence_alert: { icon: 'pi-refresh', border: 'border-amber-500', },
session_status: { icon: 'pi-calendar-times', border: 'border-orange-500', },
}
const typeIcon = computed(() => typeIconMap[props.item.type] || { icon: 'pi-bell', color: 'text-gray-400' })
const meta = computed(() => typeMap[props.item.type] || { icon: 'pi-bell', border: 'border-gray-300' })
const isUnread = computed(() => !props.item.read_at)
const timeAgo = computed(() =>
formatDistanceToNow(new Date(props.item.created_at), { addSuffix: true, locale: ptBR })
)
const avatarInitials = computed(() =>
props.item.payload?.avatar_initials || '??'
)
const initials = computed(() => props.item.payload?.avatar_initials || '?')
function handleRowClick () {
const deeplink = props.item.payload?.deeplink
if (deeplink) {
router.push(deeplink)
store.drawerOpen = false
emit('read', props.item.id)
}
}
@@ -56,45 +53,42 @@ function handleArchive (e) {
<template>
<div
class="notification-item"
:class="{ 'notification-item--unread': isUnread }"
@click="handleRowClick"
class="notif-item"
:class="[meta.border, isUnread ? 'notif-item--unread' : '']"
role="button"
tabindex="0"
@click="handleRowClick"
@keydown.enter="handleRowClick"
>
<!-- Dot indicador -->
<span v-if="isUnread" class="notification-item__dot" aria-hidden="true" />
<!-- Ícone de tipo -->
<span class="notification-item__type-icon" aria-hidden="true">
<i :class="['pi', typeIcon.icon, typeIcon.color]" />
</span>
<!-- Ícone do tipo -->
<div class="notif-item__icon" aria-hidden="true">
<i :class="['pi', meta.icon]" />
</div>
<!-- Avatar -->
<span class="notification-item__avatar" aria-hidden="true">
{{ avatarInitials }}
</span>
<div class="notif-item__avatar" aria-hidden="true">
{{ initials }}
</div>
<!-- Conteúdo -->
<div class="notification-item__content">
<p class="notification-item__title">{{ item.payload?.title }}</p>
<p class="notification-item__detail">{{ item.payload?.detail }}</p>
<p class="notification-item__time">{{ timeAgo }}</p>
<div class="notif-item__body">
<p class="notif-item__title">{{ item.payload?.title }}</p>
<p class="notif-item__detail">{{ item.payload?.detail }}</p>
<p class="notif-item__time">{{ timeAgo }}</p>
</div>
<!-- Ações -->
<div class="notification-item__actions" @click.stop>
<div class="notif-item__actions" @click.stop>
<button
v-if="isUnread"
class="notification-item__action-btn"
class="notif-item__btn"
title="Marcar como lida"
@click="handleMarkRead"
>
<i class="pi pi-check" />
</button>
<button
class="notification-item__action-btn"
class="notif-item__btn"
title="Arquivar"
@click="handleArchive"
>
@@ -105,101 +99,92 @@ function handleArchive (e) {
</template>
<style scoped>
.notification-item {
position: relative;
.notif-item {
display: flex;
align-items: flex-start;
gap: 0.625rem;
padding: 0.75rem 1rem;
cursor: pointer;
border-left-width: 3px;
border-left-style: solid;
border-bottom: 1px solid var(--surface-border);
background: transparent;
cursor: pointer;
transition: background 0.15s;
}
.notification-item:hover {
.notif-item:hover {
background: var(--surface-hover);
}
.notification-item--unread {
background: color-mix(in srgb, var(--primary-color) 6%, transparent);
.notif-item--unread {
background: rgba(99, 102, 241, 0.05);
}
.notification-item--unread:hover {
background: color-mix(in srgb, var(--primary-color) 10%, transparent);
.notif-item--unread:hover {
background: rgba(99, 102, 241, 0.09);
}
.notification-item__dot {
position: absolute;
left: 0.25rem;
top: 50%;
transform: translateY(-50%);
width: 6px;
height: 6px;
border-radius: 50%;
background: #6366f1;
.notif-item__icon {
flex-shrink: 0;
}
.notification-item__type-icon {
display: flex;
align-items: center;
padding-top: 0.125rem;
flex-shrink: 0;
font-size: 0.85rem;
padding-top: 0.15rem;
font-size: 0.9rem;
color: var(--text-color-secondary);
}
.notification-item__avatar {
display: flex;
align-items: center;
justify-content: center;
.notif-item__avatar {
flex-shrink: 0;
width: 2rem;
height: 2rem;
border-radius: 50%;
background: var(--surface-200);
color: var(--text-color);
font-size: 0.7rem;
background: linear-gradient(135deg, #6366f1, #38bdf8);
color: #fff;
font-size: 0.68rem;
font-weight: 700;
flex-shrink: 0;
letter-spacing: 0.05em;
letter-spacing: 0.04em;
display: flex;
align-items: center;
justify-content: center;
}
.notification-item__content {
.notif-item__body {
flex: 1;
min-width: 0;
}
.notification-item__title {
.notif-item__title {
font-weight: 600;
font-size: 0.85rem;
color: var(--text-color);
margin: 0 0 0.125rem;
margin: 0 0 0.1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.notification-item__detail {
font-size: 0.8rem;
.notif-item__detail {
font-size: 0.78rem;
color: var(--text-color-secondary);
margin: 0 0 0.125rem;
margin: 0 0 0.1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.notification-item__time {
font-size: 0.72rem;
.notif-item__time {
font-size: 0.7rem;
color: var(--text-color-secondary);
opacity: 0.7;
margin: 0;
opacity: 0.75;
}
.notification-item__actions {
.notif-item__actions {
display: flex;
align-items: center;
gap: 0.125rem;
opacity: 0;
transition: opacity 0.15s;
}
.notification-item:hover .notification-item__actions {
.notif-item:hover .notif-item__actions {
opacity: 1;
}
.notification-item__action-btn {
.notif-item__btn {
width: 1.75rem;
height: 1.75rem;
border-radius: 50%;
@@ -212,7 +197,7 @@ function handleArchive (e) {
font-size: 0.75rem;
transition: background 0.15s, color 0.15s;
}
.notification-item__action-btn:hover {
.notif-item__btn:hover {
background: var(--surface-border);
color: var(--text-color);
}
@@ -0,0 +1,219 @@
<!-- src/components/patients/PatientActionMenu.vue -->
<script setup>
import { ref } from 'vue'
import { useToast } from 'primevue/usetoast'
import { usePatientLifecycle } from '@/composables/usePatientLifecycle'
const props = defineProps({
patient: { type: Object, required: true },
hasHistory: { type: Boolean, default: false }
})
const emit = defineEmits(['updated'])
const toast = useToast()
const { deletePatient, deactivatePatient, archivePatient, reactivatePatient } = usePatientLifecycle()
const loading = ref(false)
const menu = ref()
// ── Dialogs ────────────────────────────────────────────────
const deactivateDialogOpen = ref(false)
const archiveDialogOpen = ref(false)
const hasHistoryDialogOpen = ref(false)
// ── Helpers ───────────────────────────────────────────────
function showSuccess (detail) {
toast.add({ severity: 'success', summary: 'Ok', detail, life: 2500 })
}
function showError (detail) {
toast.add({ severity: 'error', summary: 'Erro', detail, life: 4000 })
}
async function runAction (fn, successMsg) {
loading.value = true
try {
const result = await fn(props.patient.id)
if (!result.ok) { showError(result.message || result.error?.message || 'Falha ao executar ação.'); return }
showSuccess(successMsg)
emit('updated')
} catch (e) {
showError(e?.message || 'Falha inesperada.')
} finally {
loading.value = false
}
}
// ── Desativar — sempre mostra dialog informativo
function handleDeactivate () {
deactivateDialogOpen.value = true
}
async function confirmDeactivate () {
deactivateDialogOpen.value = false
await runAction(deactivatePatient, 'Paciente desativado.')
}
// ── Arquivar — sempre mostra dialog informativo
function handleArchive () {
archiveDialogOpen.value = true
}
async function confirmArchive () {
archiveDialogOpen.value = false
hasHistoryDialogOpen.value = false
await runAction(archivePatient, 'Paciente arquivado.')
}
// ── Excluir — se tem histórico, mostra dialog informativo
function handleDelete () {
if (props.hasHistory) {
hasHistoryDialogOpen.value = true
return
}
doDelete()
}
async function doDelete () {
loading.value = true
try {
const result = await deletePatient(props.patient.id)
if (!result.ok) { showError(result.message || 'Não foi possível excluir.'); return }
showSuccess('Paciente excluído.')
emit('updated')
} catch (e) {
showError(e?.message || 'Falha inesperada.')
} finally {
loading.value = false
}
}
// ── Menu de ações ─────────────────────────────────────────
function menuItems () {
const status = props.patient.status
const items = []
if (status === 'Ativo') {
items.push({ label: 'Desativar', icon: 'pi pi-pause', command: () => handleDeactivate() })
items.push({ label: 'Arquivar', icon: 'pi pi-archive', command: () => handleArchive() })
}
if (status === 'Inativo') {
items.push({ label: 'Reativar', icon: 'pi pi-play', command: () => runAction(reactivatePatient, 'Paciente reativado.') })
items.push({ label: 'Arquivar', icon: 'pi pi-archive', command: () => handleArchive() })
}
if (status === 'Arquivado') {
items.push({ label: 'Reativar', icon: 'pi pi-play', command: () => runAction(reactivatePatient, 'Paciente reativado.') })
}
if (status === 'Alta' || status === 'Encaminhado') {
items.push({ label: 'Arquivar', icon: 'pi pi-archive', command: () => handleArchive() })
items.push({ label: 'Reativar', icon: 'pi pi-play', command: () => runAction(reactivatePatient, 'Paciente reativado.') })
}
if (items.length) items.push({ separator: true })
items.push({ label: 'Excluir paciente', icon: 'pi pi-trash', class: 'text-red-500', command: () => handleDelete() })
return items
}
</script>
<template>
<div class="flex gap-1 justify-end">
<Button
icon="pi pi-ellipsis-v"
severity="secondary"
outlined
size="small"
:loading="loading"
v-tooltip.top="'Ações do paciente'"
@click="(e) => menu.toggle(e)"
/>
<Menu ref="menu" :model="menuItems()" popup appendTo="body" />
</div>
<!-- Dialog: Desativar -->
<Dialog
v-model:visible="deactivateDialogOpen"
modal
:draggable="false"
header="Desativar paciente"
:style="{ width: '460px', maxWidth: '95vw' }"
>
<div class="flex gap-3 items-start py-2">
<i class="pi pi-exclamation-triangle text-amber-500 text-2xl mt-0.5 flex-shrink-0" />
<div class="text-sm text-[var(--text-color)] leading-relaxed space-y-2">
<p class="m-0 font-semibold">
Atenção: este paciente pode possuir sessões agendadas e/ou recorrências ativas.
</p>
<p class="m-0">
Ao desativar, todas as sessões e recorrências serão mantidas na agenda porém novos agendamentos ficarão bloqueados.
</p>
<p class="m-0 text-[var(--text-color-secondary)]">
Recomendamos revisar a agenda e encerrar as recorrências manualmente antes de prosseguir.
</p>
</div>
</div>
<template #footer>
<div class="flex gap-2 justify-end">
<Button label="Cancelar" severity="secondary" outlined @click="deactivateDialogOpen = false" />
<Button label="Entendo, desativar mesmo assim!" icon="pi pi-pause" severity="warn" @click="confirmDeactivate" />
</div>
</template>
</Dialog>
<!-- Dialog: Arquivar -->
<Dialog
v-model:visible="archiveDialogOpen"
modal
:draggable="false"
header="Arquivar paciente"
:style="{ width: '460px', maxWidth: '95vw' }"
>
<div class="flex gap-3 items-start py-2">
<i class="pi pi-archive text-slate-500 text-2xl mt-0.5 flex-shrink-0" />
<div class="text-sm text-[var(--text-color)] leading-relaxed space-y-2">
<p class="m-0 font-semibold">O que acontece ao arquivar um paciente?</p>
<ul class="m-0 pl-4 space-y-1 text-[var(--text-color)]">
<li>O paciente sairá da listagem ativa e ficará oculto nas buscas padrão.</li>
<li>Todo o histórico clínico, sessões e registros financeiros serão preservados.</li>
<li>Novos agendamentos para este paciente ficarão bloqueados.</li>
<li>O paciente pode ser <strong>reativado</strong> a qualquer momento pelo menu de ações.</li>
</ul>
<p class="m-0 text-[var(--text-color-secondary)]">
Arquivar é indicado para pacientes que concluíram o acompanhamento ou que estão em pausa prolongada.
</p>
</div>
</div>
<template #footer>
<div class="flex gap-2 justify-end">
<Button label="Cancelar" severity="secondary" outlined @click="archiveDialogOpen = false" />
<Button label="Compreendo, arquivar mesmo assim!" icon="pi pi-archive" @click="confirmArchive" />
</div>
</template>
</Dialog>
<!-- Dialog: Exclusão bloqueada (paciente com histórico) -->
<Dialog
v-model:visible="hasHistoryDialogOpen"
modal
:draggable="false"
header="Exclusão não permitida"
:style="{ width: '420px', maxWidth: '95vw' }"
>
<div class="flex gap-3 items-start py-2">
<i class="pi pi-info-circle text-amber-500 text-2xl mt-0.5 flex-shrink-0" />
<p class="text-[var(--text-color)] text-sm leading-relaxed m-0">
Este paciente possui histórico clínico e <strong>não pode ser removido permanentemente</strong>.
Apenas o arquivamento é permitido para pacientes com registros de atendimento.
</p>
</div>
<template #footer>
<div class="flex gap-2 justify-end">
<Button label="Cancelar" severity="secondary" outlined @click="hasHistoryDialogOpen = false" />
<Button label="Arquivar paciente" icon="pi pi-archive" @click="confirmArchive" />
</div>
</template>
</Dialog>
</template>