|
|
@@ -6,7 +6,9 @@
|
|
|
transition-show="slide-up"
|
|
|
>
|
|
|
<div class="dialog-root">
|
|
|
- <div class="dialog-header row items-center q-px-md q-pt-md q-pb-sm bg-white">
|
|
|
+ <div
|
|
|
+ class="dialog-header row items-center q-px-md q-pt-md q-pb-sm bg-white"
|
|
|
+ >
|
|
|
<q-btn
|
|
|
v-close-popup
|
|
|
color="primary"
|
|
|
@@ -16,8 +18,10 @@
|
|
|
round
|
|
|
/>
|
|
|
|
|
|
- <div class="col text-center font16 fontbold text-primary gradient-diarista q-mb-xs">
|
|
|
- {{ $t('scheduling_page.title') }}
|
|
|
+ <div
|
|
|
+ class="col text-center font16 fontbold text-primary gradient-diarista q-mb-xs"
|
|
|
+ >
|
|
|
+ {{ $t("scheduling_page.title") }}
|
|
|
</div>
|
|
|
|
|
|
<div style="width: 36px"></div>
|
|
|
@@ -26,48 +30,74 @@
|
|
|
<div class="dialog-body">
|
|
|
<div class="q-px-md q-pt-md">
|
|
|
<div class="font16 fontbold gradient-diarista q-mb-xs">
|
|
|
- {{ $t('scheduling_page.about_provider') }}
|
|
|
+ {{ $t("scheduling_page.about_provider") }}
|
|
|
</div>
|
|
|
|
|
|
- <q-card :flat="false" class="card-border shadow-card bg-surface text-text">
|
|
|
+ <q-card
|
|
|
+ class="card-border shadow-card bg-surface text-text"
|
|
|
+ :flat="false"
|
|
|
+ >
|
|
|
<q-card-section class="q-pa-md">
|
|
|
<div class="row items-center no-wrap q-gutter-x-md">
|
|
|
<div class="col-2">
|
|
|
- <q-avatar :style="avatarColors[provider.provider_id % avatarColors.length]">
|
|
|
+ <q-avatar
|
|
|
+ :style="
|
|
|
+ avatarColors[provider.provider_id % avatarColors.length]
|
|
|
+ "
|
|
|
+ >
|
|
|
<img
|
|
|
- v-if="provider.provider_photo"
|
|
|
- :src="provider.provider_photo"
|
|
|
- style="object-fit:cover;border-radius:50%;"
|
|
|
+ v-if="getProfileMediaUrl(provider)"
|
|
|
+ style="object-fit: cover; border-radius: 50%"
|
|
|
+ :src="getProfileMediaUrl(provider)"
|
|
|
/>
|
|
|
|
|
|
<span v-else>
|
|
|
- {{ getFirstName(provider.provider_name)?.slice(0, 2).toUpperCase() ?? '??' }}
|
|
|
+ {{
|
|
|
+ getFirstName(provider.provider_name)
|
|
|
+ ?.slice(0, 2)
|
|
|
+ .toUpperCase() ?? "??"
|
|
|
+ }}
|
|
|
</span>
|
|
|
</q-avatar>
|
|
|
</div>
|
|
|
|
|
|
<div class="col-3 column">
|
|
|
<div class="font12 fontbold text-text">
|
|
|
- {{ getFirstName(provider?.provider_name) ?? '—' }}
|
|
|
+ {{ getFirstName(provider?.provider_name) ?? "—" }}
|
|
|
</div>
|
|
|
|
|
|
<div class="font10 fontmedium text-grey-7">
|
|
|
- {{ provider?.district ?? '—' }}
|
|
|
+ {{ provider?.district ?? "—" }}
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="col-3">
|
|
|
<div class="row items-center q-gutter-x-md q-mt-xs">
|
|
|
<div class="row items-center">
|
|
|
- <q-icon color="warning" name="mdi-star" size="12px" />
|
|
|
+ <q-icon
|
|
|
+ color="warning"
|
|
|
+ name="mdi-star"
|
|
|
+ size="12px"
|
|
|
+ />
|
|
|
|
|
|
<span class="font9 fontmedium q-ml-xs">
|
|
|
- {{ (provider?.average_rating != null ? Number(provider.average_rating).toFixed(1) : '') + ' (' + (provider?.total_reviews ?? 0) + ')' }}
|
|
|
+ {{
|
|
|
+ (provider?.average_rating != null
|
|
|
+ ? Number(provider.average_rating).toFixed(1)
|
|
|
+ : "") +
|
|
|
+ " (" +
|
|
|
+ (provider?.total_reviews ?? 0) +
|
|
|
+ ")"
|
|
|
+ }}
|
|
|
</span>
|
|
|
</div>
|
|
|
|
|
|
<div class="row items-center">
|
|
|
- <q-icon color="secondary" name="mdi-broom" size="14px" />
|
|
|
+ <q-icon
|
|
|
+ color="secondary"
|
|
|
+ name="mdi-broom"
|
|
|
+ size="14px"
|
|
|
+ />
|
|
|
|
|
|
<span class="font9 fontmedium q-ml-xs">
|
|
|
{{ provider?.total_services ?? 0 }}
|
|
|
@@ -81,10 +111,10 @@
|
|
|
color="pink-4"
|
|
|
dense
|
|
|
flat
|
|
|
- :icon="isFavorite ? 'mdi-heart' : 'mdi-heart-outline'"
|
|
|
- :loading="favoriteLoading"
|
|
|
round
|
|
|
size="sm"
|
|
|
+ :icon="isFavorite ? 'mdi-heart' : 'mdi-heart-outline'"
|
|
|
+ :loading="favoriteLoading"
|
|
|
@click="toggleFavorite"
|
|
|
/>
|
|
|
|
|
|
@@ -104,14 +134,23 @@
|
|
|
|
|
|
<div class="q-px-md q-pt-lg">
|
|
|
<div class="font16 fontbold gradient-diarista q-mb-xs">
|
|
|
- {{ $t('scheduling_page.schedule_service') }}
|
|
|
+ {{ $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
|
|
|
+ v-if="loadingAvailability"
|
|
|
+ class="row items-center justify-center q-py-lg"
|
|
|
+ >
|
|
|
+ <q-spinner-dots
|
|
|
+ color="primary"
|
|
|
+ size="36px"
|
|
|
+ />
|
|
|
</div>
|
|
|
|
|
|
- <div v-else class="calendar-wrapper shadow-card q-mb-md">
|
|
|
+ <div
|
|
|
+ v-else
|
|
|
+ class="calendar-wrapper shadow-card q-mb-md"
|
|
|
+ >
|
|
|
<q-date
|
|
|
v-model="selectedDate"
|
|
|
class="full-width"
|
|
|
@@ -127,11 +166,11 @@
|
|
|
<div class="q-px-md q-pt-sm q-pb-xl">
|
|
|
<div class="row items-center justify-between q-mb-sm">
|
|
|
<div class="font16 fontbold gradient-diarista">
|
|
|
- {{ $t('scheduling_page.reviews_title') }}
|
|
|
+ {{ $t("scheduling_page.reviews_title") }}
|
|
|
</div>
|
|
|
|
|
|
- <span class=" text-text cursor-pointer">
|
|
|
- {{ $t('scheduling_page.see_all') }}
|
|
|
+ <span class="text-text cursor-pointer">
|
|
|
+ {{ $t("scheduling_page.see_all") }}
|
|
|
|
|
|
<q-icon
|
|
|
class="text-text"
|
|
|
@@ -140,18 +179,27 @@
|
|
|
</span>
|
|
|
</div>
|
|
|
|
|
|
- <div v-if="loadingReviews" class="row items-center justify-center q-py-md">
|
|
|
+ <div
|
|
|
+ v-if="loadingReviews"
|
|
|
+ class="row items-center justify-center q-py-md"
|
|
|
+ >
|
|
|
<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
|
|
|
+ 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="reviews-scroll">
|
|
|
+ <div
|
|
|
+ v-else
|
|
|
+ class="reviews-scroll"
|
|
|
+ >
|
|
|
<q-card
|
|
|
v-for="review in reviews"
|
|
|
:key="review.id"
|
|
|
@@ -160,20 +208,29 @@
|
|
|
>
|
|
|
<q-card-section class="q-pa-sm">
|
|
|
<div class="row items-center no-wrap q-gutter-x-sm q-mb-xs">
|
|
|
- <q-avatar :style="avatarColors[review.schedule?.client?.id % avatarColors.length]">
|
|
|
+ <q-avatar
|
|
|
+ :style="
|
|
|
+ avatarColors[
|
|
|
+ review.schedule?.client?.id % avatarColors.length
|
|
|
+ ]
|
|
|
+ "
|
|
|
+ >
|
|
|
<img
|
|
|
v-if="review.schedule?.client?.profile_photo"
|
|
|
+ style="object-fit: cover; border-radius: 50%"
|
|
|
:src="review.schedule?.client?.profile_photo"
|
|
|
- style="object-fit:cover;border-radius:50%;"
|
|
|
/>
|
|
|
|
|
|
<span v-else>
|
|
|
- {{review.schedule?.client?.name?.slice(0,1) ?? '—' }}
|
|
|
+ {{ review.schedule?.client?.name?.slice(0, 1) ?? "—" }}
|
|
|
</span>
|
|
|
</q-avatar>
|
|
|
|
|
|
<div class="col-3 text-text font10 fontbold">
|
|
|
- {{ review.schedule?.client?.name ?? $t('scheduling_page.unknown_client') }}
|
|
|
+ {{
|
|
|
+ review.schedule?.client?.name ??
|
|
|
+ $t("scheduling_page.unknown_client")
|
|
|
+ }}
|
|
|
</div>
|
|
|
|
|
|
<div class="row items-center q-mb-xs q-my-auto">
|
|
|
@@ -188,7 +245,7 @@
|
|
|
</div>
|
|
|
|
|
|
<div class="font9 fontregular text-text review-comment">
|
|
|
- {{ review.comment ?? '--------' }}
|
|
|
+ {{ review.comment ?? "--------" }}
|
|
|
</div>
|
|
|
</q-card-section>
|
|
|
</q-card>
|
|
|
@@ -200,139 +257,157 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { computed, onMounted, ref } from 'vue';
|
|
|
-import { date } from 'quasar';
|
|
|
-import { getClientProviderBlocks } from 'src/api/schedule';
|
|
|
-import { getProviderBlockedDays, getProviderWorkingDays } from 'src/api/providerAvailability';
|
|
|
-import { getProviderReceivedReviews } from 'src/api/review';
|
|
|
+import { avatarColors } from "src/helpers/avatarColors";
|
|
|
+import { computed, onMounted, ref } from "vue";
|
|
|
+
|
|
|
import {
|
|
|
createClientFavoriteProvider,
|
|
|
deleteClientFavoriteProvider,
|
|
|
getClientFavoriteProviders,
|
|
|
-} from 'src/api/clientFavoriteProvider';
|
|
|
-import { useDialogPluginComponent, useQuasar } from 'quasar';
|
|
|
-import { useI18n } from 'vue-i18n';
|
|
|
-import { useScheduleCartStore } from 'src/stores/scheduleCart';
|
|
|
-import { userStore } from 'src/stores/user';
|
|
|
-import {getFirstName} from 'src/helpers/utils';
|
|
|
-import OrderSummaryDialog from './OrderSummaryDialog.vue';
|
|
|
-import ServiceSelectionSheet from './ServiceSelectionSheet.vue';
|
|
|
-import ServiceTimeSelectionDialog from './ServiceTimeSelectionDialog.vue';
|
|
|
+} from "src/api/clientFavoriteProvider";
|
|
|
|
|
|
-const props = defineProps({
|
|
|
- provider: { required: true, type: Object },
|
|
|
-});
|
|
|
+import { date, useDialogPluginComponent, useQuasar } from "quasar";
|
|
|
+import { getClientProviderBlocks } from "src/api/schedule";
|
|
|
+import { getFirstName } from "src/helpers/utils";
|
|
|
+import { getProfileMediaUrl } from "src/helpers/profileMedia";
|
|
|
+
|
|
|
+import {
|
|
|
+ getProviderBlockedDays,
|
|
|
+ getProviderWorkingDays,
|
|
|
+} from "src/api/providerAvailability";
|
|
|
+
|
|
|
+import { getProviderReceivedReviews } from "src/api/review";
|
|
|
+import { useI18n } from "vue-i18n";
|
|
|
+import { userStore } from "src/stores/user";
|
|
|
+import { useScheduleCartStore } from "src/stores/orders";
|
|
|
+
|
|
|
+import OrderSummaryDialog from "./OrderSummaryDialog.vue";
|
|
|
+import ServiceSelectionSheet from "./ServiceSelectionSheet.vue";
|
|
|
+import ServiceTimeSelectionDialog from "./ServiceTimeSelectionDialog.vue";
|
|
|
|
|
|
defineEmits([...useDialogPluginComponent.emits]);
|
|
|
|
|
|
-const { dialogRef } = useDialogPluginComponent();
|
|
|
+const props = defineProps({
|
|
|
+ provider: { required: true, type: Object },
|
|
|
+});
|
|
|
|
|
|
-const $q = useQuasar();
|
|
|
const { t } = useI18n();
|
|
|
-
|
|
|
-const store = userStore();
|
|
|
+const $q = useQuasar();
|
|
|
+const { dialogRef } = useDialogPluginComponent();
|
|
|
|
|
|
const scheduleCart = useScheduleCartStore();
|
|
|
+const store = userStore();
|
|
|
|
|
|
-const selectedDate = ref(null);
|
|
|
-const workingDays = ref([]);
|
|
|
-const blockedDays = ref([]);
|
|
|
-const loadingAvailability = ref(true);
|
|
|
-const reviews = ref([]);
|
|
|
-const loadingReviews = ref(true);
|
|
|
-const isFavorite = ref(false);
|
|
|
-const favoriteId = ref(null);
|
|
|
-const favoriteLoading = ref(false);
|
|
|
-
|
|
|
-const avatarColors = [
|
|
|
- { background: '#ffd5df', color: '#932e57' },
|
|
|
- { background: '#d7e8ff', color: '#2158a8' },
|
|
|
- { background: '#dfd', color: '#2a7a3b' },
|
|
|
- { background: '#ffe5cc', color: '#8a4500' },
|
|
|
-];
|
|
|
-
|
|
|
+const blockedDays = ref([]);
|
|
|
const bookings = ref([]);
|
|
|
-
|
|
|
-const providerClientBlocks = ref({ existing_schedules: [], fully_blocked_weeks: [] });
|
|
|
-
|
|
|
-const availableWeekDays = computed(() =>
|
|
|
- [...new Set(workingDays.value.map((wd) => wd.day))]
|
|
|
-);
|
|
|
-
|
|
|
-const blockedDateSet = computed(() =>
|
|
|
- new Set(
|
|
|
- blockedDays.value
|
|
|
- .filter((bd) => bd.period === 'all')
|
|
|
- .map((bd) => bd.date)
|
|
|
- )
|
|
|
+const favoriteId = ref(null);
|
|
|
+const favoriteLoading = ref(false);
|
|
|
+const isFavorite = ref(false);
|
|
|
+const loadingAvailability = ref(true);
|
|
|
+const loadingReviews = ref(true);
|
|
|
+const providerClientBlocks = ref({
|
|
|
+ existing_schedules: [],
|
|
|
+ fully_blocked_weeks: [],
|
|
|
+});
|
|
|
+const reviews = ref([]);
|
|
|
+const selectedDate = ref(null);
|
|
|
+const workingDays = ref([]);
|
|
|
+
|
|
|
+const availableWeekDays = computed(() => [
|
|
|
+ ...new Set(workingDays.value.map((wd) => wd.day)),
|
|
|
+]);
|
|
|
+
|
|
|
+const blockedDateSet = computed(
|
|
|
+ () =>
|
|
|
+ new Set(
|
|
|
+ blockedDays.value
|
|
|
+ .filter((bd) => bd.period === "all")
|
|
|
+ .map((bd) => bd.date),
|
|
|
+ ),
|
|
|
);
|
|
|
|
|
|
-const blockedWeekStartSet = computed(() =>
|
|
|
- new Set(providerClientBlocks.value.fully_blocked_weeks ?? [])
|
|
|
+const blockedWeekStartSet = computed(
|
|
|
+ () => new Set(providerClientBlocks.value.fully_blocked_weeks ?? []),
|
|
|
);
|
|
|
|
|
|
-const dateOptions = (d) => {
|
|
|
- const today = date.formatDate(new Date(), 'YYYY/MM/DD');
|
|
|
-
|
|
|
- if (d < today) return false;
|
|
|
-
|
|
|
- const raw = normalizeDate(d);
|
|
|
- const parsed = new Date(`${raw}T12:00:00`);
|
|
|
- const dayOfWeek = parsed.getDay();
|
|
|
+const normalizeDate = (dateStr) => (dateStr ?? "").replace(/\//g, "-");
|
|
|
|
|
|
- const isWorkingDay = availableWeekDays.value.includes(dayOfWeek);
|
|
|
- const isBlocked = blockedDateSet.value.has(raw);
|
|
|
- const isWeekBlocked = blockedWeekStartSet.value.has(getWeekStart(raw));
|
|
|
+const getWeekStart = (dateStr) => {
|
|
|
+ const normalizedDate = normalizeDate(dateStr);
|
|
|
|
|
|
- if (!isWorkingDay || isBlocked || isWeekBlocked) return false;
|
|
|
+ const d = new Date(`${normalizedDate}T12:00:00`);
|
|
|
|
|
|
- if (wouldExceedWeekLimit(raw)) return false;
|
|
|
+ d.setDate(d.getDate() - d.getDay());
|
|
|
|
|
|
- return true;
|
|
|
+ return d.toISOString().split("T")[0];
|
|
|
};
|
|
|
|
|
|
-const formatHour = (h) => `${String(h).padStart(2, '0')}:00`;
|
|
|
-
|
|
|
const getCartWeekCount = (selectedDate) => {
|
|
|
const weekStart = getWeekStart(selectedDate);
|
|
|
|
|
|
- return scheduleCart.items.filter((item) =>
|
|
|
- Number(item.provider_id) === Number(props.provider.provider_id) &&
|
|
|
- getWeekStart(item.date) === weekStart
|
|
|
+ return scheduleCart.items.filter(
|
|
|
+ (item) =>
|
|
|
+ Number(item.provider_id) === Number(props.provider.provider_id) &&
|
|
|
+ getWeekStart(item.date) === weekStart,
|
|
|
).length;
|
|
|
};
|
|
|
|
|
|
const getLocalWeekCount = (selectedDate) => {
|
|
|
const weekStart = getWeekStart(selectedDate);
|
|
|
|
|
|
- return bookings.value.filter((booking) => getWeekStart(booking.date) === weekStart).length;
|
|
|
+ return bookings.value.filter(
|
|
|
+ (booking) => getWeekStart(booking.date) === weekStart,
|
|
|
+ ).length;
|
|
|
};
|
|
|
|
|
|
const getServerWeekCount = (selectedDate) => {
|
|
|
const weekStart = getWeekStart(selectedDate);
|
|
|
|
|
|
return (providerClientBlocks.value.existing_schedules ?? []).filter(
|
|
|
- (schedule) => getWeekStart(schedule.date) === weekStart
|
|
|
+ (schedule) => getWeekStart(schedule.date) === weekStart,
|
|
|
).length;
|
|
|
};
|
|
|
|
|
|
-const getWeekStart = (dateStr) => {
|
|
|
- const normalizedDate = normalizeDate(dateStr);
|
|
|
+const wouldExceedWeekLimit = (selectedDate) => {
|
|
|
+ const cartCount = getCartWeekCount(selectedDate);
|
|
|
+ const localCount = getLocalWeekCount(selectedDate);
|
|
|
+ const serverCount = getServerWeekCount(selectedDate);
|
|
|
|
|
|
- const d = new Date(`${normalizedDate}T12:00:00`);
|
|
|
+ return serverCount + localCount + cartCount >= 2;
|
|
|
+};
|
|
|
|
|
|
- d.setDate(d.getDate() - d.getDay());
|
|
|
+const dateOptions = (d) => {
|
|
|
+ const today = date.formatDate(new Date(), "YYYY/MM/DD");
|
|
|
+
|
|
|
+ if (d < today) return false;
|
|
|
+
|
|
|
+ const raw = normalizeDate(d);
|
|
|
+ const parsed = new Date(`${raw}T12:00:00`);
|
|
|
+ const dayOfWeek = parsed.getDay();
|
|
|
|
|
|
- return d.toISOString().split('T')[0];
|
|
|
+ const isWorkingDay = availableWeekDays.value.includes(dayOfWeek);
|
|
|
+ const isBlocked = blockedDateSet.value.has(raw);
|
|
|
+ const isWeekBlocked = blockedWeekStartSet.value.has(getWeekStart(raw));
|
|
|
+
|
|
|
+ if (!isWorkingDay || isBlocked || isWeekBlocked) return false;
|
|
|
+
|
|
|
+ if (wouldExceedWeekLimit(raw)) return false;
|
|
|
+
|
|
|
+ return true;
|
|
|
};
|
|
|
|
|
|
+const formatHour = (h) => `${String(h).padStart(2, "0")}:00`;
|
|
|
+
|
|
|
const loadAvailability = async () => {
|
|
|
loadingAvailability.value = true;
|
|
|
|
|
|
try {
|
|
|
const clientId = store.user?.client?.id;
|
|
|
|
|
|
- const defaultClientBlocks = { existing_schedules: [], fully_blocked_weeks: [] };
|
|
|
+ const defaultClientBlocks = {
|
|
|
+ existing_schedules: [],
|
|
|
+ fully_blocked_weeks: [],
|
|
|
+ };
|
|
|
|
|
|
const [wd, bd, clientBlocks] = await Promise.all([
|
|
|
getProviderWorkingDays(props.provider.provider_id),
|
|
|
@@ -350,7 +425,10 @@ const loadAvailability = async () => {
|
|
|
} catch {
|
|
|
blockedDays.value = [];
|
|
|
|
|
|
- providerClientBlocks.value = { existing_schedules: [], fully_blocked_weeks: [] };
|
|
|
+ providerClientBlocks.value = {
|
|
|
+ existing_schedules: [],
|
|
|
+ fully_blocked_weeks: [],
|
|
|
+ };
|
|
|
|
|
|
workingDays.value = [];
|
|
|
} finally {
|
|
|
@@ -358,20 +436,6 @@ const loadAvailability = async () => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-const loadReviews = async () => {
|
|
|
- loadingReviews.value = true;
|
|
|
-
|
|
|
- try {
|
|
|
- const all = await getProviderReceivedReviews(props.provider.provider_id);
|
|
|
-
|
|
|
- reviews.value = (all ?? []).slice(0, 10);
|
|
|
- } catch {
|
|
|
- reviews.value = [];
|
|
|
- } finally {
|
|
|
- loadingReviews.value = false;
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
const loadFavoriteState = async () => {
|
|
|
const clientId = store.user?.client?.id;
|
|
|
if (!clientId) return;
|
|
|
@@ -379,7 +443,7 @@ const loadFavoriteState = async () => {
|
|
|
try {
|
|
|
const favorites = await getClientFavoriteProviders(clientId);
|
|
|
const match = (favorites ?? []).find(
|
|
|
- (f) => Number(f.provider_id) === Number(props.provider.provider_id)
|
|
|
+ (f) => Number(f.provider_id) === Number(props.provider.provider_id),
|
|
|
);
|
|
|
|
|
|
isFavorite.value = !!match;
|
|
|
@@ -390,43 +454,20 @@ const loadFavoriteState = async () => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-const toggleFavorite = async () => {
|
|
|
- if (favoriteLoading.value) return;
|
|
|
-
|
|
|
- const clientId = store.user?.client?.id;
|
|
|
- if (!clientId) {
|
|
|
- $q.notify({ type: 'negative', message: t('scheduling_page.favorite_error') });
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- favoriteLoading.value = true;
|
|
|
+const loadReviews = async () => {
|
|
|
+ loadingReviews.value = true;
|
|
|
|
|
|
try {
|
|
|
- if (isFavorite.value) {
|
|
|
- if (favoriteId.value) {
|
|
|
- await deleteClientFavoriteProvider(favoriteId.value);
|
|
|
- }
|
|
|
- isFavorite.value = false;
|
|
|
- favoriteId.value = null;
|
|
|
- $q.notify({ type: 'positive', message: t('scheduling_page.favorite_removed') });
|
|
|
- } else {
|
|
|
- const created = await createClientFavoriteProvider({
|
|
|
- client_id: clientId,
|
|
|
- provider_id: props.provider.provider_id,
|
|
|
- });
|
|
|
- isFavorite.value = true;
|
|
|
- favoriteId.value = created?.id ?? null;
|
|
|
- $q.notify({ type: 'positive', message: t('scheduling_page.favorite_added') });
|
|
|
- }
|
|
|
+ const all = await getProviderReceivedReviews(props.provider.provider_id);
|
|
|
+
|
|
|
+ reviews.value = (all ?? []).slice(0, 10);
|
|
|
} catch {
|
|
|
- $q.notify({ type: 'negative', message: t('scheduling_page.favorite_error') });
|
|
|
+ reviews.value = [];
|
|
|
} finally {
|
|
|
- favoriteLoading.value = false;
|
|
|
+ loadingReviews.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-const normalizeDate = (dateStr) => (dateStr ?? '').replace(/\//g, '-');
|
|
|
-
|
|
|
const onDateSelected = (val) => {
|
|
|
if (!val) return;
|
|
|
|
|
|
@@ -435,7 +476,7 @@ const onDateSelected = (val) => {
|
|
|
const valFormatted = normalizeDate(val);
|
|
|
|
|
|
const blocksOfDate = blockedDays.value.filter(
|
|
|
- (bd) => bd.date === valFormatted && bd.period !== 'all'
|
|
|
+ (bd) => bd.date === valFormatted && bd.period !== "all",
|
|
|
);
|
|
|
|
|
|
const dayOfWeek = new Date(`${valFormatted}T12:00:00`).getDay();
|
|
|
@@ -446,35 +487,38 @@ const onDateSelected = (val) => {
|
|
|
|
|
|
const workingDayBlocks = [];
|
|
|
|
|
|
- if (!dayPeriods.includes('afternoon')) {
|
|
|
- workingDayBlocks.push({ init_hour: '14:00:00', end_hour: '20:00:00' });
|
|
|
+ if (!dayPeriods.includes("afternoon")) {
|
|
|
+ workingDayBlocks.push({ init_hour: "14:00:00", end_hour: "20:00:00" });
|
|
|
}
|
|
|
|
|
|
- if (!dayPeriods.includes('morning')) {
|
|
|
- workingDayBlocks.push({ init_hour: '07:00:00', end_hour: '13:00:00' });
|
|
|
+ if (!dayPeriods.includes("morning")) {
|
|
|
+ workingDayBlocks.push({ init_hour: "07:00:00", end_hour: "13:00:00" });
|
|
|
}
|
|
|
|
|
|
const localBookingBlocks = bookings.value
|
|
|
- .filter((booking) => booking.date.replace(/\//g, '-') === valFormatted)
|
|
|
+ .filter((booking) => booking.date.replace(/\//g, "-") === valFormatted)
|
|
|
.map((booking) => ({
|
|
|
- end_hour: `${booking.slot.endHour}:00:00`,
|
|
|
+ end_hour: `${booking.slot.endHour}:00:00`,
|
|
|
init_hour: `${booking.slot.startHour}:00:00`,
|
|
|
}));
|
|
|
|
|
|
const cartBookingBlocks = scheduleCart.items
|
|
|
- .filter((item) =>
|
|
|
- Number(item.provider_id) === Number(props.provider.provider_id) &&
|
|
|
- normalizeDate(item.date) === valFormatted
|
|
|
+ .filter(
|
|
|
+ (item) =>
|
|
|
+ Number(item.provider_id) === Number(props.provider.provider_id) &&
|
|
|
+ normalizeDate(item.date) === valFormatted,
|
|
|
)
|
|
|
.map((item) => ({
|
|
|
- end_hour: item.end_time,
|
|
|
+ end_hour: item.end_time,
|
|
|
init_hour: item.start_time,
|
|
|
}));
|
|
|
|
|
|
- const serverBookingBlocks = (providerClientBlocks.value.existing_schedules ?? [])
|
|
|
+ const serverBookingBlocks = (
|
|
|
+ providerClientBlocks.value.existing_schedules ?? []
|
|
|
+ )
|
|
|
.filter((schedule) => normalizeDate(schedule.date) === valFormatted)
|
|
|
.map((schedule) => ({
|
|
|
- end_hour: schedule.end_time,
|
|
|
+ end_hour: schedule.end_time,
|
|
|
init_hour: schedule.start_time,
|
|
|
}));
|
|
|
|
|
|
@@ -487,25 +531,46 @@ const onDateSelected = (val) => {
|
|
|
];
|
|
|
|
|
|
$q.dialog({
|
|
|
- component: ServiceSelectionSheet, componentProps: { partialBlocks, provider: props.provider, selectedDate: val },
|
|
|
+ component: ServiceSelectionSheet,
|
|
|
+ componentProps: {
|
|
|
+ partialBlocks,
|
|
|
+ provider: props.provider,
|
|
|
+ selectedDate: val,
|
|
|
+ },
|
|
|
}).onOk(({ action, serviceType, date: date_, provider: prov }) => {
|
|
|
$q.dialog({
|
|
|
- component: ServiceTimeSelectionDialog, componentProps: {partialBlocks, provider: prov, selectedDate: date_, serviceType },
|
|
|
+ component: ServiceTimeSelectionDialog,
|
|
|
+ componentProps: {
|
|
|
+ partialBlocks,
|
|
|
+ provider: prov,
|
|
|
+ selectedDate: date_,
|
|
|
+ serviceType,
|
|
|
+ },
|
|
|
}).onOk((booking) => {
|
|
|
- if (action === 'cart') {
|
|
|
+ if (action === "cart") {
|
|
|
scheduleCart.addItem({
|
|
|
- date: normalizeDate(booking.date),
|
|
|
- end_time: formatHour(booking.slot.endHour),
|
|
|
- local_id: `${prov.provider_id}-${booking.date}-${booking.slot.value}-${Date.now()}`,
|
|
|
- offers_meal: booking.meal === 'offer' ? true : booking.meal === 'no_offer' ? false : null,
|
|
|
- period_type: booking.serviceType.hoursCount,
|
|
|
- provider_id: prov.provider_id,
|
|
|
+ date: normalizeDate(booking.date),
|
|
|
+ end_time: formatHour(booking.slot.endHour),
|
|
|
+ local_id: `${prov.provider_id}-${booking.date}-${booking.slot.value}-${Date.now()}`,
|
|
|
+ offers_meal:
|
|
|
+ booking.meal === "offer"
|
|
|
+ ? true
|
|
|
+ : booking.meal === "no_offer"
|
|
|
+ ? false
|
|
|
+ : null,
|
|
|
+ period_type: booking.serviceType.hoursCount,
|
|
|
+ provider_id: prov.provider_id,
|
|
|
provider_name: prov.provider_name,
|
|
|
- start_time: formatHour(booking.slot.startHour),
|
|
|
- base_amount: booking.serviceType.basePrice ?? booking.serviceType.price,
|
|
|
+ provider_photo: getProfileMediaUrl(prov),
|
|
|
+ start_time: formatHour(booking.slot.startHour),
|
|
|
+ base_amount:
|
|
|
+ booking.serviceType.basePrice ?? booking.serviceType.price,
|
|
|
});
|
|
|
|
|
|
- $q.notify({ message: t('schedule_cart.add_success'), type: 'positive' });
|
|
|
+ $q.notify({
|
|
|
+ message: t("schedule_cart.add_success"),
|
|
|
+ type: "positive",
|
|
|
+ });
|
|
|
|
|
|
return;
|
|
|
}
|
|
|
@@ -513,7 +578,8 @@ const onDateSelected = (val) => {
|
|
|
bookings.value.push(booking);
|
|
|
|
|
|
$q.dialog({
|
|
|
- component: OrderSummaryDialog, componentProps: { initialBooking: booking, provider: props.provider },
|
|
|
+ component: OrderSummaryDialog,
|
|
|
+ componentProps: { initialBooking: booking, provider: props.provider },
|
|
|
}).onOk(() => {
|
|
|
dialogRef.value.hide();
|
|
|
});
|
|
|
@@ -521,18 +587,62 @@ const onDateSelected = (val) => {
|
|
|
});
|
|
|
};
|
|
|
|
|
|
-const wouldExceedWeekLimit = (selectedDate) => {
|
|
|
- const cartCount = getCartWeekCount(selectedDate);
|
|
|
- const localCount = getLocalWeekCount(selectedDate);
|
|
|
- const serverCount = getServerWeekCount(selectedDate);
|
|
|
+const toggleFavorite = async () => {
|
|
|
+ if (favoriteLoading.value) return;
|
|
|
|
|
|
- return (serverCount + localCount + cartCount) >= 2;
|
|
|
+ const clientId = store.user?.client?.id;
|
|
|
+ if (!clientId) {
|
|
|
+ $q.notify({
|
|
|
+ message: t("scheduling_page.favorite_error"),
|
|
|
+ type: "negative",
|
|
|
+ });
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ favoriteLoading.value = true;
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (isFavorite.value) {
|
|
|
+ if (favoriteId.value) {
|
|
|
+ await deleteClientFavoriteProvider(favoriteId.value);
|
|
|
+ }
|
|
|
+
|
|
|
+ favoriteId.value = null;
|
|
|
+ isFavorite.value = false;
|
|
|
+
|
|
|
+ $q.notify({
|
|
|
+ message: t("scheduling_page.favorite_removed"),
|
|
|
+ type: "positive",
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ const created = await createClientFavoriteProvider({
|
|
|
+ client_id: clientId,
|
|
|
+ provider_id: props.provider.provider_id,
|
|
|
+ });
|
|
|
+
|
|
|
+ favoriteId.value = created?.id ?? null;
|
|
|
+ isFavorite.value = true;
|
|
|
+
|
|
|
+ $q.notify({
|
|
|
+ message: t("scheduling_page.favorite_added"),
|
|
|
+ type: "positive",
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ $q.notify({
|
|
|
+ message: t("scheduling_page.favorite_error"),
|
|
|
+ type: "negative",
|
|
|
+ });
|
|
|
+ } finally {
|
|
|
+ favoriteLoading.value = false;
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
onMounted(() => {
|
|
|
loadAvailability();
|
|
|
- loadReviews();
|
|
|
loadFavoriteState();
|
|
|
+ loadReviews();
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
@@ -545,7 +655,7 @@ onMounted(() => {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
overflow: hidden;
|
|
|
- background: #F9FAFB;
|
|
|
+ background: #f9fafb;
|
|
|
}
|
|
|
|
|
|
.dialog-header {
|
|
|
@@ -571,7 +681,7 @@ onMounted(() => {
|
|
|
}
|
|
|
|
|
|
:deep(.q-date__calendar-days .q-btn__content) {
|
|
|
- color: #1E293B !important;
|
|
|
+ color: #1e293b !important;
|
|
|
}
|
|
|
|
|
|
// dias desabilitados: visíveis mas opacos
|
|
|
@@ -589,14 +699,18 @@ onMounted(() => {
|
|
|
|
|
|
// cabeçalho dos dias (dom, seg, ter, qua...)
|
|
|
:deep(.q-date__calendar-weekdays > div) {
|
|
|
- color: #6366F1;
|
|
|
+ color: #6366f1;
|
|
|
font-weight: 700;
|
|
|
opacity: 0.8;
|
|
|
}
|
|
|
|
|
|
:deep(.q-date__navigation) {
|
|
|
- .q-btn { color: #6366F1 !important; }
|
|
|
- .q-btn__content { color: #6366F1 !important; }
|
|
|
+ .q-btn {
|
|
|
+ color: #6366f1 !important;
|
|
|
+ }
|
|
|
+ .q-btn__content {
|
|
|
+ color: #6366f1 !important;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
:deep(.q-date__event) {
|
|
|
@@ -614,7 +728,7 @@ onMounted(() => {
|
|
|
|
|
|
:deep(.q-date__selected .q-btn__content),
|
|
|
:deep(.q-date__calendar-item .q-btn.q-date__selected .q-btn__content) {
|
|
|
- background: #6366F1 !important;
|
|
|
+ background: #6366f1 !important;
|
|
|
color: #ffffff !important;
|
|
|
border-radius: 50%;
|
|
|
box-shadow: 0 4px 10px rgba(99, 102, 241, 0.4);
|
|
|
@@ -622,7 +736,7 @@ onMounted(() => {
|
|
|
|
|
|
:deep(.q-date__view--months .q-btn),
|
|
|
:deep(.q-date__view--years .q-btn) {
|
|
|
- color: #6366F1 !important;
|
|
|
+ color: #6366f1 !important;
|
|
|
}
|
|
|
|
|
|
:deep(.q-date__calendar-item--out) {
|
|
|
@@ -639,7 +753,9 @@ onMounted(() => {
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
scrollbar-width: none;
|
|
|
padding-bottom: 8px;
|
|
|
- &::-webkit-scrollbar { display: none; }
|
|
|
+ &::-webkit-scrollbar {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
.review-card {
|