|
|
@@ -1,332 +0,0 @@
|
|
|
-<template>
|
|
|
- <q-page class="bg-page">
|
|
|
-
|
|
|
- <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-header">
|
|
|
- <q-btn flat round dense icon="mdi-chevron-left" color="primary" @click="router.back()" />
|
|
|
- <div class="col text-center text-subtitle1 text-weight-bold text-primary">
|
|
|
- {{ $t('scheduling_page.title') }}
|
|
|
- </div>
|
|
|
- <div style="width: 36px" />
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="col overflow-auto q-pb-xl">
|
|
|
-
|
|
|
- <div class="q-px-md q-pt-md">
|
|
|
- <q-card class="card-border bg-white" flat>
|
|
|
- <q-card-section class="q-pa-md">
|
|
|
- <div class="text-subtitle2 text-weight-bold text-primary q-mb-sm">
|
|
|
- {{ $t('scheduling_page.about_provider') }}
|
|
|
- </div>
|
|
|
- <div class="row items-center no-wrap q-gutter-x-md">
|
|
|
- <q-avatar :style="avatarStyle" size="52px" class="text-weight-bold text-body1">
|
|
|
- {{ provider?.provider_name?.slice(0, 1).toUpperCase() ?? '—' }}
|
|
|
- </q-avatar>
|
|
|
- <div class="col">
|
|
|
- <div class="text-weight-bold text-text">{{ provider?.provider_name ?? '—' }}</div>
|
|
|
- <div class="text-caption text-grey-6">{{ provider?.city ?? provider?.district ?? '—' }}</div>
|
|
|
- <div class="row items-center q-gutter-x-sm q-mt-xs">
|
|
|
- <div class="row items-center">
|
|
|
- <q-icon name="mdi-star" color="warning" size="14px" />
|
|
|
- <span class="text-caption text-weight-medium q-ml-xs">
|
|
|
- {{ provider?.average_rating != null ? Number(provider.average_rating).toFixed(1) : '' }}
|
|
|
- <span class="text-grey-5">{{ '(' + (provider?.total_reviews ?? 0) + ')' }}</span>
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- <div class="row items-center">
|
|
|
- <q-icon name="mdi-broom" color="secondary" size="14px" />
|
|
|
- <span class="text-caption q-ml-xs">{{ provider?.total_services ?? 0 }}</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="column items-center q-gutter-y-xs">
|
|
|
- <q-btn flat round dense icon="mdi-heart-outline" color="pink-4" size="sm" />
|
|
|
- <q-btn flat round dense icon="mdi-information-outline" color="grey-5" size="sm" />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </q-card-section>
|
|
|
- </q-card>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="q-px-md q-pt-md">
|
|
|
- <div class="text-h6 text-weight-bold gradient-diarista q-mb-xs">
|
|
|
- {{ $t('scheduling_page.schedule_service') }}
|
|
|
- </div>
|
|
|
-
|
|
|
- <div v-if="loadingAvailability" class="row items-center justify-center q-py-lg">
|
|
|
- <q-spinner-dots color="primary" size="36px" />
|
|
|
- </div>
|
|
|
-
|
|
|
- <div v-else class="shadow-card q-mb-md" style="border-radius: 20px; overflow: hidden;">
|
|
|
- <q-date
|
|
|
- v-model="selectedDate"
|
|
|
- square
|
|
|
- class="full-width calendar-custom text-text"
|
|
|
- :first-day-of-week="0"
|
|
|
- :events="availableDatesForCalendar"
|
|
|
- event-color="positive"
|
|
|
- :options="dateOptions"
|
|
|
- minimal
|
|
|
- />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="q-px-md q-pt-sm">
|
|
|
- <div class="row items-center justify-between q-mb-sm">
|
|
|
- <div class="text-h6 text-weight-bold gradient-diarista">
|
|
|
- {{ $t('scheduling_page.reviews_title') }}
|
|
|
- </div>
|
|
|
- <span class="text-caption text-primary cursor-pointer">
|
|
|
- {{ $t('scheduling_page.see_all') }}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div v-if="loadingReviews" class="row items-center justify-center q-py-lg">
|
|
|
- <q-spinner-dots color="primary" size="36px" />
|
|
|
- </div>
|
|
|
-
|
|
|
- <div v-else-if="reviews.length === 0" class="text-center text-grey-6 text-body2 q-py-md">
|
|
|
- {{ $t('scheduling_page.no_reviews') }}
|
|
|
- </div>
|
|
|
-
|
|
|
- <div v-else class="row no-wrap scroll-reviews q-pb-sm" style="overflow-x: auto;">
|
|
|
- <q-card
|
|
|
- v-for="review in reviews"
|
|
|
- :key="review.id"
|
|
|
- class="review-card card-border bg-white q-mr-sm flex-shrink-0"
|
|
|
- flat
|
|
|
- style="min-width: 220px; max-width: 240px;"
|
|
|
- >
|
|
|
- <q-card-section class="q-pa-sm">
|
|
|
- <div class="row items-center no-wrap q-gutter-x-sm q-mb-xs">
|
|
|
- <q-avatar size="32px" :style="clientAvatarStyle(review)" class="text-weight-bold text-caption">
|
|
|
- {{ review.schedule?.client?.name?.slice(0, 1).toUpperCase() ?? '?' }}
|
|
|
- </q-avatar>
|
|
|
- <div class="col text-weight-medium text-text text-caption ellipsis">
|
|
|
- {{ review.schedule?.client?.name ?? $t('scheduling_page.unknown_client') }}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="row items-center q-mb-xs">
|
|
|
- <q-icon
|
|
|
- v-for="s in 5"
|
|
|
- :key="s"
|
|
|
- :name="s <= review.stars ? 'mdi-star' : 'mdi-star-outline'"
|
|
|
- color="warning"
|
|
|
- size="14px"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div class="text-caption text-text review-comment ellipsis-2-lines">
|
|
|
- {{ review.comment ?? '' }}
|
|
|
- </div>
|
|
|
- </q-card-section>
|
|
|
- </q-card>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- </div>
|
|
|
- </q-page>
|
|
|
-</template>
|
|
|
-
|
|
|
-<script setup>
|
|
|
-import { ref, computed, onMounted } from 'vue';
|
|
|
-import { useRouter } from 'vue-router';
|
|
|
-import { date } from 'quasar';
|
|
|
-import { getProviderWorkingDays, getProviderBlockedDays } from 'src/api/providerAvailability';
|
|
|
-import { getProviderReceivedReviews } from 'src/api/review';
|
|
|
-
|
|
|
-const router = useRouter();
|
|
|
-
|
|
|
-const provider = ref(history.state?.provider ?? null);
|
|
|
-
|
|
|
-const selectedDate = ref(null);
|
|
|
-const workingDays = ref([]);
|
|
|
-const blockedDays = ref([]);
|
|
|
-const loadingAvailability = ref(true);
|
|
|
-
|
|
|
-const reviews = ref([]);
|
|
|
-const loadingReviews = ref(true);
|
|
|
-
|
|
|
-const avatarColors = [
|
|
|
- { background: '#ffd5df', color: '#932e57' },
|
|
|
- { background: '#d7e8ff', color: '#2158a8' },
|
|
|
- { background: '#dfd', color: '#2a7a3b' },
|
|
|
- { background: '#ffe5cc', color: '#8a4500' },
|
|
|
-];
|
|
|
-
|
|
|
-const avatarStyle = computed(() => {
|
|
|
- const idx = (provider.value?.provider_id ?? 0) % avatarColors.length;
|
|
|
- return avatarColors[idx];
|
|
|
-});
|
|
|
-
|
|
|
-const clientAvatarStyle = (review) => {
|
|
|
- const idx = (review.id ?? 0) % avatarColors.length;
|
|
|
- return avatarColors[idx];
|
|
|
-};
|
|
|
-
|
|
|
-const availableWeekDays = computed(() =>
|
|
|
- [...new Set(workingDays.value.map((wd) => wd.day))]
|
|
|
-);
|
|
|
-
|
|
|
-const blockedDateSet = computed(() =>
|
|
|
- new Set(blockedDays.value.map((bd) => bd.date))
|
|
|
-);
|
|
|
-
|
|
|
-const availableDatesForCalendar = computed(() => {
|
|
|
- const result = [];
|
|
|
- const today = new Date();
|
|
|
- const end = date.addToDate(today, { months: 3 });
|
|
|
- let current = new Date(today);
|
|
|
- while (current <= end) {
|
|
|
- const dayOfWeek = current.getDay();
|
|
|
- const dateStr = date.formatDate(current, 'YYYY-MM-DD');
|
|
|
- const isWorkingDay = availableWeekDays.value.includes(dayOfWeek);
|
|
|
- const isBlocked = blockedDateSet.value.has(dateStr);
|
|
|
- if (isWorkingDay && !isBlocked) {
|
|
|
- result.push(date.formatDate(current, 'YYYY/MM/DD'));
|
|
|
- }
|
|
|
- current = date.addToDate(current, { days: 1 });
|
|
|
- }
|
|
|
- return result;
|
|
|
-});
|
|
|
-
|
|
|
-const dateOptions = (d) => {
|
|
|
- const today = date.formatDate(new Date(), 'YYYY/MM/DD');
|
|
|
- if (d < today) return false;
|
|
|
- const raw = d.replace(/\//g, '-');
|
|
|
- const dayOfWeek = new Date(d.replace(/\//g, '-')).getDay();
|
|
|
- const isWorkingDay = availableWeekDays.value.includes(dayOfWeek);
|
|
|
- const isBlocked = blockedDateSet.value.has(raw);
|
|
|
- return isWorkingDay && !isBlocked;
|
|
|
-};
|
|
|
-
|
|
|
-const loadAvailability = async () => {
|
|
|
- if (!provider.value?.provider_id) return;
|
|
|
- loadingAvailability.value = true;
|
|
|
- try {
|
|
|
- const [wd, bd] = await Promise.all([
|
|
|
- getProviderWorkingDays(provider.value.provider_id),
|
|
|
- getProviderBlockedDays(provider.value.provider_id),
|
|
|
- ]);
|
|
|
- workingDays.value = wd ?? [];
|
|
|
- blockedDays.value = bd ?? [];
|
|
|
- } catch {
|
|
|
- workingDays.value = [];
|
|
|
- blockedDays.value = [];
|
|
|
- } finally {
|
|
|
- loadingAvailability.value = false;
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
-const loadReviews = async () => {
|
|
|
- if (!provider.value?.provider_id) return;
|
|
|
- loadingReviews.value = true;
|
|
|
- try {
|
|
|
- const all = await getProviderReceivedReviews(provider.value.provider_id);
|
|
|
- reviews.value = (all ?? []).slice(0, 10);
|
|
|
- } catch {
|
|
|
- reviews.value = [];
|
|
|
- } finally {
|
|
|
- loadingReviews.value = false;
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
-onMounted(async () => {
|
|
|
- await Promise.all([loadAvailability(), loadReviews()]);
|
|
|
-});
|
|
|
-</script>
|
|
|
-
|
|
|
-<style scoped lang="scss">
|
|
|
-.shadow-header {
|
|
|
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
|
|
-}
|
|
|
-
|
|
|
-.review-card {
|
|
|
- border-radius: 12px;
|
|
|
-}
|
|
|
-
|
|
|
-.review-comment {
|
|
|
- display: -webkit-box;
|
|
|
- -webkit-box-orient: vertical;
|
|
|
- overflow: hidden;
|
|
|
-}
|
|
|
-
|
|
|
-.calendar-custom {
|
|
|
- border-radius: 20px;
|
|
|
- background-color: white !important;
|
|
|
-
|
|
|
- :deep(.q-date__main) {
|
|
|
- background-color: white !important;
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.q-date__content) {
|
|
|
- background-color: white !important;
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.q-date__calendar) {
|
|
|
- background-color: white !important;
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.q-date__calendar-item--out) {
|
|
|
- .q-btn__content {
|
|
|
- color: #CBD5E1 !important;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.q-date__calendar-days .q-btn__content) {
|
|
|
- font-family: 'Inter', sans-serif;
|
|
|
- font-weight: 500;
|
|
|
- color: #1E293B;
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.q-date__calendar-weekdays > div) {
|
|
|
- color: #6366F1;
|
|
|
- font-weight: 700;
|
|
|
- opacity: 0.8;
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.q-date__navigation) {
|
|
|
- .q-btn {
|
|
|
- color: #1E293B !important;
|
|
|
- }
|
|
|
- .q-btn__content {
|
|
|
- color: #1E293B !important;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.q-date__nav-btn-month),
|
|
|
- :deep(.q-date__nav-btn-year) {
|
|
|
- color: #6366F1 !important;
|
|
|
- font-weight: 700;
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.q-date__event) {
|
|
|
- bottom: 4px;
|
|
|
- height: 6px;
|
|
|
- width: 6px;
|
|
|
- border-radius: 50%;
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.q-date__today) {
|
|
|
- .q-btn__content {
|
|
|
- color: #7c4dff !important;
|
|
|
- background: #7c4dff15;
|
|
|
- border-radius: 50%;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.q-date__selected) {
|
|
|
- .q-btn__content {
|
|
|
- background: #6366F1 !important;
|
|
|
- color: white !important;
|
|
|
- border-radius: 50%;
|
|
|
- box-shadow: 0 4px 10px rgba(99, 102, 241, 0.4);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.q-date__view--months),
|
|
|
- :deep(.q-date__view--years) {
|
|
|
- .q-btn {
|
|
|
- color: #1E293B !important;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-</style>
|