Correcao Sidebar Classico e Rail, Correcao Layout, Ajuste de Breakpoint para Tailwind, Ajuste AppTopbar, Ajuste Menu PopOver, Recriado Paleta de Cores, Inserido algumas animações leves, Reajuste Cor items NOVOS da tabela, Drawer Ajuda Corrigido no Logout, Whatsapp, sms, email, recursos extras

This commit is contained in:
Leonardo
2026-03-24 21:26:58 -03:00
parent a89d1f5560
commit 53a4980396
453 changed files with 121427 additions and 174407 deletions
@@ -15,250 +15,220 @@
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useNotificationStore } from '@/stores/notificationStore'
import NotificationItem from './NotificationItem.vue'
import { ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import { useNotificationStore } from '@/stores/notificationStore';
import NotificationItem from './NotificationItem.vue';
const store = useNotificationStore()
const router = useRouter()
const store = useNotificationStore();
const router = useRouter();
const filter = ref('unread') // 'unread' | 'all'
const filter = ref('unread'); // 'unread' | 'all'
const drawerOpen = computed({
get: () => store.drawerOpen,
set: (v) => { store.drawerOpen = v }
})
get: () => store.drawerOpen,
set: (v) => {
store.drawerOpen = v;
}
});
const displayedItems = computed(() =>
filter.value === 'unread' ? store.unreadItems : store.allItems
)
const displayedItems = computed(() => (filter.value === 'unread' ? store.unreadItems : store.allItems));
function handleRead (id) {
store.markRead(id)
// Fecha o drawer e deixa a navegação acontecer
store.drawerOpen = false
function handleRead(id) {
store.markRead(id);
// Fecha o drawer e deixa a navegação acontecer
store.drawerOpen = false;
}
function handleArchive (id) {
store.archive(id)
function handleArchive(id) {
store.archive(id);
}
function goToHistory () {
router.push('/therapist/notificacoes')
store.drawerOpen = false
function goToHistory() {
router.push('/therapist/notificacoes');
store.drawerOpen = false;
}
</script>
<template>
<Drawer
v-model:visible="drawerOpen"
position="right"
:style="{ width: '380px' }"
:pt="{ header: { class: 'notification-drawer__header' } }"
>
<!-- Header -->
<template #header>
<div class="notification-drawer__header-content">
<span class="notification-drawer__title">Notificações</span>
<Badge
v-if="store.unreadCount > 0"
:value="store.unreadCount > 99 ? '99+' : store.unreadCount"
severity="danger"
/>
</div>
</template>
<Drawer v-model:visible="drawerOpen" position="right" :style="{ width: '380px' }" :pt="{ header: { class: 'notification-drawer__header' } }">
<!-- Header -->
<template #header>
<div class="notification-drawer__header-content">
<span class="notification-drawer__title">Notificações</span>
<Badge v-if="store.unreadCount > 0" :value="store.unreadCount > 99 ? '99+' : store.unreadCount" severity="danger" />
</div>
</template>
<!-- Corpo -->
<div class="notification-drawer__body">
<!-- Ação em lote -->
<div class="notification-drawer__toolbar">
<!-- Filtro tabs -->
<div class="notification-drawer__tabs">
<button
class="notification-drawer__tab"
:class="{ 'notification-drawer__tab--active': filter === 'unread' }"
@click="filter = 'unread'"
>
Não lidas
<span v-if="store.unreadCount > 0" class="notification-drawer__tab-count">
{{ store.unreadCount }}
</span>
</button>
<button
class="notification-drawer__tab"
:class="{ 'notification-drawer__tab--active': filter === 'all' }"
@click="filter = 'all'"
>
Todas
</button>
<!-- Corpo -->
<div class="notification-drawer__body">
<!-- Ação em lote -->
<div class="notification-drawer__toolbar">
<!-- Filtro tabs -->
<div class="notification-drawer__tabs">
<button class="notification-drawer__tab" :class="{ 'notification-drawer__tab--active': filter === 'unread' }" @click="filter = 'unread'">
Não lidas
<span v-if="store.unreadCount > 0" class="notification-drawer__tab-count">
{{ store.unreadCount }}
</span>
</button>
<button class="notification-drawer__tab" :class="{ 'notification-drawer__tab--active': filter === 'all' }" @click="filter = 'all'">Todas</button>
</div>
<Button v-if="store.unreadCount > 0" link size="small" label="Marcar todas como lidas" @click="store.markAllRead()" class="notification-drawer__mark-all" />
</div>
<!-- Lista -->
<div v-if="displayedItems.length > 0" class="notification-drawer__list">
<NotificationItem v-for="item in displayedItems" :key="item.id" :item="item" @read="handleRead" @archive="handleArchive" />
</div>
<!-- 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 por aqui 🎉</p>
<p class="notification-drawer__empty-sub">Nenhuma notificação{{ filter === 'unread' ? ' não lida' : '' }}.</p>
</div>
</div>
<Button
v-if="store.unreadCount > 0"
link
size="small"
label="Marcar todas como lidas"
@click="store.markAllRead()"
class="notification-drawer__mark-all"
/>
</div>
<!-- Lista -->
<div v-if="displayedItems.length > 0" class="notification-drawer__list">
<NotificationItem
v-for="item in displayedItems"
:key="item.id"
:item="item"
@read="handleRead"
@archive="handleArchive"
/>
</div>
<!-- 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 por aqui 🎉</p>
<p class="notification-drawer__empty-sub">Nenhuma notificação{{ filter === 'unread' ? ' não lida' : '' }}.</p>
</div>
</div>
<!-- Footer -->
<template #footer>
<div class="notification-drawer__footer">
<button class="notification-drawer__history-link" @click="goToHistory">
<i class="pi pi-history" />
Ver histórico completo
</button>
</div>
</template>
</Drawer>
<!-- Footer -->
<template #footer>
<div class="notification-drawer__footer">
<button class="notification-drawer__history-link" @click="goToHistory">
<i class="pi pi-history" />
Ver histórico completo
</button>
</div>
</template>
</Drawer>
</template>
<style scoped>
.notification-drawer__header-content {
display: flex;
align-items: center;
gap: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.notification-drawer__title {
font-size: 1rem;
font-weight: 600;
color: var(--text-color);
font-size: 1rem;
font-weight: 600;
color: var(--text-color);
}
.notification-drawer__body {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.notification-drawer__toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 1rem;
border-bottom: 1px solid var(--surface-border);
flex-shrink: 0;
gap: 0.5rem;
flex-wrap: wrap;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 1rem;
border-bottom: 1px solid var(--surface-border);
flex-shrink: 0;
gap: 0.5rem;
flex-wrap: wrap;
}
.notification-drawer__tabs {
display: flex;
gap: 0.25rem;
display: flex;
gap: 0.25rem;
}
.notification-drawer__tab {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.3rem 0.75rem;
border-radius: 999px;
border: 1px solid var(--surface-border);
background: transparent;
color: var(--text-color-secondary);
font-size: 0.8rem;
cursor: pointer;
transition: background 0.15s, color 0.15s;
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.3rem 0.75rem;
border-radius: 999px;
border: 1px solid var(--surface-border);
background: transparent;
color: var(--text-color-secondary);
font-size: 0.8rem;
cursor: pointer;
transition:
background 0.15s,
color 0.15s;
}
.notification-drawer__tab--active {
background: var(--primary-color);
color: var(--primary-color-text);
border-color: var(--primary-color);
background: var(--primary-color);
color: var(--primary-color-text);
border-color: var(--primary-color);
}
.notification-drawer__tab-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.1rem;
height: 1.1rem;
padding: 0 0.25rem;
border-radius: 999px;
background: rgba(255,255,255,0.25);
font-size: 0.7rem;
font-weight: 700;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.1rem;
height: 1.1rem;
padding: 0 0.25rem;
border-radius: 999px;
background: rgba(255, 255, 255, 0.25);
font-size: 0.7rem;
font-weight: 700;
}
.notification-drawer__mark-all {
white-space: nowrap;
font-size: 0.78rem !important;
padding: 0 !important;
white-space: nowrap;
font-size: 0.78rem !important;
padding: 0 !important;
}
.notification-drawer__list {
flex: 1;
overflow-y: auto;
flex: 1;
overflow-y: auto;
}
.notification-drawer__empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 3rem 1rem;
text-align: center;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 3rem 1rem;
text-align: center;
}
.notification-drawer__empty-icon {
font-size: 2.5rem;
color: var(--text-color-secondary);
opacity: 0.4;
font-size: 2.5rem;
color: var(--text-color-secondary);
opacity: 0.4;
}
.notification-drawer__empty-text {
font-size: 1rem;
font-weight: 600;
color: var(--text-color);
margin: 0;
font-size: 1rem;
font-weight: 600;
color: var(--text-color);
margin: 0;
}
.notification-drawer__empty-sub {
font-size: 0.82rem;
color: var(--text-color-secondary);
margin: 0;
font-size: 0.82rem;
color: var(--text-color-secondary);
margin: 0;
}
.notification-drawer__footer {
display: flex;
justify-content: center;
padding: 0.75rem 1rem;
border-top: 1px solid var(--surface-border);
display: flex;
justify-content: center;
padding: 0.75rem 1rem;
border-top: 1px solid var(--surface-border);
}
.notification-drawer__history-link {
display: inline-flex;
align-items: center;
gap: 0.4rem;
background: transparent;
border: none;
color: var(--primary-color);
font-size: 0.85rem;
cursor: pointer;
padding: 0;
transition: opacity 0.15s;
display: inline-flex;
align-items: center;
gap: 0.4rem;
background: transparent;
border: none;
color: var(--primary-color);
font-size: 0.85rem;
cursor: pointer;
padding: 0;
transition: opacity 0.15s;
}
.notification-drawer__history-link:hover {
opacity: 0.75;
text-decoration: underline;
opacity: 0.75;
text-decoration: underline;
}
</style>
+131 -147
View File
@@ -15,205 +15,189 @@
|--------------------------------------------------------------------------
-->
<script setup>
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'
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 emit = defineEmits(['read', 'archive']);
const router = useRouter()
const store = useNotificationStore()
const router = useRouter();
const store = useNotificationStore();
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', },
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 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 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);
}
}
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 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)
}
function handleMarkRead(e) {
e.stopPropagation();
emit('read', props.item.id);
}
function handleMarkRead (e) {
e.stopPropagation()
emit('read', props.item.id)
}
function handleArchive (e) {
e.stopPropagation()
emit('archive', props.item.id)
function handleArchive(e) {
e.stopPropagation();
emit('archive', props.item.id);
}
</script>
<template>
<div
class="notif-item"
:class="[meta.border, isUnread ? 'notif-item--unread' : '']"
role="button"
tabindex="0"
@click="handleRowClick"
@keydown.enter="handleRowClick"
>
<!-- Ícone do tipo -->
<div class="notif-item__icon" aria-hidden="true">
<i :class="['pi', meta.icon]" />
</div>
<div class="notif-item" :class="[meta.border, isUnread ? 'notif-item--unread' : '']" role="button" tabindex="0" @click="handleRowClick" @keydown.enter="handleRowClick">
<!-- Ícone do tipo -->
<div class="notif-item__icon" aria-hidden="true">
<i :class="['pi', meta.icon]" />
</div>
<!-- Avatar -->
<div class="notif-item__avatar" aria-hidden="true">
{{ initials }}
</div>
<!-- Avatar -->
<div class="notif-item__avatar" aria-hidden="true">
{{ initials }}
</div>
<!-- Conteúdo -->
<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>
<!-- Conteúdo -->
<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="notif-item__actions" @click.stop>
<button
v-if="isUnread"
class="notif-item__btn"
title="Marcar como lida"
@click="handleMarkRead"
>
<i class="pi pi-check" />
</button>
<button
class="notif-item__btn"
title="Arquivar"
@click="handleArchive"
>
<i class="pi pi-times" />
</button>
<!-- Ações -->
<div class="notif-item__actions" @click.stop>
<button v-if="isUnread" class="notif-item__btn" title="Marcar como lida" @click="handleMarkRead">
<i class="pi pi-check" />
</button>
<button class="notif-item__btn" title="Arquivar" @click="handleArchive">
<i class="pi pi-times" />
</button>
</div>
</div>
</div>
</template>
<style scoped>
.notif-item {
display: flex;
align-items: flex-start;
gap: 0.625rem;
padding: 0.75rem 1rem;
border-left-width: 3px;
border-left-style: solid;
border-bottom: 1px solid var(--surface-border);
background: transparent;
cursor: pointer;
transition: background 0.15s;
display: flex;
align-items: flex-start;
gap: 0.625rem;
padding: 0.75rem 1rem;
border-left-width: 3px;
border-left-style: solid;
border-bottom: 1px solid var(--surface-border);
background: transparent;
cursor: pointer;
transition: background 0.15s;
}
.notif-item:hover {
background: var(--surface-hover);
background: var(--surface-hover);
}
.notif-item--unread {
background: rgba(99, 102, 241, 0.05);
background: rgba(99, 102, 241, 0.05);
}
.notif-item--unread:hover {
background: rgba(99, 102, 241, 0.09);
background: rgba(99, 102, 241, 0.09);
}
.notif-item__icon {
flex-shrink: 0;
display: flex;
align-items: center;
padding-top: 0.15rem;
font-size: 0.9rem;
color: var(--text-color-secondary);
flex-shrink: 0;
display: flex;
align-items: center;
padding-top: 0.15rem;
font-size: 0.9rem;
color: var(--text-color-secondary);
}
.notif-item__avatar {
flex-shrink: 0;
width: 2rem;
height: 2rem;
border-radius: 50%;
background: linear-gradient(135deg, #6366f1, #38bdf8);
color: #fff;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.04em;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 2rem;
height: 2rem;
border-radius: 50%;
background: linear-gradient(135deg, #6366f1, #38bdf8);
color: #fff;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.04em;
display: flex;
align-items: center;
justify-content: center;
}
.notif-item__body {
flex: 1;
min-width: 0;
flex: 1;
min-width: 0;
}
.notif-item__title {
font-weight: 600;
font-size: 0.85rem;
color: var(--text-color);
margin: 0 0 0.1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
font-size: 0.85rem;
color: var(--text-color);
margin: 0 0 0.1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.notif-item__detail {
font-size: 0.78rem;
color: var(--text-color-secondary);
margin: 0 0 0.1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 0.78rem;
color: var(--text-color-secondary);
margin: 0 0 0.1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.notif-item__time {
font-size: 0.7rem;
color: var(--text-color-secondary);
opacity: 0.7;
margin: 0;
font-size: 0.7rem;
color: var(--text-color-secondary);
opacity: 0.7;
margin: 0;
}
.notif-item__actions {
display: flex;
align-items: center;
gap: 0.125rem;
opacity: 0;
transition: opacity 0.15s;
display: flex;
align-items: center;
gap: 0.125rem;
opacity: 0;
transition: opacity 0.15s;
}
.notif-item:hover .notif-item__actions {
opacity: 1;
opacity: 1;
}
.notif-item__btn {
width: 1.75rem;
height: 1.75rem;
border-radius: 50%;
border: none;
background: transparent;
color: var(--text-color-secondary);
display: grid;
place-items: center;
cursor: pointer;
font-size: 0.75rem;
transition: background 0.15s, color 0.15s;
width: 1.75rem;
height: 1.75rem;
border-radius: 50%;
border: none;
background: transparent;
color: var(--text-color-secondary);
display: grid;
place-items: center;
cursor: pointer;
font-size: 0.75rem;
transition:
background 0.15s,
color 0.15s;
}
.notif-item__btn:hover {
background: var(--surface-border);
color: var(--text-color);
background: var(--surface-border);
color: var(--text-color);
}
</style>