Explorar o código

Merge branch 'feature/DIARIA-kay-modulo-de-notificação' of Softpar/sfp_front_vue_diarista_prestador into development

zntt hai 2 semanas
pai
achega
c6c194b093

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

+ 5 - 0
src/i18n/locales/pt.json

@@ -808,5 +808,10 @@
     "payments": "Pagamentos",
     "agenda": "Agenda",
     "profile": "Perfil"
+  },
+  "notifications": {
+    "title": "Notificações",
+    "unread": "Não lidas",
+    "mark_all_read": " Marcar todas como lidas"
   }
 }

+ 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) {

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

@@ -0,0 +1,274 @@
+<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)"
+  >
+
+    <div class="row no-wrap items-start">
+
+      <!-- AVATAR -->
+      <q-avatar size="42px" class="q-mr-md">
+        <img :src="logoDiaria" />
+      </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 { useRouter } from 'vue-router'
+
+import { api } from 'boot/axios'
+
+import logoDiaria from 'src/assets/logo_diaria_colorido_sem_texto.svg'
+
+const router = useRouter()
+
+const notifications = ref([])
+
+onMounted(() => {
+  loadNotifications()
+})
+
+const unreadCount = computed(() => {
+  return notifications.value.filter((n) => !n.read).length
+})
+
+const loadNotifications = async () => {
+  try {
+
+    const response = await api.get('/notifications')
+
+    notifications.value = response.data.payload || []
+
+  } catch (error) {
+    console.error(error)
+  }
+}
+
+const markAsRead = async (notification) => {
+  try {
+
+    await api.put(`/notifications/${notification.id}/read`)
+
+    notifications.value = notifications.value.map((item) => {
+
+      if (item.id === notification.id) {
+        return {
+          ...item,
+          read: true
+        }
+      }
+
+      return item
+    })
+
+    if (
+      notification.origin === 'schedule'
+      && notification.origin_id
+    ) {
+      router.push(`/schedule/${notification.origin_id}`)
+    }
+
+  } catch (error) {
+    console.error(error)
+  }
+}
+
+const markAllAsRead = async () => {
+  try {
+
+    await api.put('/notifications/read-all')
+
+    notifications.value = notifications.value.map((notification) => ({
+      ...notification,
+      read: true
+    }))
+
+  } catch (error) {
+    console.error(error)
+  }
+}
+</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
+    }
+  }
+]