Pārlūkot izejas kodu

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

zntt 2 nedēļas atpakaļ
vecāks
revīzija
6740cd1382

+ 67 - 4
src/components/dashboard/DashboardHeaderBar.vue

@@ -18,15 +18,71 @@
     </div>
 
     <div class="col row justify-end items-center">
-      <q-btn flat round dense icon="mdi-bell-outline" color="grey-7" size="sm" />
-    </div>
+
+  <q-btn
+    flat
+    round
+    dense
+    color="grey-7"
+    size="sm"
+    @click="goToNotifications"
+  >
+
+    <q-icon
+      name="mdi-bell-outline"
+      size="20px"
+    />
+
+    <!-- BADGE -->
+    <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 LogoDiariaColorida from 'src/assets/logo_diaria_colorido_sem_texto.svg'
+
+import { computed } from 'vue'
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
 
-defineProps({ data: { type: Object, default: () => null } });
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => null
+  },
+
+  notifications: {
+    type: Array,
+    default: () => []
+  }
+})
+
+//vai para dashboard as notificações tem que ser mocada no backend
+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 +109,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>

+ 11 - 6
src/i18n/locales/pt.json

@@ -444,12 +444,12 @@
       "btn_close": "fechar",
       "btn_help": "Ajuda"
     },
-      "client_proposals": {
-        "candidate": "Candidato",
-        "custom": "sob medida",
-        "age": "({idade} anos)",
-        "distance": "Distância"
-      },
+    "client_proposals": {
+      "candidate": "Candidato",
+      "custom": "sob medida",
+      "age": "({idade} anos)",
+      "distance": "Distância"
+    },
     "last_schedules": {
       "title": "Últimos serviços",
       "reschedule": "reagendar",
@@ -820,5 +820,10 @@
         "btn_keep": "Cancelar o serviço"
       }
     }
+  },
+  "notifications": {
+    "title": "Notificações",
+    "unread": "Não lidas",
+    "mark_all_read": " Marcar todas como lidas"
   }
 }

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

@@ -6,7 +6,10 @@
       </div>
     </template>
     <template v-else>
-      <DashboardHeaderBar :data="headerBar" />
+      <DashboardHeaderBar
+  :data="headerBar"
+  :notifications="notifications"
+/>
       <DashboardRegistrationIncomplete v-if="!registrationComplete" />
       <DashboardSummaryInfos v-else :data="summaryInfos" />
       <DashboardPaymentIncomplete v-if="!hasPaymentMethods" />
@@ -68,11 +71,13 @@ const lastDoneSchedules = ref([]);
 const favoriteProviders = ref([]);
 const providersClose = ref([]);
 const todaySchedules = ref([]);
+const notifications = ref([]);
 const loading = ref(true);
 const showSuccessModal = ref(router.currentRoute.value.fullPath.includes('showSuccessModal') || false);
 
 const registrationComplete = computed(() => store.user?.registration_complete ?? true);
 
