|
|
@@ -56,7 +56,11 @@
|
|
|
class="flex items-center no-wrap q-gutter-x-md q-px-sm q-ml-md"
|
|
|
style="flex-shrink: 0"
|
|
|
>
|
|
|
- <q-img src="icons/user-icon.jpg" class="avatar-circle" />
|
|
|
+ <q-img
|
|
|
+ :src="user?.avatar_url || 'icons/user-icon.jpg'"
|
|
|
+ class="avatar-circle"
|
|
|
+ />
|
|
|
+
|
|
|
|
|
|
<div
|
|
|
v-if="$q.screen.gt.xs"
|
|
|
@@ -91,9 +95,154 @@
|
|
|
class="flex items-center no-wrap"
|
|
|
style="gap: 2px; flex-shrink: 0"
|
|
|
>
|
|
|
- <q-btn flat round dense icon="mdi-bell-badge" color="secondary" />
|
|
|
- <q-btn flat round dense icon="mdi-account" color="secondary" />
|
|
|
- <q-btn flat round dense icon="mdi-cog-outline" color="secondary" />
|
|
|
+ <!-- Notificações -->
|
|
|
+ <q-btn flat round dense icon="mdi-bell-badge" color="secondary">
|
|
|
+ <q-badge
|
|
|
+ v-if="unreadCount > 0"
|
|
|
+ color="negative"
|
|
|
+ floating
|
|
|
+ rounded
|
|
|
+ :label="unreadCount"
|
|
|
+ />
|
|
|
+ <q-menu
|
|
|
+ anchor="bottom right"
|
|
|
+ self="top right"
|
|
|
+ :offset="[0, 8]"
|
|
|
+ class="header-menu"
|
|
|
+ >
|
|
|
+ <div class="notifications-panel">
|
|
|
+ <div class="row items-center justify-between q-px-md q-py-sm">
|
|
|
+ <span class="text-subtitle2 text-primary text-weight-medium">
|
|
|
+ Notificações
|
|
|
+ </span>
|
|
|
+ <q-btn
|
|
|
+ flat
|
|
|
+ dense
|
|
|
+ no-caps
|
|
|
+ size="sm"
|
|
|
+ color="secondary"
|
|
|
+ label="Marcar tudo como lido"
|
|
|
+ :disable="unreadCount === 0"
|
|
|
+ @click="markAllAsRead"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <q-separator />
|
|
|
+
|
|
|
+ <q-list v-if="notifications.length" separator>
|
|
|
+ <q-item
|
|
|
+ v-for="n in notifications"
|
|
|
+ :key="n.id"
|
|
|
+ clickable
|
|
|
+ :class="!n.read ? 'bg-grey-2' : ''"
|
|
|
+ @click="markAsRead(n.id)"
|
|
|
+ >
|
|
|
+ <q-item-section avatar top>
|
|
|
+ <q-icon
|
|
|
+ :name="n.icon"
|
|
|
+ :color="n.read ? 'grey-5' : 'secondary'"
|
|
|
+ size="24px"
|
|
|
+ />
|
|
|
+ </q-item-section>
|
|
|
+ <q-item-section>
|
|
|
+ <q-item-label
|
|
|
+ class="text-primary"
|
|
|
+ :class="!n.read ? 'text-weight-medium' : ''"
|
|
|
+ >
|
|
|
+ {{ n.title }}
|
|
|
+ </q-item-label>
|
|
|
+ <q-item-label caption lines="2">
|
|
|
+ {{ n.message }}
|
|
|
+ </q-item-label>
|
|
|
+ <q-item-label caption class="text-grey-6 q-mt-xs">
|
|
|
+ {{ formatNotificationDate(n.datetime) }}
|
|
|
+ </q-item-label>
|
|
|
+ </q-item-section>
|
|
|
+ <q-item-section v-if="!n.read" side top>
|
|
|
+ <q-badge color="negative" rounded style="padding: 4px" />
|
|
|
+ </q-item-section>
|
|
|
+ </q-item>
|
|
|
+ </q-list>
|
|
|
+
|
|
|
+ <div
|
|
|
+ v-else
|
|
|
+ class="column items-center justify-center q-pa-lg text-grey-6"
|
|
|
+ >
|
|
|
+ <q-icon name="mdi-bell-off-outline" size="32px" />
|
|
|
+ <span class="text-caption q-mt-sm">Sem notificações</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </q-menu>
|
|
|
+ </q-btn>
|
|
|
+
|
|
|
+ <!-- Perfil do usuário -->
|
|
|
+ <q-btn flat round dense icon="mdi-account" color="secondary">
|
|
|
+ <q-menu
|
|
|
+ anchor="bottom right"
|
|
|
+ self="top right"
|
|
|
+ :offset="[0, 8]"
|
|
|
+ class="header-menu"
|
|
|
+ >
|
|
|
+ <div class="profile-panel q-pa-md">
|
|
|
+ <div class="row items-center no-wrap q-gutter-x-md">
|
|
|
+ <q-img
|
|
|
+ :src="user?.avatar_url || 'icons/user-icon.jpg'"
|
|
|
+ class="avatar-circle"
|
|
|
+ />
|
|
|
+ <div class="column" style="min-width: 0">
|
|
|
+ <span class="text-body2 text-primary text-weight-medium ellipsis">
|
|
|
+ {{ user?.name || "Usuário" }}
|
|
|
+ </span>
|
|
|
+ <span class="text-caption text-grey-6 ellipsis">
|
|
|
+ {{ user?.email }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <q-separator class="q-my-md" />
|
|
|
+ <q-btn
|
|
|
+ v-close-popup
|
|
|
+ flat
|
|
|
+ no-caps
|
|
|
+ align="left"
|
|
|
+ class="full-width"
|
|
|
+ color="primary"
|
|
|
+ icon="mdi-account-edit-outline"
|
|
|
+ label="Editar"
|
|
|
+ @click="goToEditProfile"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </q-menu>
|
|
|
+ </q-btn>
|
|
|
+
|
|
|
+ <!-- Configurações -->
|
|
|
+ <q-btn flat round dense icon="mdi-cog-outline" color="secondary">
|
|
|
+ <q-menu
|
|
|
+ anchor="bottom right"
|
|
|
+ self="top right"
|
|
|
+ :offset="[0, 8]"
|
|
|
+ class="header-menu"
|
|
|
+ >
|
|
|
+ <div class="settings-panel q-pa-md">
|
|
|
+ <div class="column q-mb-md">
|
|
|
+ <span class="text-caption text-grey-6">Unidade</span>
|
|
|
+ <span class="text-body2 text-primary text-weight-medium">
|
|
|
+ {{ unitLabel }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <q-separator class="q-mb-md" />
|
|
|
+ <q-btn
|
|
|
+ v-close-popup
|
|
|
+ flat
|
|
|
+ no-caps
|
|
|
+ align="left"
|
|
|
+ class="full-width"
|
|
|
+ color="negative"
|
|
|
+ icon="mdi-logout"
|
|
|
+ label="Sair"
|
|
|
+ @click="logoutFn"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </q-menu>
|
|
|
+ </q-btn>
|
|
|
</div>
|
|
|
</div>
|
|
|
<slot name="after" />
|
|
|
@@ -104,10 +253,11 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { computed } from "vue";
|
|
|
-import { useRoute } from "vue-router";
|
|
|
+import { computed, ref } from "vue";
|
|
|
+import { useRoute, useRouter } from "vue-router";
|
|
|
import { useI18n } from "vue-i18n";
|
|
|
import { userStore } from "src/stores/user";
|
|
|
+import { useAuth } from "src/composables/useAuth";
|
|
|
|
|
|
const { title, breadcrumbs, filterOpen, showFilterIcon } = defineProps({
|
|
|
title: {
|
|
|
@@ -131,8 +281,80 @@ const { title, breadcrumbs, filterOpen, showFilterIcon } = defineProps({
|
|
|
defineEmits(["show-filter"]);
|
|
|
|
|
|
const store = userStore();
|
|
|
+const router = useRouter();
|
|
|
+const { logout } = useAuth();
|
|
|
const user = computed(() => store.user);
|
|
|
|
|
|
+// Unidade exibida em Configurações. Sem unidade vinculada = matriz.
|
|
|
+const unitLabel = computed(() => {
|
|
|
+ const units = store.user?.units ?? [];
|
|
|
+ if (!units.length) return "Franqueadora / Matriz";
|
|
|
+ return units.map((u) => u.fantasy_name).join(", ");
|
|
|
+});
|
|
|
+
|
|
|
+// TODO: substituir por dados reais da API de notificações (mock por enquanto).
|
|
|
+const notifications = ref([
|
|
|
+ {
|
|
|
+ id: 1,
|
|
|
+ icon: "mdi-cash-multiple",
|
|
|
+ title: "Novo faturamento gerado",
|
|
|
+ message: "As parcelas do mês foram geradas para as unidades ativas.",
|
|
|
+ datetime: "2026-07-02T09:15:00",
|
|
|
+ read: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 2,
|
|
|
+ icon: "mdi-account-plus-outline",
|
|
|
+ title: "Novo usuário cadastrado",
|
|
|
+ message: "Um novo usuário foi adicionado à unidade Centro.",
|
|
|
+ datetime: "2026-07-01T16:42:00",
|
|
|
+ read: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 3,
|
|
|
+ icon: "mdi-file-document-outline",
|
|
|
+ title: "Contrato atualizado",
|
|
|
+ message: "O contrato da franquia foi atualizado com sucesso.",
|
|
|
+ datetime: "2026-06-30T11:05:00",
|
|
|
+ read: true,
|
|
|
+ },
|
|
|
+]);
|
|
|
+
|
|
|
+const unreadCount = computed(
|
|
|
+ () => notifications.value.filter((n) => !n.read).length,
|
|
|
+);
|
|
|
+
|
|
|
+const markAllAsRead = () => {
|
|
|
+ notifications.value.forEach((n) => (n.read = true));
|
|
|
+};
|
|
|
+
|
|
|
+const markAsRead = (id) => {
|
|
|
+ const item = notifications.value.find((n) => n.id === id);
|
|
|
+ if (item) item.read = true;
|
|
|
+};
|
|
|
+
|
|
|
+const formatNotificationDate = (raw) => {
|
|
|
+ if (!raw) return "";
|
|
|
+ const d = new Date(raw);
|
|
|
+ return new Intl.DateTimeFormat("pt-BR", {
|
|
|
+ day: "2-digit",
|
|
|
+ month: "2-digit",
|
|
|
+ year: "numeric",
|
|
|
+ hour: "2-digit",
|
|
|
+ minute: "2-digit",
|
|
|
+ }).format(d);
|
|
|
+};
|
|
|
+
|
|
|
+const goToEditProfile = () => {
|
|
|
+ if (!user.value?.id) return;
|
|
|
+ router.push({ name: "UserEditPage", params: { id: user.value.id } });
|
|
|
+};
|
|
|
+
|
|
|
+const logoutFn = async () => {
|
|
|
+ await logout();
|
|
|
+ router.push({ name: "LoginPage" });
|
|
|
+};
|
|
|
+
|
|
|
const lastLoginFormatted = computed(() => {
|
|
|
const raw = store.user?.last_login_at;
|
|
|
if (!raw) return null;
|
|
|
@@ -190,4 +412,19 @@ const displayBreadcrumbs = computed(() => {
|
|
|
border-radius: 50%;
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
+
|
|
|
+.header-menu {
|
|
|
+ border-radius: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.notifications-panel {
|
|
|
+ width: 340px;
|
|
|
+ max-width: 90vw;
|
|
|
+}
|
|
|
+
|
|
|
+.profile-panel,
|
|
|
+.settings-panel {
|
|
|
+ width: 260px;
|
|
|
+ max-width: 90vw;
|
|
|
+}
|
|
|
</style>
|