Procházet zdrojové kódy

feat: :sparkles: feat (modulo-de-notificação) Foi criado e ajustado a pagina de notificões

Foi criada a página de notificações responsável por listar todas as notificações enviadas tanto para o prestador quanto para o cliente.

fase:dev | origin:escopo
kayo henrique před 3 týdny
rodič
revize
f8f465591b

+ 65 - 5
src/components/dashboard/DashboardHeaderBar.vue

@@ -17,16 +17,69 @@
       <img :src="LogoDiariaColorida" alt="Diária" class="dashboard-logo" />
     </div>
 
-    <div class="col row justify-end items-center">
-      <q-btn flat round dense icon="mdi-bell-outline" color="grey-7" size="sm" />
-    </div>
+   <div class="col row justify-end items-center">
+
+  <q-btn
+    flat
+    round
+    dense
+    color="grey-7"
+    size="sm"
+    @click="goToNotifications"
+  >
+
+    <q-icon
+      name="mdi-bell-outline"
+      size="20px"
+    />
+
+    <q-badge
+      v-if="unreadNotifications > 0"
+      floating
+      rounded
+      color="pink"
+      class="notification-badge"
+    >
+      {{ unreadNotifications }}
+    </q-badge>
+
+  </q-btn>
+
+</div>
   </div>
 </template>
 
 <script setup>
-import LogoDiariaColorida from 'src/assets/logo_diaria_colorido_sem_texto.svg';
+import { computed } from 'vue'
+import { useRouter } from 'vue-router'
+import LogoDiariaColorida from 'src/assets/logo_diaria_colorido_sem_texto.svg'
+
+const router = useRouter()
+
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => null
+  },
 
-defineProps({ data: { type: Object, default: () => null } });
+  notifications: {
+    type: Array,
+    default: () => []
+  }
+})
+
+const unreadNotifications = computed(() => {
+  return props.notifications.filter((notification) => !notification.read).length
+})
+
+const goToNotifications = () => {
+  router.push({
+    name: 'NotificationsPage',
+    query: {
+      notifications: JSON.stringify(props.notifications)
+    }
+  })
+}
 </script>
 
 <style scoped lang="scss">
@@ -53,4 +106,11 @@ defineProps({ data: { type: Object, default: () => null } });
   color: #999;
   line-height: 1;
 }
+
+.notification-badge {
+  min-width: 16px;
+  height: 16px;
+  font-size: 10px;
+  font-weight: 700;
+}
 </style>

+ 6 - 1
src/pages/dashboard/DashboardPage.vue

@@ -6,7 +6,10 @@
       </div>
     </template>
     <template v-else>
-      <DashboardHeaderBar :data="headerBar" />
+      <DashboardHeaderBar
+  :data="headerBar"
+  :notifications="notifications"
+/>
       <DashboardSummaryInfos :data="summaryInfos" />
       <DashboardPriceSuggest :data="priceSuggestion"/>
       <DashboardTodayServices v-if="todayServices?.length > 0" :data="todayServices" @refresh="loadDashboard" @rate="openRatingDialog" />
@@ -52,6 +55,7 @@ const solicitations = ref([]);
 const todayServices = ref([]);
 const nextSchedules = ref([]);
 const opportunities = ref([]);
+const notifications = ref([]);
 
 const showSuccessModal = ref(router.currentRoute.value.fullPath.includes('showSuccessModal') || false);
 