+
 const openAcceptedDialog = (schedule) => {
   $q.dialog({
     component: ScheduleAcceptedDialog,
@@ -95,6 +100,7 @@ const reloadDashboard = async () => {
     providersClose.value = response.providersClose ?? [];
     clientProposals.value = response.schedulesProposals ?? [];
     todaySchedules.value = response.todaySchedules ?? [];
+    notifications.value = response.notifications ?? [];
     hasPaymentMethods.value = response.has_payment_methods ?? true;
   }
   if( showSuccessModal.value == true) {

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

@@ -0,0 +1,363 @@
+<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="handleNotification(item)"
+      >
+
+        <div class="notification-wrapper">
+
+          <!-- AVATAR -->
+          <q-avatar size="44px" class="notification-avatar">
+          <img :src="getNotificationIcon(item.type)" />
+          </q-avatar>
+
+          <!-- CONTENT -->
+          <div class="notification-content">
+
+            <div class="notification-header">
+
+              <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 (id) => {
+  try {
+
+    await api.put(`/notifications/${id}/read`)
+
+    notifications.value = notifications.value.map((notification) => {
+
+      if (notification.id === id) {
+        return {
+          ...notification,
+          read: true
+        }
+      }
+
+      return notification
+    })
+
+  } 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)
+  }
+}
+
+const handleNotification = async (notification) => {
+
+  if (!notification.read) {
+    await markAsRead(notification.id)
+  }
+
+  if (
+    notification.origin === 'schedule'
+    && notification.origin_id
+  ) {
+    router.push(`/schedule/${notification.origin_id}`)
+  }
+}
+
+const getNotificationIcon = (type) => {
+
+  switch (type) {
+
+    case 'schedule_client_provider_accepted':
+      return logoDiaria
+
+    case 'schedule_client_provider_refused':
+      return logoDiaria
+
+    case 'schedule_client_provider_cancelled':
+      return logoDiaria
+
+    case 'schedule_client_provider_coming':
+      return logoDiaria
+
+    case 'schedule_client_provider_finished':
+      return logoDiaria
+
+    default:
+      return logoDiaria
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.notifications-page {
+  background: #f4f4f5;
+  min-height: 100vh;
+}
+
+/* HEADER */
+.header {
+  position: relative;
+
+  padding: 18px 16px 12px;
+
+  background: #fff;
+}
+
+.back-btn {
+  position: absolute;
+  left: 10px;
+}
+
+.header-title {
+  width: 100%;
+
+  text-align: center;
+
+  font-size: 17px;
+  font-weight: 700;
+
+  color: #8B5CF6;
+}
+
+/* ACTIONS */
+.actions {
+  padding: 14px 16px 10px;
+}
+
+.unread-text {
+  font-size: 13px;
+  font-weight: 500;
+
+  color: #6b7280;
+}
+
+.mark-read-btn {
+  font-size: 12px;
+  font-weight: 600;
+
+  color: #ff4fd8;
+}
+
+/* LIST */
+.notifications-list {
+  display: flex;
+  flex-direction: column;
+
+  gap: 12px;
+
+  padding: 0 14px 24px;
+}
+
+/* CARD */
+.notification-card {
+  background: #ffffff;
+
+  border-radius: 24px;
+
+  padding: 16px 16px;
+
+  box-shadow:
+    0 2px 10px rgba(0, 0, 0, 0.05);
+
+  transition: 0.2s ease;
+}
+
+.notification-card.unread {
+  background: #ffffff;
+}
+
+/* WRAPPER */
+.notification-wrapper {
+  display: flex;
+
+  align-items: center;
+
+  gap: 12px;
+}
+
+/* AVATAR */
+.notification-avatar {
+  flex-shrink: 0;
+
+  width: 44px !important;
+  height: 44px !important;
+}
+
+/* CONTENT */
+.notification-content {
+  flex: 1;
+
+  min-width: 0;
+}
+
+/* HEADER */
+.notification-header {
+  display: flex;
+
+  align-items: flex-start;
+
+  justify-content: space-between;
+
+  gap: 10px;
+}
+
+/* TITLE */
+.notification-title {
+  font-size: 16px;
+  font-weight: 700;
+
+  color: #5b5b5b;
+
+  line-height: 1.1;
+}
+
+/* DESCRIPTION */
+.notification-description {
+  margin-top: 4px;
+
+  font-size: 12px;
+  font-weight: 500;
+
+  line-height: 1.35;
+
+  color: #8f8f8f;
+}
+
+/* TIME */
+.notification-time {
+  margin-top: 8px;
+
+  font-size: 11px;
+  font-weight: 500;
+
+  color: #b5b5b5;
+}
+
+/* STATUS */
+.status-dot {
+  width: 14px;
+  height: 14px;
+
+  border-radius: 999px;
+
+  background: #e6e6e6;
+
+  flex-shrink: 0;
+}
+
+.status-dot.active {
+  background: #d8d8d8;
+}
+</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
+    }
+  }
+]