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