| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371 |
- <template>
- <q-dialog ref="dialogRef" persistent maximized transition-show="slide-up" transition-hide="slide-down">
- <div class="dialog-root">
- <div class="dialog-header row items-center q-px-md q-pt-md q-pb-sm bg-white">
- <q-btn v-close-popup flat round dense icon="mdi-chevron-left" color="primary" />
- <div class="col text-center text-subtitle1 text-weight-bold text-primary gradient-diarista q-mb-xs">
- {{ $t('scheduling_page.title') }}
- </div>
- <div style="width: 36px" />
- </div>
- <div class="dialog-body">
- <div class="q-px-md q-pt-md">
- <div class="text-h6 text-weight-bold gradient-diarista q-mb-xs">
- {{ $t('scheduling_page.about_provider') }}
- </div>
- <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">
- <q-avatar :style="avatarStyle" size="52px" class="text-weight-bold text-body1">
- {{ provider?.provider_name?.slice(0, 1).toUpperCase() ?? '—' }}
- </q-avatar>
- <div class="col min-width-0">
- <div class="text-weight-bold text-text">{{ provider?.provider_name ?? '—' }}</div>
- <div class="text-caption text-grey-6">{{ provider?.district ?? '—' }}</div>
- <div class="row items-center q-gutter-x-md 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) : '') + ' (' + (provider?.total_reviews ?? 0) + ')' }}
- </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-lg">
- <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="calendar-wrapper shadow-card q-mb-md">
- <q-date
- v-model="selectedDate"
- square
- class="full-width"
- :first-day-of-week="0"
- :options="dateOptions"
- minimal
- @update:model-value="onDateSelected"
- />
- </div>
- </div>
- <div class="q-px-md q-pt-sm q-pb-xl">
- <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-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>
- <div v-else class="reviews-scroll">
- <q-card
- v-for="review in reviews"
- :key="review.id"
- class="review-card card-border bg-white q-mr-sm shadow-card"
- :flat="false"
- >
- <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">
- {{ review.comment ?? '' }}
- </div>
- </q-card-section>
- </q-card>
- </div>
- </div>
- </div>
- </div>
- </q-dialog>
- </template>
- <script setup>
- import { ref, computed, onMounted } from 'vue';
- import { useDialogPluginComponent, useQuasar } from 'quasar';
- import { date } from 'quasar';
- import { getProviderWorkingDays, getProviderBlockedDays } from 'src/api/providerAvailability';
- import { getProviderReceivedReviews } from 'src/api/review';
- import ServiceSelectionSheet from './ServiceSelectionSheet.vue';
- import ServiceTimeSelectionDialog from './ServiceTimeSelectionDialog.vue';
- import OrderSummaryDialog from './OrderSummaryDialog.vue';
- const props = defineProps({
- provider: {
- type: Object,
- required: true,
- },
- });
- defineEmits([...useDialogPluginComponent.emits]);
- const { dialogRef } = useDialogPluginComponent();
- const $q = useQuasar();
- const onDateSelected = (val) => {
- if (!val) return;
- selectedDate.value = null;
- $q.dialog({
- component: ServiceSelectionSheet,
- componentProps: { provider: props.provider, selectedDate: val },
- }).onOk(({ serviceType, date: date_, provider: prov }) => {
- $q.dialog({
- component: ServiceTimeSelectionDialog,
- componentProps: { serviceType, selectedDate: date_, provider: prov },
- }).onOk((booking) => {
- $q.dialog({
- component: OrderSummaryDialog,
- componentProps: { provider: props.provider, initialBooking: booking },
- });
- });
- });
- };
- 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 = (props.provider?.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 dateOptions = (d) => {
- const today = date.formatDate(new Date(), 'YYYY/MM/DD');
- if (d < today) return false;
- const raw = d.replace(/\//g, '-');
- const parsed = new Date(`${raw}T12:00:00`);
- const dayOfWeek = parsed.getDay();
- const isWorkingDay = availableWeekDays.value.includes(dayOfWeek);
- const isBlocked = blockedDateSet.value.has(raw);
- return isWorkingDay && !isBlocked;
- };
- const loadAvailability = async () => {
- loadingAvailability.value = true;
- try {
- const [wd, bd] = await Promise.all([
- getProviderWorkingDays(props.provider.provider_id),
- getProviderBlockedDays(props.provider.provider_id),
- ]);
- workingDays.value = wd ?? [];
- blockedDays.value = bd ?? [];
- } catch {
- workingDays.value = [];
- blockedDays.value = [];
- } finally {
- loadingAvailability.value = false;
- }
- };
- 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;
- }
- };
- onMounted(() => {
- Promise.all([loadAvailability(), loadReviews()]);
- });
- </script>
- <style scoped lang="scss">
- .dialog-root {
- width: 100vw;
- max-width: 100vw;
- height: 100vh;
- max-height: 100vh;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- background: #F9FAFB;
- }
- .dialog-header {
- flex-shrink: 0;
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
- }
- .dialog-body {
- flex: 1;
- min-height: 0;
- overflow-y: auto;
- overflow-x: clip;
- }
- .calendar-wrapper {
- border-radius: 20px;
- overflow: hidden;
- background: white;
- :deep(.q-date) {
- background: white;
- width: 100%;
- }
- :deep(.q-date__calendar-days .q-btn__content) {
- color: #1E293B !important;
- }
- // dias desabilitados: visíveis mas opacos
- :deep(.q-date__calendar-days .q-btn.disabled .q-btn__content),
- :deep(.q-date__calendar-days .q-btn[disabled] .q-btn__content) {
- color: #000000 !important;
- opacity: 1 !important;
- }
- // o Quasar aplica opacity no elemento .q-btn quando disabled — reseta
- :deep(.q-date__calendar-days .q-btn.disabled),
- :deep(.q-date__calendar-days .q-btn[disabled]) {
- opacity: 1 !important;
- }
- // cabeçalho dos dias (dom, seg, ter, qua...)
- :deep(.q-date__calendar-weekdays > div) {
- color: #6366F1;
- font-weight: 700;
- opacity: 0.8;
- }
- :deep(.q-date__navigation) {
- .q-btn { color: #6366F1 !important; }
- .q-btn__content { color: #6366F1 !important; }
- }
- :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),
- :deep(.q-date__calendar-item .q-btn.q-date__selected .q-btn__content) {
- background: #6366F1 !important;
- color: #ffffff !important;
- border-radius: 50%;
- box-shadow: 0 4px 10px rgba(99, 102, 241, 0.4);
- }
- :deep(.q-date__view--months .q-btn),
- :deep(.q-date__view--years .q-btn) {
- color: #6366F1 !important;
- }
- :deep(.q-date__calendar-item--out) {
- color: #b9b9b9 !important;
- opacity: 0.8 !important;
- }
- }
- // Reviews scroll horizontal
- .reviews-scroll {
- display: flex;
- flex-direction: row;
- overflow-x: auto;
- -webkit-overflow-scrolling: touch;
- scrollbar-width: none;
- padding-bottom: 8px;
- &::-webkit-scrollbar { display: none; }
- }
- .review-card {
- flex-shrink: 0;
- min-width: 220px;
- max-width: 240px;
- border-radius: 12px;
- margin-right: 8px;
- }
- .review-comment {
- display: -webkit-box;
- -webkit-line-clamp: 3;
- line-clamp: 3;
- -webkit-box-orient: vertical;
- overflow: hidden;
- }
- .min-width-0 {
- min-width: 0;
- }
- </style>
|