@@ -69,6 +73,7 @@ const loadDashboard = async () => {
     todayServices.value = response.todayServices ?? [];
     nextSchedules.value = response.nextSchedules ?? [];
     opportunities.value = response.opportunities ?? [];
+    notifications.value = response.notifications ?? [];
   }
 
   if( showSuccessModal.value == true) {

+ 236 - 0
src/pages/notifications/NotificationsPage.vue

@@ -0,0 +1,236 @@
+<template>
+  <q-page class="notifications-page">
+
+    <!-- HEADER -->
+    <div class="header row items-center">
+
+      <q-btn
+        flat
+        round
+        dense
+        icon="chevron_left"
+        class="back-btn"
+        @click="router.back()"
+      />
+
+      <div class="header-title">
+        {{ $t('notifications.title') }}
+      </div>
+
+    </div>
+
+    <!-- ACTIONS -->
+    <div class="actions row justify-between items-center">
+
+      <div class="unread-text">
+        {{ $t('notifications.unread') }} {{ unreadCount }}
+      </div>
+
+      <q-btn
+        flat
+        no-caps
+        class="mark-read-btn"
+        label="✓ Marcar todas como lidas"
+        @click="markAllAsRead"
+      />
+
+    </div>
+
+    <!-- LIST -->
+    <div class="notifications-list">
+
+      <q-card
+        v-for="item in notifications"
+        :key="item.id"
+        flat
+        clickable
+        class="notification-card"
+        :class="{ unread: !item.read }"
+        @click="markAsRead(item.id)"
+      >
+
+        <div class="row no-wrap items-start">
+
+          <!-- AVATAR -->
+          <q-avatar size="42px" class="q-mr-md">
+            <img :src="item.avatar" />
+          </q-avatar>
+
+          <!-- CONTENT -->
+          <div class="col">
+
+            <div class="row justify-between items-start">
+
+              <div class="notification-title">
+                {{ item.title }}
+              </div>
+
+              <!-- STATUS -->
+              <div
+                class="status-dot"
+                :class="{ active: !item.read }"
+              />
+
+            </div>
+
+            <div class="notification-description">
+              {{ item.description }}
+            </div>
+
+            <div class="notification-time">
+              {{ item.time }}
+            </div>
+
+          </div>
+
+        </div>
+
+      </q-card>
+
+    </div>
+
+  </q-page>
+</template>
+
+<script setup>
+import { computed, ref, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+
+const router = useRouter()
+
+
+const route = useRoute()
+
+const notifications = ref([])
+
+onMounted(() => {
+  notifications.value = JSON.parse(route.query.notifications || '[]')
+})
+
+const unreadCount = computed(() => {
+  return notifications.value.filter((n) => !n.read).length
+})
+
+const markAsRead = (id) => {
+  notifications.value = notifications.value.map((notification) => {
+    if (notification.id === id) {
+      return {
+        ...notification,
+        read: true
+      }
+    }
+
+    return notification
+  })
+}
+
+const markAllAsRead = () => {
+  notifications.value = notifications.value.map((notification) => ({
+    ...notification,
+    read: true
+  }))
+}
+</script>
+
+<style scoped lang="scss">
+.notifications-page {
+  background: #f5f5f7;
+  min-height: 100vh;
+}
+
+/* HEADER */
+.header {
+  position: relative;
+  padding: 18px 16px 12px;
+  background: white;
+}
+
+.back-btn {
+  position: absolute;
+  left: 10px;
+}
+
+.header-title {
+  width: 100%;
+  text-align: center;
+
+  font-size: 16px;
+  font-weight: 700;
+
+  color: #8B5CF6;
+}
+
+/* ACTIONS */
+.actions {
+  padding: 14px 16px;
+}
+
+.unread-text {
+  font-size: 13px;
+  color: #666;
+}
+
+.mark-read-btn {
+  font-size: 12px;
+  color: #ff4fd8;
+}
+
+/* LIST */
+.notifications-list {
+  display: flex;
+  flex-direction: column;
+}
+
+/* CARD */
+.notification-card {
+  border-radius: 0;
+  padding: 16px;
+  background: white;
+
+  border-bottom: 1px solid #ececec;
+
+  transition: 0.2s ease;
+}
+
+.notification-card.unread {
+  background: #f8eff7;
+}
+
+/* TITLE */
+.notification-title {
+  font-size: 14px;
+  font-weight: 700;
+  color: #555;
+}
+
+/* DESCRIPTION */
+.notification-description {
+  margin-top: 4px;
+
+  font-size: 12px;
+  line-height: 1.4;
+
+  color: #777;
+}
+
+/* TIME */
+.notification-time {
+  margin-top: 10px;
+
+  font-size: 11px;
+  color: #aaa;
+}
+
+/* STATUS */
+.status-dot {
+  width: 10px;
+  height: 10px;
+
+  border-radius: 50%;
+  background: #ddd;
+}
+
+.status-dot.active {
+  background: #ff5be1;
+}
+</style>

+ 11 - 0
src/router/routes/notifications.route.js

@@ -0,0 +1,11 @@
+export default [
+  {
+    path: '/notifications',
+    name: 'NotificationsPage',
+    component: () => import('src/pages/notifications/NotificationsPage.vue'),
+    meta: {
+      title: 'Notificações',
+      requireAuth: true
+    }
+  }
+]