Explorar el Código

feat: :sparkles: feat (agenda) criada tela de agenda

foi criada a tela de agenda para o app cliente e prestador com exibicao dos servicos ativos e ja feitos, opcao de avaliar e reagendar

fase:dev | origin:escopo
Gustavo Zanatta hace 1 mes
padre
commit
cc33d1d98a

+ 6 - 0
src/api/providerCalendar.js

@@ -0,0 +1,6 @@
+import api from 'src/api'
+
+export const getProviderCalendar = async () => {
+  const { data } = await api.get('/dados-agenda-prestador')
+  return data.payload
+}

+ 23 - 1
src/css/quasar.variables.scss

@@ -87,10 +87,32 @@ $colors: (
   // Blue
   "info-light": #60A5FA,
   // Light Blue
-  "info-dark": #2563EB
+  "info-dark": #2563EB,
   // Dark Blue
+
+  // Pastel Background Variants (for chips/badges)
+  "success-bg": #D1FAE5,
+  // Light Green background
+  "warning-bg": #FEF3C7,
+  // Light Amber background
+  "info-bg": #DBEAFE,
+  // Light Blue background
+  "secondary-bg": #FCE7F3,
+  // Light Pink background
+  "neutral-bg": #F3F4F6,
+  // Light Grey background
+  "status-finished": #9CA3AF
+  // Grey text for finished status
 );
 
+// Standalone status chip variables (usable in component scoped SCSS)
+$status-bg-pending:   #FEF3C7;
+$status-bg-confirmed: #D1FAE5;
+$status-bg-started:   #DBEAFE;
+$status-bg-finished:  #F3F4F6;
+$status-bg-cancelled: #FCE7F3;
+$status-color-finished: #9CA3AF;
+
 // Dark Theme Color Overrides
 // $colors-dark: (
 //   // Primary Colors and Variants (Purple - adjusted for dark mode)

+ 18 - 0
src/i18n/locales/en.json

@@ -357,6 +357,24 @@
         "until_8h": "Up to 8h",
         "place_home": "Residential",
         "schedule": "Schedule"
+      },
+      "agenda": {
+        "title": "Agenda",
+        "upcoming_title": "Upcoming services",
+        "completed_title": "Completed services",
+        "type_default": "Appointment",
+        "type_custom": "Custom",
+        "status_pending": "Pending",
+        "status_accepted": "Confirmed",
+        "status_paid": "Confirmed",
+        "status_started": "In progress",
+        "status_finished": "Completed",
+        "status_cancelled": "Cancelled",
+        "btn_view_details": "view details",
+        "btn_rate": "rate",
+        "btn_reschedule": "reschedule",
+        "empty_upcoming": "No scheduled services",
+        "empty_completed": "No completed services"
       }
     }
   },

+ 18 - 0
src/i18n/locales/es.json

@@ -355,6 +355,24 @@
         "until_8h": "Hasta 8h",
         "place_home": "Residencial",
         "schedule": "Agendar"
+      },
+      "agenda": {
+        "title": "Agenda",
+        "upcoming_title": "Próximos servicios",
+        "completed_title": "Servicios completados",
+        "type_default": "Agendamiento",
+        "type_custom": "A medida",
+        "status_pending": "Pendiente",
+        "status_accepted": "Confirmado",
+        "status_paid": "Confirmado",
+        "status_started": "En progreso",
+        "status_finished": "Completado",
+        "status_cancelled": "Cancelado",
+        "btn_view_details": "ver detalles",
+        "btn_rate": "evaluar",
+        "btn_reschedule": "reprogramar",
+        "empty_upcoming": "Sin servicios programados",
+        "empty_completed": "Sin servicios completados"
       }
     }
   },

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

@@ -369,6 +369,24 @@
         "until_8h": "Até 8h",
         "place_home": "Residencial",
         "schedule": "Agendar"
+      },
+      "agenda": {
+        "title": "Agenda",
+        "upcoming_title": "Próximos serviços",
+        "completed_title": "Serviços concluídos",
+        "type_default": "Agendamento",
+        "type_custom": "Sob Medida",
+        "status_pending": "Pendente",
+        "status_accepted": "Confirmado",
+        "status_paid": "Confirmado",
+        "status_started": "Em andamento",
+        "status_finished": "Concluído",
+        "status_cancelled": "Cancelado",
+        "btn_view_details": "ver detalhes",
+        "btn_rate": "avaliar",
+        "btn_reschedule": "reagendar",
+        "empty_upcoming": "Nenhum serviço agendado",
+        "empty_completed": "Nenhum serviço concluído"
       }
     }
   },

