Parcourir la source

feat(header): add user avatar and notifications panel to header

ebagabee il y a 4 semaines
Parent
commit
32ffd60630
1 fichiers modifiés avec 243 ajouts et 6 suppressions
  1. 243 6
      src/components/layout/DefaultHeaderPage.vue

+ 243 - 6
src/components/layout/DefaultHeaderPage.vue

@@ -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>