| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492 |
- <template>
- <div class="q-pa-sm">
- <q-breadcrumbs
- v-if="displayBreadcrumbs != null"
- class="q-mb-xs text-secondary flex items-center"
- :class="$q.screen.lt.sm ? '' : 'q-pl-lg'"
- >
- <q-breadcrumbs-el
- v-for="crumb in displayBreadcrumbs"
- :key="crumb.name || crumb.label"
- :label="crumb.title"
- :to="crumb.name ? { name: crumb.name, params: crumb.params } : null"
- />
- </q-breadcrumbs>
- <div
- v-else
- style="max-width: 180px"
- :class="$q.screen.lt.sm ? '' : 'q-pl-lg'"
- >
- <q-skeleton type="text" />
- </div>
- <div class="flex items-center justify-between">
- <div
- class="flex items-center q-pl-xs"
- :class="$q.screen.lt.sm ? '' : 'q-pt-md'"
- >
- <span v-if="title" class="text-h6 text-primary text-weight-regular">
- {{ title }}
- </span>
- <div v-else style="width: 280px">
- <q-skeleton type="text" height="40px" />
- </div>
- <q-icon
- v-if="showFilterIcon"
- name="mdi-filter-outline"
- :color="filterOpen ? 'background' : 'primary'"
- size="sm"
- class="q-ml-sm cursor-pointer"
- :class="filterOpen ? 'bg-primary' : ''"
- :style="
- filterOpen
- ? 'border-radius: 8px; padding: 2px'
- : 'border-radius: 8px; padding: 2px; border: 1.5px solid var(--q-primary)'
- "
- @click="$emit('show-filter')"
- />
- </div>
- <div
- class="flex items-center q-pr-sm"
- :class="$q.screen.lt.sm ? '' : 'q-pt-md'"
- >
- <div class="flex items-center no-wrap" style="gap: 12px">
- <div
- class="flex items-center no-wrap q-gutter-x-md q-px-sm q-ml-md"
- style="flex-shrink: 0"
- >
- <q-img
- :src="user?.avatar_url || 'icons/user-icon.jpg'"
- class="avatar-circle"
- />
- <div
- v-if="$q.screen.gt.xs"
- class="column q-gutter-y-none"
- style="white-space: nowrap"
- >
- <span class="text-body2 text-center">{{ user?.name }}</span>
- </div>
- </div>
- <template v-if="$q.screen.gt.sm && lastLoginFormatted">
- <q-separator
- vertical
- style="height: 36px; width: 2px; flex-shrink: 0"
- color="dark"
- />
- <div
- class="column"
- style="line-height: 1.2; white-space: nowrap; flex-shrink: 0"
- >
- <span class="text-caption text-grey-6 text-primary text-center"
- >Ultimo acesso</span
- >
- <span class="text-caption text-primary text-center">{{
- lastLoginFormatted
- }}</span>
- </div>
- </template>
- <div
- class="flex items-center no-wrap"
- style="gap: 2px; flex-shrink: 0"
- >
- <!-- 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-scroll-area
- v-if="notifications.length"
- style="height: 360px"
- :thumb-style="{ width: '5px', borderRadius: '4px' }"
- >
- <q-list 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>
- </q-scroll-area>
- <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" />
- </div>
- </div>
- <q-separator class="q-my-sm" />
- </div>
- </template>
- <script setup>
- 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: {
- type: String,
- default: null,
- },
- breadcrumbs: {
- type: Object,
- default: null,
- },
- filterOpen: {
- type: Boolean,
- default: false,
- },
- showFilterIcon: {
- type: Boolean,
- default: true,
- },
- });
- 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: false,
- },
- {
- id: 4,
- icon: "mdi-alert-circle-outline",
- title: "Pagamento em atraso",
- message: "A unidade Zona Sul possui uma parcela vencida há 3 dias.",
- datetime: "2026-06-29T08:20:00",
- read: false,
- },
- {
- id: 5,
- icon: "mdi-check-decagram-outline",
- title: "Pagamento confirmado",
- message: "O boleto da unidade Barra foi compensado com sucesso.",
- datetime: "2026-06-28T14:10:00",
- read: false,
- },
- {
- id: 6,
- icon: "mdi-lifebuoy",
- title: "Novo chamado de suporte",
- message: "A unidade Norte abriu um chamado sobre acesso ao sistema.",
- datetime: "2026-06-27T10:55:00",
- read: true,
- },
- {
- id: 7,
- icon: "mdi-package-variant-closed",
- title: "Pacote de aulas publicado",
- message: "Um novo pacote foi disponibilizado para todas as unidades.",
- datetime: "2026-06-26T17:30:00",
- read: true,
- },
- {
- id: 8,
- icon: "mdi-cake-variant-outline",
- title: "Aniversariantes do dia",
- message: "5 alunos fazem aniversário hoje na sua rede.",
- datetime: "2026-06-25T07:00:00",
- read: true,
- },
- {
- id: 9,
- icon: "mdi-store-plus-outline",
- title: "Nova unidade cadastrada",
- message: "A unidade Jardins foi adicionada à franquia.",
- datetime: "2026-06-24T13:45:00",
- read: true,
- },
- {
- id: 10,
- icon: "mdi-shield-check-outline",
- title: "Atualização de segurança",
- message: "Recomendamos revisar as permissões de acesso dos usuários.",
- datetime: "2026-06-23T09: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;
- const d = new Date(raw.replace(" ", "T") + "Z");
- return new Intl.DateTimeFormat("pt-BR", {
- day: "2-digit",
- month: "2-digit",
- year: "numeric",
- hour: "2-digit",
- minute: "2-digit",
- timeZone: "America/Sao_Paulo",
- }).format(d);
- });
- const route = useRoute();
- const { t } = useI18n();
- const displayBreadcrumbs = computed(() => {
- if (!breadcrumbs && breadcrumbs?.length <= 0) {
- return null;
- } else if (breadcrumbs && breadcrumbs?.length > 0) {
- return (breadcrumbs || []).map((b) => {
- if (b.translate) {
- return t(b.title);
- } else {
- return b.title;
- }
- });
- }
- if (!route.meta?.breadcrumbs && route.meta?.breadcrumbs?.length <= 0) {
- return null;
- } else if (route.meta?.breadcrumbs && route.meta?.breadcrumbs?.length > 0) {
- return (route.meta?.breadcrumbs || []).map((b) => {
- if (b.translate) {
- return {
- ...b,
- title: t(b.title),
- };
- } else {
- return {
- ...b,
- title: b.title,
- };
- }
- });
- }
- return null;
- });
- </script>
- <style scoped>
- .avatar-circle {
- width: 36px;
- height: 36px;
- 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>
|