+ 1 - 1
src/layouts/MainLayout.vue

@@ -75,7 +75,7 @@ const navItems = computed(() => [
     icon: "mdi-credit-card-outline",
   },
   {
-    name: "AgendaPage",
+    name: "CalendarPage",
     label: t('nav.agenda'),
     icon: "mdi-calendar-blank-outline",
   },

+ 0 - 52
src/pages/agenda/AgendaPage.vue

@@ -1,52 +0,0 @@
-<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
-<template>
-  <section class="mobile-placeholder">
-    <div class="mobile-placeholder__badge">
-      <q-icon name="mdi-calendar-blank-outline" />
-    </div>
-    <h1 class="mobile-placeholder__title">Agenda</h1>
-    <p class="mobile-placeholder__description">
-      Área reservada para exibir compromissos, confirmações e próximas diárias.
-    </p>
-  </section>
-</template>
-
-<style scoped>
-.mobile-placeholder {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  min-height: calc(100dvh - 240px);
-  padding: 32px 20px;
-  text-align: center;
-}
-
-.mobile-placeholder__badge {
-  display: grid;
-  place-items: center;
-  width: 88px;
-  height: 88px;
-  border-radius: 28px;
-  margin-bottom: 20px;
-  background: linear-gradient(180deg, rgba(255, 0, 234, 0.14), rgba(107, 17, 203, 0.08));
-  color: #ff00ea;
-  font-size: 44px;
-}
-
-.mobile-placeholder__title {
-  margin: 0 0 8px;
-  font-size: 28px;
-  font-weight: 700;
-  line-height: 1.1;
-  color: #4d4d4d;
-}
-
-.mobile-placeholder__description {
-  max-width: 280px;
-  margin: 0;
-  font-size: 16px;
-  line-height: 1.5;
-  color: #8d8d8d;
-}
-</style>

+ 366 - 0
src/pages/calendar/CalendarPage.vue

@@ -0,0 +1,366 @@
+<template>
+  <q-page class="bg-page q-pb-xl">
+    <div class="calendar-header row items-center bg-white">
+      <q-space />
+      <span class="text-subtitle1 text-weight-bold gradient-diarista">{{ $t('provider.dashboard.agenda.title') }}</span>
+      <q-space />
+    </div>
+
+    <template v-if="loading">
+      <div class="row items-center justify-center full-width" style="height: 60vh">
+        <q-spinner-dots color="primary" />
+      </div>
+    </template>
+
+    <template v-else>
+      <div class="q-mt-md q-mx-md">
+        <div class="section-title gradient-diarista q-mb-sm">{{ $t('provider.dashboard.agenda.upcoming_title') }}</div>
+
+        <template v-if="upcomingSchedules.length > 0">
+          <q-card
+            v-for="item in upcomingSchedules"
+            :key="item.id"
+            class="calendar-card bg-surface shadow-card q-mb-sm"
+            :flat="false"
+          >
+            <q-card-section class="q-pa-sm">
+              <div class="row no-wrap items-start q-gutter-x-sm">
+                <q-avatar size="44px">
+                  <img :src="item.customer_photo || defaultAvatar">
+                </q-avatar>
+
+                <div class="col columns">
+                  <span class="text-name ellipsis">{{ item.client_name }}</span>
+                  <div class="row items-center no-wrap">
+                    <span class="text-date-bold">{{ formatWeekday(item.date) }}</span>
+                    <span class="text-date-regular">{{ ', ' + formatDayMonth(item.date) }}</span>
+                  </div>
+                  <span class="text-date-regular">
+                    {{ $t('common.from') }}
+                    <span class="text-date-bold">{{ item.start_time?.slice(0, 5) }}</span>
+                    {{ $t('common.to') }}
+                    <span class="text-date-bold">{{ item.end_time?.slice(0, 5) }}</span>
+                  </span>
+                </div>
+
+                <div class="col-auto column items-ends">
+                  <q-chip
+                    dense
+                    square
+                    :color="statusBgColor(item.status)"
+                    :text-color="statusTextColor(item.status)"
+                    :label="statusLabel(item.status)"
+                    class="status-chip"
+                  />
+                  <span class="text-price">{{ formatCurrency(item.total_amount) }}</span>
+                  <span class="text-period">{{ periodLabel(item.period_type) }}</span>
+                </div>
+              </div>
+
+              <div class="row items-center no-wrap q-mt-xs">
+                <span class="type-label" :class="item.schedule_type === 'custom' ? 'type-custom' : 'type-default'">
+                  {{ item.schedule_type === 'custom' ? $t('provider.dashboard.agenda.type_custom') : $t('provider.dashboard.agenda.type_default') }}
+                </span>
+                <q-space />
+                <q-btn
+                  flat
+                  no-caps
+                  color="primary"
+                  size="xs"
+                  class="btn-action"
+                  :label="$t('provider.dashboard.agenda.btn_view_details')"
+                  @click="openDetailsDialog(item)"
+                />
+              </div>
+            </q-card-section>
+          </q-card>
+        </template>
+
+        <div v-else class="text-center text-grey-5 q-py-lg text-body2">
+          {{ $t('provider.dashboard.agenda.empty_upcoming') }}
+        </div>
+      </div>
+
+      <div class="q-mt-lg q-mx-md">
+        <div class="section-title gradient-diarista q-mb-sm">{{ $t('provider.dashboard.agenda.completed_title') }}</div>
+
+        <template v-if="completedSchedules.length > 0">
+          <q-card
+            v-for="item in completedSchedules"
+            :key="item.id"
+            class="calendar-card bg-surface shadow-card q-mb-sm"
+            :flat="false"
+          >
+            <q-card-section class="q-pa-sm">
+              <div class="row no-wrap items-start q-gutter-x-sm">
+                <q-avatar size="44px">
+                  <img :src="item.customer_photo || defaultAvatar">
+                </q-avatar>
+
+                <div class="col columns">
+                  <span class="text-name ellipsis">{{ item.client_name }}</span>
+                  <div class="row items-center no-wrap">
+                    <span class="text-date-bold">{{ formatWeekday(item.date) }}</span>
+                    <span class="text-date-regular">{{ ', ' + formatDayMonth(item.date) }}</span>
+                  </div>
+                  <span class="text-date-regular">
+                    {{ $t('common.from') }}
+                    <span class="text-date-bold">{{ item.start_time?.slice(0, 5) }}</span>
+                    {{ $t('common.to') }}
+                    <span class="text-date-bold">{{ item.end_time?.slice(0, 5) }}</span>
+                  </span>
+                </div>
+
+                <div class="col-auto column items-ends">
+                  <q-chip
+                    dense
+                    square
+                    :color="statusBgColor(item.status)"
+                    :text-color="statusTextColor(item.status)"
+                    :label="statusLabel(item.status)"
+                    class="status-chip"
+                  />
+                  <span class="text-price">{{ formatCurrency(item.total_amount) }}</span>
+                  <span class="text-period">{{ periodLabel(item.period_type) }}</span>
+                </div>
+              </div>
+
+              <div class="row items-center no-wrap q-mt-xs">
+                <span class="type-label" :class="item.schedule_type === 'custom' ? 'type-custom' : 'type-default'">
+                  {{ item.schedule_type === 'custom' ? $t('provider.dashboard.agenda.type_custom') : $t('provider.dashboard.agenda.type_default') }}
+                </span>
+                <q-space />
+                <q-rating
+                  :model-value="item.provider_reviewed ? item.provider_stars : 0"
+                  :max="5"
+                  size="14px"
+                  color="amber"
+                  icon="mdi-star-outline"
+                  icon-selected="mdi-star"
+                  readonly
+                  class="q-mr-sm"
+                />
+                <q-btn
+                  v-if="!item.provider_reviewed"
+                  unelevated
+                  rounded
+                  no-caps
+                  color="secondary"
+                  size="xs"
+                  class="btn-rate"
+                  :label="$t('provider.dashboard.agenda.btn_rate')"
+                  @click="openRatingDialog(item)"
+                />
+              </div>
+            </q-card-section>
+          </q-card>
+        </template>
+
+        <div v-else class="text-center text-grey-5 q-py-lg text-body2">
+          {{ $t('provider.dashboard.agenda.empty_completed') }}
+        </div>
+      </div>
+    </template>
+  </q-page>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue';
+import { useQuasar } from 'quasar';
+import { useI18n } from 'vue-i18n';
+import { getProviderCalendar } from 'src/api/providerCalendar';
+import { formatCurrency } from 'src/helpers/utils';
+import { labelsPeriodTypes } from 'src/helpers/arraysOptions/labelsPeriodTypes.js';
+import NextSchedulesDetailsDialog from 'src/components/dashboard/NextSchedulesDetailsDialog.vue';
+import ScheduleRatingDialog from 'src/components/dashboard/ScheduleRatingDialog.vue';
+
+const $q = useQuasar();
+const { t } = useI18n();
+
+const defaultAvatar = 'https://cdn.quasar.dev/img/avatar.png';
+const loading = ref(true);
+const upcomingSchedules = ref([]);
+const completedSchedules = ref([]);
+
+const parseLocalDate = (dateStr) => {
+  if (!dateStr) return null;
+  const s = String(dateStr);
+  const iso = s.match(/^(\d{4})-(\d{2})-(\d{2})/);
+  if (iso) return new Date(+iso[1], +iso[2] - 1, +iso[3]);
+  const dmy = s.match(/^(\d{2})\/(\d{2})\/(\d{4})/);
+  if (dmy) return new Date(+dmy[3], +dmy[2] - 1, +dmy[1]);
+  return null;
+};
+
+const formatWeekday = (dateStr) => {
+  const d = parseLocalDate(dateStr);
+  if (!d) return '';
+  const w = d.toLocaleDateString('pt-BR', { weekday: 'long' });
+  return w.charAt(0).toUpperCase() + w.slice(1);
+};
+
+const formatDayMonth = (dateStr) => {
+  const d = parseLocalDate(dateStr);
+  if (!d) return '';
+  return d.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
+};
+
+const periodLabel = (periodType) => {
+  const found = labelsPeriodTypes.find(l => l.value == periodType);
+  return found ? t(found.label) : '';
+};
+
+const statusLabel = (status) => {
+  const map = {
+    pending: t('provider.dashboard.agenda.status_pending'),
+    accepted: t('provider.dashboard.agenda.status_accepted'),
+    paid: t('provider.dashboard.agenda.status_paid'),
+    started: t('provider.dashboard.agenda.status_started'),
+    finished: t('provider.dashboard.agenda.status_finished'),
+    cancelled: t('provider.dashboard.agenda.status_cancelled'),
+  };
+  return map[status] ?? status;
+};
+
+const statusBgColor = (status) => {
+  const map = {
+    pending: 'warning-bg',
+    accepted: 'success-bg',
+    paid: 'success-bg',
+    started: 'info-bg',
+    finished: 'neutral-bg',
+    cancelled: 'secondary-bg',
+  };
+  return map[status] ?? 'neutral-bg';
+};
+
+const statusTextColor = (status) => {
+  const map = {
+    pending: 'warning',
+    accepted: 'success',
+    paid: 'success',
+    started: 'info',
+    finished: 'status-finished',
+    cancelled: 'secondary',
+  };
+  return map[status] ?? 'text';
+};
+
+const loadCalendar = async () => {
+  const response = await getProviderCalendar();
+  if (response) {
+    upcomingSchedules.value = response.upcomingSchedules ?? [];
+    completedSchedules.value = response.completedSchedules ?? [];
+  }
+};
+
+const openDetailsDialog = (schedule) => {
+  $q.dialog({
+    component: NextSchedulesDetailsDialog,
+    componentProps: { schedule },
+  }).onOk(async ({ action }) => {
+    if (action === 'cancelled') {
+      await loadCalendar();
+    }
+  });
+};
+
+const openRatingDialog = (schedule) => {
+  $q.dialog({
+    component: ScheduleRatingDialog,
+    componentProps: { schedule },
+  }).onOk(() => {
+    loadCalendar();
+  });
+};
+
+
+onMounted(async () => {
+  await loadCalendar();
+  loading.value = false;
+});
+</script>
+
+<style scoped lang="scss">
+.calendar-header {
+  padding-top: calc(env(safe-area-inset-top) + 12px);
+  padding-bottom: 12px;
+  box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.08);
+}
+
+.calendar-card {
+  border-radius: 12px;
+  width: 100%;
+}
+
+.type-label {
+  font-size: 10px;
+  font-weight: 600;
+  line-height: 1.2;
+}
+
+.type-default {
+  color: #8B5CF6;
+}
+
+.type-custom {
+  color: #EC4899;
+}
+
+.text-name {
+  font-size: 13px;
+  font-weight: 700;
+  color: #3a3a4a;
+  max-width: 130px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.text-date-bold {
+  font-family: 'Inter', sans-serif;
+  font-size: 11px;
+  font-weight: 700;
+  color: #3a3a4a;
+}
+
+.text-date-regular {
+  font-family: 'Inter', sans-serif;
+  font-size: 11px;
+  font-weight: 400;
+  color: #666;
+}
+
+.text-price {
+  font-size: 13px;
+  font-weight: 700;
+  color: #3a3a4a;
+  white-space: nowrap;
+}
+
+.text-period {
+  font-size: 10px;
+  color: #888;
+  text-align: right;
+  white-space: nowrap;
+}
+
+.status-chip {
+  font-size: 11px !important;
+  font-weight: 700;
+  height: auto;
+  padding: 2px 2px;
+}
+
+.btn-action {
+  font-size: 11px;
+  font-weight: 700;
+}
+
+.btn-rate {
+  font-size: 11px;
+  font-weight: 700;
+  padding: 3px 10px;
+}
+
+</style>

+ 2 - 9
src/pages/dashboard/DashboardPage.vue

@@ -43,10 +43,6 @@ import { dadosDashboard } from 'src/api/dashboard';
 import { updateScheduleStatus } from 'src/api/schedule';
 import { useRouter } from 'vue-router';
 
-
-
-
-
 const router = useRouter();
 
 const headerBar = ref({});
@@ -57,7 +53,7 @@ const todayServices = ref([]);
 const nextSchedules = ref([]);
 const opportunities = ref([]);
 
-const showSuccessModal = ref(router.currentRoute.value.fullPath.includes('showSuccessModal') || 'true');
+const showSuccessModal = ref(router.currentRoute.value.fullPath.includes('showSuccessModal') || false);
 
 const $q = useQuasar();
 
@@ -75,7 +71,7 @@ const loadDashboard = async () => {
     opportunities.value = response.opportunities ?? [];
   }
 
-  if( showSuccessModal.value ) {
+  if( showSuccessModal.value == true) {
     $q.dialog({
        component: OpportunityDialog   
        })
@@ -83,11 +79,8 @@ const loadDashboard = async () => {
     showSuccessModal.value = false;
     router.replace({ path: router.currentRoute.value.path, query: {} });
   }
-
-  
 }
 
-
 const handleScheduleAction = async (id, status) => {
   try {
     await updateScheduleStatus(id, status);

+ 0 - 36
src/router/routes.js

@@ -29,42 +29,6 @@ const routes = [
         },
       },
 
-      {
-        path: "pagamentos",
-        name: "PagamentosPage",
-        component: () => import("src/pages/search/PagamentosPage.vue"),
-      },
-      {
-        path: "agenda",
-        name: "AgendaPage",
-        component: () => import("src/pages/agenda/AgendaPage.vue"),
-      },
-      {
-        path: "perfil",
-        name: "ProfilePage",
-        component: () => import("src/pages/profile/ProfilePage.vue"),
-      },
-      {
-        path: "opportunities",
-        name: "OpportunitiesPage",
-        component: () =>
-          import("src/pages/opportunities/OpportunitiesPage.vue"),
-        meta: {
-          title: "Oportunidades",
-          requireAuth: true,
-          breadcrumbs: [
-            {
-              name: "DashboardPage",
-              title: "ui.navigation.dashboard",
-            },
-            {
-              name: "OpportunitiesPage",
-              title: "Oportunidades",
-            },
-          ],
-        },
-      },
-
       ...sub_routes,
     ],
   },

+ 36 - 0
src/router/routes/navbar.route.js

@@ -0,0 +1,36 @@
+export default [
+  {
+    path: "pagamentos",
+    name: "PagamentosPage",
+    component: () => import("src/pages/search/PagamentosPage.vue"),
+  },
+  {
+    path: "agenda",
+    name: "CalendarPage",
+    component: () => import("src/pages/calendar/CalendarPage.vue"),
+  },
+  {
+    path: "perfil",
+    name: "ProfilePage",
+    component: () => import("src/pages/profile/ProfilePage.vue"),
+  },
+  {
+    path: "opportunities",
+    name: "OpportunitiesPage",
+    component: () => import("src/pages/opportunities/OpportunitiesPage.vue"),
+    meta: {
+      title: "Oportunidades",
+      requireAuth: true,
+      breadcrumbs: [
+        {
+          name: "DashboardPage",
+          title: "ui.navigation.dashboard",
+        },
+        {
+          name: "OpportunitiesPage",
+          title: "Oportunidades",
+        },
+      ],
+    },
+  },
+];