| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680 |
- <!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
- <template>
- <q-page class="sob-medida-page">
- <div class="page-shell">
- <span class="page-title gradient-diarista">Serviço Sob Medida</span>
- <q-card flat bordered class="figma-card compact-card">
- <div class="card-title text-center gradient-diarista">
- Seu pedido
- </div>
- <div class="field-label text-center gradient-diarista">
- Quantidade de serviço
- </div>
- <div class="quantity-stepper">
- <q-btn
- round
- flat
- icon="remove"
- class="quantity-btn"
- @click="decreaseQuantity"
- />
- <span class="quantity-value gradient-diarista">
- {{ quantity }}
- </span>
- <q-btn
- round
- flat
- icon="add"
- class="quantity-btn"
- @click="increaseQuantity"
- />
- </div>
- <div class="options-grid gradient-diarista">
- <div
- v-for="option in addressTypesOptions"
- :key="option.value"
- class="option-col"
- >
- <q-radio
- v-model="selectedOption"
- :val="option.value"
- :label="option.label"
- color="purple"
- dense
- />
- </div>
- </div>
- <div class="field-label text-center gradient-diarista">
- Tipo de serviço
- </div>
- <div class="service-type-inline">
- <div
- v-for="serviceType in serviceTypes"
- :key="serviceType.id"
- class="option-col"
- >
- <q-radio
- v-model="selectedServiceType"
- :val="serviceType.id"
- :label="serviceType.description"
- color="purple"
- dense
- />
- </div>
- </div>
- <div class="field-label text-center gradient-diarista">Especialidade preferencial?</div>
- <div class="options-grid">
- <div class="options-grid">
- <div
- v-for="item in specialties"
- :key="item.id"
- class="option-col"
- >
- <q-checkbox
- v-model="selectedSpecialties"
- :val="item.id"
- :label="item.description"
- color="purple"
- dense
- />
- </div>
- </div>
- </div>
- <div class="field-label text-center gradient-diarista">
- Descreva detalhes do pedido
- <span class="optional">(opcional)</span>
- </div>
- <q-input
- v-model="description"
- type="textarea"
- outlined
- bg-color="white"
- color="dark"
- input-class="text-black"
- class="description-box"
- placeholder="Olá, desejo profissional dedicado que irá fazer..."
- />
- </q-card>
- <!-- Faixa -->
- <div class="section-title gradient-diarista">Faixa de preço por 8 horas</div>
- <q-card flat bordered class="figma-card compact-card">
- <div class="range-container">
- <!-- bolha min -->
- <div
- class="price-pin"
- :style="{ left: minPosition + '%' }"
- >
- <span>{{ priceRange.min }}</span>
- </div>
- <!-- bolha max -->
- <div
- class="price-pin"
- :style="{ left: maxPosition + '%' }"
- >
- <span>{{ priceRange.max }}</span>
- </div>
- <q-range
- v-model="priceRange"
- :min="PRICE_LIMITS.min"
- :max="PRICE_LIMITS.max"
- color="secondary"
- class="price-range"
- />
- </div>
- <div class="range-helper gradient-diarista">
- Selecione a faixa de preço integral para receber propostas de diaristas.
- </div>
- </q-card>
- <!-- Data -->
- <div class="section-title gradient-diarista">Data e hora</div>
- <q-card flat bordered class="figma-card date-card">
- <q-date
- v-model="selectedDate"
- minimal
- color="purple"
- class="figma-date calendar-custom "
- />
- </q-card>
- </div>
- </q-page>
- </template>
- <script setup>
- import { ref, computed, watch, onMounted } from 'vue'
- import { useQuasar } from 'quasar'
- import ServiceSelectionSheet from 'src/pages/search/components/ServiceSelectionSheet.vue'
- import ServiceTimeSelectionDialog from 'src/pages/search/components/ServiceTimeSelectionDialog.vue'
- import { createCustomSchedule } from 'src/api/customSchedules'
- import { getPrimaryAddress } from 'src/api/address'
- import { getPublicServiceTypes } from 'src/api/serviceTypes'
- import { getPublicSpecialties } from 'src/api/specialties'
- import { userStore } from 'src/stores/user'
- import { calculateDailyPrices } from 'src/helpers/utils'
- const $q = useQuasar()
- const user = userStore()
- const addressTypesOptions = computed(() => [
- { label: 'Residencial', value: 'home' },
- { label: 'Comercial', value: 'commercial' }
- ])
- // listas vindas da API
- const serviceTypes = ref([])
- const specialties = ref([])
- // seleções do usuário
- const selectedServiceType = ref(null)
- const selectedSpecialties = ref([])
- const selectedOption = ref('home')
- // campos do formulário
- const address = ref([])
- const description = ref('')
- const selectedDate = ref(null)
- const quantity = ref(1)
- const PRICE_LIMITS = Object.freeze({
- min: 100,
- max: 500
- })
- const priceRange = ref({
- min: 150,
- max: 300
- })
- const minPosition = computed(() =>
- ((priceRange.value.min - PRICE_LIMITS.min) /
- (PRICE_LIMITS.max - PRICE_LIMITS.min)) * 100
- )
- const maxPosition = computed(() =>
- ((priceRange.value.max - PRICE_LIMITS.min) /
- (PRICE_LIMITS.max - PRICE_LIMITS.min)) * 100
- )
- onMounted(async () => {
- const { data } = await getPrimaryAddress(user.user.client.id, 'client')
- address.value = data.payload
- const publicServiceTypes = await getPublicServiceTypes()
- serviceTypes.value = publicServiceTypes
- const publicSpecialties = await getPublicSpecialties()
- specialties.value = publicSpecialties
- })
- watch(selectedDate, (newDate, oldDate) => {
- if (!newDate || newDate === oldDate) return
- openServiceSelection()
- })
- function openServiceSelection () {
- $q.dialog({
- component: ServiceSelectionSheet,
- componentProps: {
- provider: calculateDailyPrices(priceRange.value.max * quantity.value),
- selectedDate: selectedDate.value
- }
- }).onOk((payload) => {
- if (payload?.serviceType) {
- openServiceTimeSelection(payload.serviceType)
- }
- })
- }
- function openServiceTimeSelection (serviceType) {
- $q.dialog({
- component: ServiceTimeSelectionDialog,
- componentProps: {
- serviceType,
- provider: calculateDailyPrices(priceRange.value.max * quantity.value),
- selectedDate: selectedDate.value
- }
- }).onOk(saveFinalOrder)
- }
- async function saveFinalOrder (payloadFinal) {
- let [startHour, endHour] = payloadFinal.slot.split('-')
- if(startHour < 10) {
- startHour = '0' + startHour
- }
- if(endHour < 10) {
- endHour = '0' + endHour
- }
- const payload = {
- client_id: user.user.client.id,
- address_id: address.value?.id,
- quantity: quantity.value,
- date: payloadFinal.date,
- period_type: String(payloadFinal.serviceType.hoursCount),
- start_time: `${startHour}:00`,
- end_time: `${endHour}:00`,
- address_type: selectedOption.value.toLowerCase(),
- service_type_id: selectedServiceType.value,
- description: description.value,
- min_price: priceRange.value.min,
- max_price: priceRange.value.max,
- offers_meal: payloadFinal.meal === 'offer',
- speciality_ids: selectedSpecialties.value
- }
- await createCustomSchedule(payload)
- $q.notify({
- type: 'positive',
- message: 'Pedido sob medida salvo com sucesso!'
- })
- }
- function increaseQuantity () {
- quantity.value++
- }
- function decreaseQuantity () {
- if (quantity.value > 1) quantity.value--
- }
- </script>
- <style scoped lang="scss">
- .sob-medida-page {
- background: #f6f5fb;
- min-height: 100vh;
- padding: 16px;
- }
- .page-shell {
- max-width: 420px;
- margin: 0 auto;
- }
- .page-title {
- display: block;
- text-align: center;
- font-size: 18px;
- font-weight: 700;
- margin: 0 0 20px;
- width: 100%;
- margin-top: 16px;
- }
- .figma-card {
- border-radius: 24px;
- background: #fff;
- padding: 18px;
- margin-bottom: 20px;
- box-shadow: none;
- }
- .card-title,
- .section-title {
- color: #7b61ff;
- font-size: 16px;
- font-weight: 700;
- margin-bottom: 16px;
- }
- .options-grid {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 12px 20px;
- justify-items: center;
- margin-bottom: 12px;
- :deep(.q-radio__label),
- :deep(.q-checkbox__label) {
- color: #000 !important;
- font-size: 14px;
- font-weight: 400;
- }
- :deep(.q-checkbox__inner),
- :deep(.q-radio__inner) {
- color: #a78bfa !important;
- }
- }
- .section-title {
- margin-left: 16px;
- }
- .field-label {
- color: #7b61ff;
- font-size: 13px;
- font-weight: 600;
- margin: 18px 0 10px;
- }
- .optional {
- color: #9e9e9e;
- font-weight: 400;
- }
- .option-col {
- min-width: 0;
- }
- .description-box {
- margin-top: 8px;
- }
- .description-box :deep(textarea) {
- min-height: 90px;
- resize: none;
- }
- /* CARD */
- .compact-card {
- border-radius: 32px;
- padding: 28px 22px;
- background: #fafafa;
- box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06);
- }
- .date-card {
- display: flex;
- justify-content: center;
- align-items: center;
- width: fit-content;
- min-width: 320px;
- max-width: 360px;
- margin: 0 auto 20px;
- padding: 20px;
- border-radius: 24px;
- background: #fff;
- }
- /* container */
- .range-container {
- position: relative;
- padding-top: 58px;
- padding-left: 10px;
- padding-right: 10px;
- }
- /* bolha tipo pin */
- .price-pin {
- position: absolute;
- top: 0;
- width: 46px;
- height: 46px;
- background: linear-gradient(180deg, #8b7cff, #6f57db);
- border-radius: 50% 50% 50% 0;
- transform: translateX(-50%) rotate(-45deg);
- display: flex;
- align-items: center;
- justify-content: center;
- box-shadow: 0 6px 14px rgba(111, 87, 219, 0.28);
- z-index: 5;
- }
- .price-pin span {
- transform: rotate(45deg);
- color: #fff;
- font-size: 13px;
- font-weight: 700;
- }
- /* RANGE */
- .price-range {
- padding: 8px 0 0;
- }
- /* trilha */
- .price-range :deep(.q-slider__track-container) {
- height: 6px;
- border-radius: 30px;
- background: #ddd7ea;
- }
- /* preenchimento */
- .price-range :deep(.q-slider__track) {
- height: 6px;
- border-radius: 30px;
- background: linear-gradient(90deg, #d95cff, #7a5cff);
- }
- /* THUMB REAL FIGMA */
- .price-range :deep(.q-slider__thumb) {
- width: 24px;
- height: 24px;
- min-width: 24px;
- min-height: 24px;
- border-radius: 50%;
- background: #7a5cff !important;
- border: 4px solid #ffffff;
- box-shadow: none;
- }
- /* remove quadrado interno do quasar */
- .price-range :deep(.q-slider__thumb-shape) {
- display: none;
- }
- .price-range :deep(.q-slider__focus-ring) {
- display: none;
- }
- /* texto helper */
- .range-helper {
- margin-top: 18px;
- font-size: 12px;
- line-height: 1.45;
- text-align: center;
- color: #6b6b6b;
- }
- /* transição suave */
- .price-pin,
- .price-range :deep(.q-slider__thumb) {
- transition:
- background 0.25s ease,
- box-shadow 0.25s ease,
- filter 0.25s ease;
- }
- // quantidade
- .quantity-stepper {
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 18px;
- margin: 12px 0 20px;
- }
- .quantity-btn {
- background: #f5f0ff;
- color: #7b61ff;
- width: 38px;
- height: 38px;
- }
- .quantity-value {
- min-width: 40px;
- text-align: center;
- font-size: 22px;
- font-weight: 700;
- }
- .service-type-inline {
- display: grid;
- grid-template-columns: repeat(2, 160px);
- gap: 12px 24px;
- justify-content: center;
- margin: 0 auto 16px;
- }
- /* hover individual do balão */
- .price-pin:hover {
- background: linear-gradient(180deg, #7b68ff, #5f46d8);
- box-shadow: none;
- }
- /* clique individual do balão */
- .price-pin:active {
- background: linear-gradient(180deg, #6d52ff, #5438d6);
- box-shadow: none;
- }
- /* thumb hover individual */
- .price-range :deep(.q-slider__thumb:hover) {
- background: #6d52ff !important;
- box-shadow: 0 0 0 18px rgba(109, 82, 255, 0.22);
- }
- /* thumb selecionado */
- .price-range :deep(.q-slider__thumb:active) {
- background: #5e3fff !important;
- box-shadow: 0 0 0 22px rgba(94, 63, 255, 0.28);
- }
- /* trilha mais viva só quando mouse estiver no slider */
- .price-range:hover :deep(.q-slider__track) {
- filter: brightness(0.95);
- }
- .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;
- }
- }
- }
- /* 🔥 labels pretas */
- :deep(.q-radio__label),
- :deep(.q-checkbox__label) {
- color: #000 !important;
- font-size: 14px;
- font-weight: 400;
- }
- :deep(.q-checkbox__inner),
- :deep(.q-radio__inner) {
- color: #a78bfa !important;
- }
- /* 📱 celular */
- @media (max-width: 480px) {
- .page-shell {
- max-width: 100%;
- }
- .figma-card {
- padding: 16px;
- }
- .figma-date {
- max-width: 100%;
- }
- }
- /* 📲 tablet */
- @media (min-width: 768px) {
- .page-shell {
- max-width: 460px;
- }
- }
- </style>
|