|
|
@@ -1,52 +1,281 @@
|
|
|
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
|
|
|
<template>
|
|
|
- <section class="mobile-placeholder">
|
|
|
- <div class="mobile-placeholder__badge">
|
|
|
- <q-icon name="mdi-magnify" />
|
|
|
+ <q-page class="bg-page">
|
|
|
+
|
|
|
+ <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-search">
|
|
|
+ <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('search_page.title') }}
|
|
|
+ </div>
|
|
|
+ <div style="width: 36px" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="q-px-md q-pt-md q-pb-sm">
|
|
|
+ <q-card class="custom-schedule-card bg-surface shadow-card q-pa-sm" :flat="false">
|
|
|
+ <q-card-section class="row items-center no-wrap q-pa-sm q-gutter-x-sm">
|
|
|
+ <span class="col text-text fonte-hint">
|
|
|
+ {{ $t('search_page.custom_schedule_description') }}
|
|
|
+ </span>
|
|
|
+ <q-btn
|
|
|
+ color="secondary"
|
|
|
+ no-caps
|
|
|
+ unelevated
|
|
|
+ padding="8px 16px"
|
|
|
+ class="text-weight-bold custom-schedule-btn card-border"
|
|
|
+ >
|
|
|
+ <template #default>
|
|
|
+ <div class="column items-center q-gutter-y-xs">
|
|
|
+ <q-icon name="mdi-scissors-cutting" size="16px" />
|
|
|
+ <span>{{ $t('search_page.custom_schedule_btn') }}</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </q-btn>
|
|
|
+ </q-card-section>
|
|
|
+ </q-card>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="row items-center q-px-md q-py-md q-gutter-x-sm">
|
|
|
+ <q-input
|
|
|
+ v-model="searchName"
|
|
|
+ :placeholder="$t('search_page.search_placeholder')"
|
|
|
+ outlined
|
|
|
+ rounded
|
|
|
+ dense
|
|
|
+ clearable
|
|
|
+ debounce="400"
|
|
|
+ class="col bg-white search-input"
|
|
|
+ input-class="text-text"
|
|
|
+ @update:model-value="onNameChange"
|
|
|
+ >
|
|
|
+ <template #append>
|
|
|
+ <q-icon name="mdi-magnify" color="grey-5" />
|
|
|
+ </template>
|
|
|
+ </q-input>
|
|
|
+ <q-btn
|
|
|
+ flat round dense
|
|
|
+ icon="mdi-tune-variant"
|
|
|
+ color="grey-6"
|
|
|
+ size="md"
|
|
|
+ :class="{ 'filter-active': activeSort || activeDate }"
|
|
|
+ @click="openFilterDialog"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="row items-center justify-between no-wrap q-px-md q-pb-sm">
|
|
|
+ <div class="dashboard-section-title gradient-diarista">{{ $t('search_page.choose_provider') }}</div>
|
|
|
+ <div class="row items-center no-wrap text-text">
|
|
|
+ <q-btn flat dense round icon="mdi-chevron-left" color="text" size="sm" @click="setPeriodTypePrevious" />
|
|
|
+ <span class="text-caption text-weight-medium">{{ periodLabel }}</span>
|
|
|
+ <q-btn flat dense round icon="mdi-chevron-right" color="text" size="sm" @click="setPeriodTypeNext" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="loading" class="row items-center justify-center q-py-xl">
|
|
|
+ <q-spinner-dots color="primary" size="40px" />
|
|
|
</div>
|
|
|
- <h1 class="mobile-placeholder__title">Busca</h1>
|
|
|
- <p class="mobile-placeholder__description">
|
|
|
- Área reservada para a busca de diárias e oportunidades próximas.
|
|
|
- </p>
|
|
|
- </section>
|
|
|
+
|
|
|
+ <template v-else>
|
|
|
+ <div v-if="sortedProviders.length === 0" class="text-center text-grey-6 q-px-md q-py-lg text-body2">
|
|
|
+ {{ $t('search_page.no_results') }}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-else class="column q-px-md q-pb-xl">
|
|
|
+ <q-card
|
|
|
+ v-for="p in sortedProviders"
|
|
|
+ :key="p.provider_id"
|
|
|
+ class="card-border bg-page text-text q-mb-sm"
|
|
|
+ :flat="false"
|
|
|
+ >
|
|
|
+ <q-card-section class="row no-wrap q-pa-sm">
|
|
|
+ <div class="row no-wrap full-width">
|
|
|
+ <div class="col-2">
|
|
|
+ <q-avatar :style="avatarColors[p.provider_id % avatarColors.length]" class="text-weight-bold">
|
|
|
+ {{ p.provider_name?.slice(0,1).toUpperCase() ?? '—' }}
|
|
|
+ </q-avatar>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="col-10 row">
|
|
|
+ <div class="column col-9 justify-between">
|
|
|
+ <span class="text-provider-close-name">{{ p.provider_name ?? 'Prestador' }}</span>
|
|
|
+ <span class="text-provider-close-region">{{ p.district }}</span>
|
|
|
+ <div class="row items-center justify-between q-pr-lg">
|
|
|
+ <div class="row items-center">
|
|
|
+ <q-icon name="mdi-star" color="warning" size="16px" />
|
|
|
+ <span class="text-provider-close-rating">
|
|
|
+ {{ p.average_rating != null ? (Number(p.average_rating).toFixed(1) + ' (' + (p.total_reviews ?? 0) + ')') : ('(' + (p.total_reviews ?? 0) + ')') }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div class="row items-center">
|
|
|
+ <q-icon name="mdi-broom" color="secondary" size="16px" />
|
|
|
+ <span class="text-provider-close-jobs">{{ p.total_services ?? 0 }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="row items-center">
|
|
|
+ <q-icon name="mdi-map-marker-outline" color="text" size="16px" />
|
|
|
+ <span class="text-provider-close-jobs">{{ 0 + ' km' }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="column col-3 justify-between text-center items-center">
|
|
|
+ <span class="text-provider-close-price">{{ priceByPeriod(p) }}</span>
|
|
|
+ <div class="full-width">
|
|
|
+ <q-btn
|
|
|
+ unelevated rounded no-caps
|
|
|
+ color="primary"
|
|
|
+ size="sm"
|
|
|
+ padding="3px 12px"
|
|
|
+ :label="$t('search_page.schedule_btn')"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </q-card-section>
|
|
|
+ </q-card>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ </q-page>
|
|
|
</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;
|
|
|
-}
|
|
|
+<script setup>
|
|
|
+import { ref, computed, onMounted } from 'vue';
|
|
|
+import { useRouter } from 'vue-router';
|
|
|
+import { useI18n } from 'vue-i18n';
|
|
|
+// import { useQuasar } from 'quasar';
|
|
|
+import { buscaPrestadores } from 'src/api/dashboard';
|
|
|
+import { formatCurrency } from 'src/helpers/utils';
|
|
|
+// import SearchFilterDialog from 'src/pages/search/components/SearchFilterDialog.vue';
|
|
|
|
|
|
-.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;
|
|
|
-}
|
|
|
+const { t } = useI18n();
|
|
|
+const router = useRouter();
|
|
|
+// const $q = useQuasar();
|
|
|
|
|
|
-.mobile-placeholder__title {
|
|
|
- margin: 0 0 8px;
|
|
|
- font-size: 28px;
|
|
|
- font-weight: 700;
|
|
|
- line-height: 1.1;
|
|
|
- color: #4d4d4d;
|
|
|
-}
|
|
|
+const allProviders = ref([]);
|
|
|
+const loading = ref(true);
|
|
|
+const searchName = ref('');
|
|
|
+const activeDate = ref(null);
|
|
|
+const activeSort = ref(null);
|
|
|
|
|
|
-.mobile-placeholder__description {
|
|
|
- max-width: 280px;
|
|
|
- margin: 0;
|
|
|
- font-size: 16px;
|
|
|
- line-height: 1.5;
|
|
|
- color: #8d8d8d;
|
|
|
+const currentPeriodType = ref(8);
|
|
|
+const periodTypeMap = { 2: 'daily_price_2h', 4: 'daily_price_4h', 6: 'daily_price_6h', 8: 'daily_price_8h' };
|
|
|
+
|
|
|
+const periodLabel = computed(() => {
|
|
|
+ const labels = { 8: t('search_page.until_8h'), 6: t('search_page.until_6h'), 4: t('search_page.until_4h'), 2: t('search_page.until_2h') };
|
|
|
+ return labels[currentPeriodType.value] ?? '';
|
|
|
+});
|
|
|
+
|
|
|
+const priceByPeriod = (p) => {
|
|
|
+ const key = periodTypeMap[currentPeriodType.value];
|
|
|
+ return p[key] ? formatCurrency(p[key]) : t('search_page.no_price');
|
|
|
+};
|
|
|
+
|
|
|
+const setPeriodTypePrevious = () => {
|
|
|
+ const prev = currentPeriodType.value - 2;
|
|
|
+ if (periodTypeMap[prev]) currentPeriodType.value = prev;
|
|
|
+};
|
|
|
+
|
|
|
+const setPeriodTypeNext = () => {
|
|
|
+ const next = currentPeriodType.value + 2;
|
|
|
+ if (periodTypeMap[next]) currentPeriodType.value = next;
|
|
|
+};
|
|
|
+
|
|
|
+const sortedProviders = computed(() => {
|
|
|
+ const list = [...allProviders.value];
|
|
|
+ const priceKey = periodTypeMap[currentPeriodType.value];
|
|
|
+
|
|
|
+ switch (activeSort.value) {
|
|
|
+ case 'price_asc':
|
|
|
+ return list.sort((a, b) => Number(a[priceKey] ?? 0) - Number(b[priceKey] ?? 0));
|
|
|
+ case 'price_desc':
|
|
|
+ return list.sort((a, b) => Number(b[priceKey] ?? 0) - Number(a[priceKey] ?? 0));
|
|
|
+ case 'rating_desc':
|
|
|
+ return list.sort((a, b) => Number(b.average_rating ?? 0) - Number(a.average_rating ?? 0));
|
|
|
+ case 'rating_asc':
|
|
|
+ return list.sort((a, b) => Number(a.average_rating ?? 0) - Number(b.average_rating ?? 0));
|
|
|
+ case 'reviews_desc':
|
|
|
+ return list.sort((a, b) => Number(b.total_reviews ?? 0) - Number(a.total_reviews ?? 0));
|
|
|
+ case 'reviews_asc':
|
|
|
+ return list.sort((a, b) => Number(a.total_reviews ?? 0) - Number(b.total_reviews ?? 0));
|
|
|
+ case 'services_desc':
|
|
|
+ return list.sort((a, b) => Number(b.total_services ?? 0) - Number(a.total_services ?? 0));
|
|
|
+ case 'services_asc':
|
|
|
+ return list.sort((a, b) => Number(a.total_services ?? 0) - Number(b.total_services ?? 0));
|
|
|
+ case 'oldest':
|
|
|
+ return list.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
|
|
+ case 'newest':
|
|
|
+ return list.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
|
|
+ default:
|
|
|
+ return list.sort((a, b) => Number(b.average_rating ?? 0) - Number(a.average_rating ?? 0));
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+const loadProviders = async () => {
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ allProviders.value = await buscaPrestadores({
|
|
|
+ name: searchName.value,
|
|
|
+ date: activeDate.value ?? '',
|
|
|
+ }) ?? [];
|
|
|
+ } catch {
|
|
|
+ allProviders.value = [];
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const onNameChange = () => loadProviders();
|
|
|
+
|
|
|
+// const openFilterDialog = () => {
|
|
|
+// $q.dialog({
|
|
|
+// component: SearchFilterDialog,
|
|
|
+// componentProps: {
|
|
|
+// initialSort: activeSort.value,
|
|
|
+// initialDate: activeDate.value,
|
|
|
+// },
|
|
|
+// }).onOk(({ sort, date }) => {
|
|
|
+// const dateChanged = date !== activeDate.value;
|
|
|
+// activeSort.value = sort;
|
|
|
+// activeDate.value = date;
|
|
|
+// if (dateChanged) loadProviders();
|
|
|
+// });
|
|
|
+// };
|
|
|
+
|
|
|
+const avatarColors = [
|
|
|
+ { background: '#ffd5df', color: '#932e57' },
|
|
|
+ { background: '#d7e8ff', color: '#2158a8' },
|
|
|
+ { background: '#dfd', color: '#2a7a3b' },
|
|
|
+ { background: '#ffe5cc', color: '#8a4500' },
|
|
|
+];
|
|
|
+
|
|
|
+onMounted(() => loadProviders());
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.shadow-search {
|
|
|
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
|
|
+}
|
|
|
+.search-input {
|
|
|
+ :deep(.q-field__control) {
|
|
|
+ border-radius: 28px;
|
|
|
+ }
|
|
|
+}
|
|
|
+.custom-schedule-card {
|
|
|
+ border-radius: 12px;
|
|
|
+}
|
|
|
+.custom-schedule-btn {
|
|
|
+ flex-shrink: 0;
|
|
|
+ min-width: 72px;
|
|
|
+}
|
|
|
+.filter-active {
|
|
|
+ color: var(--q-primary) !important;
|
|
|
+}
|
|
|
+.fonte-hint {
|
|
|
+ font-family: Inter;
|
|
|
+ font-weight: 500;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 100%;
|
|
|
+ letter-spacing: -0.04em;
|
|
|
+ vertical-align: middle;
|
|
|
}
|
|
|
</style>
|