SobMedidaPage.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. <!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
  2. <template>
  3. <q-page class="sob-medida-page">
  4. <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile">
  5. <q-btn flat round dense icon="mdi-chevron-left" color="primary" @click="router.back()" />
  6. <q-space />
  7. <span class="text-subtitle1 text-primary font16 fontbold gradient-diarista">{{ $t('sob_medida.page_title') }}</span>
  8. <q-space />
  9. </div>
  10. <div class="page-shell">
  11. <q-card flat bordered class="figma-card compact-card">
  12. <div class="card-title text-left font16 fontbold gradient-diarista">
  13. {{ $t('sob_medida.your_order') }}
  14. </div>
  15. <div class="field-label text-center gradient-diarista font12 fontbold">
  16. {{ $t('sob_medida.quantity_service') }}
  17. </div>
  18. <div class="quantity-stepper">
  19. <q-btn
  20. round
  21. flat
  22. icon="remove"
  23. class="quantity-btn"
  24. size="sm"
  25. @click="decreaseQuantity"
  26. />
  27. <span class="quantity-value gradient-diarista">
  28. {{ quantity }}
  29. </span>
  30. <q-btn
  31. round
  32. flat
  33. icon="add"
  34. size="sm"
  35. class="quantity-btn"
  36. @click="increaseQuantity"
  37. />
  38. </div>
  39. <div class="options-grid gradient-diarista">
  40. <div
  41. v-for="option in addressTypesOptions"
  42. :key="option.value"
  43. class="option-col"
  44. >
  45. <q-radio
  46. v-model="selectedOption"
  47. :val="option.value"
  48. :label="option.label"
  49. color="purple"
  50. dense
  51. />
  52. </div>
  53. </div>
  54. <div class="field-label text-center gradient-diarista font12 fontbold">
  55. {{ $t('sob_medida.service_type') }}
  56. </div>
  57. <div class="service-type-inline">
  58. <div
  59. v-for="serviceType in serviceTypes"
  60. :key="serviceType.id"
  61. class="option-col"
  62. >
  63. <q-radio
  64. v-model="selectedServiceType"
  65. :val="serviceType.id"
  66. :label="serviceType.description"
  67. color="purple"
  68. dense
  69. />
  70. </div>
  71. </div>
  72. <div class="field-label text-center gradient-diarista font12 fontbold">
  73. {{ $t('sob_medida.preferred_specialty') }}
  74. </div>
  75. <div class="options-grid specialties-grid">
  76. <div
  77. v-for="item in specialties"
  78. :key="item.id"
  79. class="specialty-col"
  80. >
  81. <q-checkbox
  82. v-model="selectedSpecialties"
  83. :val="item.id"
  84. :label="item.description"
  85. color="purple"
  86. dense
  87. />
  88. </div>
  89. </div>
  90. <div class="field-label text-center gradient-diarista">
  91. {{ $t('sob_medida.description_label') }}
  92. <span class="optional">
  93. {{ $t('sob_medida.optional') }}
  94. </span>
  95. </div>
  96. <q-input
  97. v-model="description"
  98. type="textarea"
  99. outlined
  100. bg-color="white"
  101. color="dark"
  102. input-class="text-black"
  103. class="description-box"
  104. :placeholder="$t('sob_medida.description_placeholder')"
  105. />
  106. </q-card>
  107. <div class="section-title gradient-diarista font16 fontbold">
  108. {{ $t('sob_medida.price_range_title') }}
  109. </div>
  110. <q-card flat bordered class="figma-card compact-card">
  111. <div class="range-container">
  112. <div
  113. class="price-pin"
  114. :style="{ left: minPosition + '%' }"
  115. >
  116. <span>{{ priceRange.min }}</span>
  117. </div>
  118. <div
  119. class="price-pin"
  120. :style="{ left: maxPosition + '%' }"
  121. >
  122. <span>{{ priceRange.max }}</span>
  123. </div>
  124. <q-range
  125. v-model="priceRange"
  126. :min="PRICE_LIMITS.min"
  127. :max="PRICE_LIMITS.max"
  128. color="secondary"
  129. class="price-range"
  130. />
  131. </div>
  132. <div class="range-helper">
  133. {{ $t('sob_medida.price_range_helper') }}
  134. </div>
  135. </q-card>
  136. <div class="font16 fontbold section-title gradient-diarista">
  137. {{ $t('sob_medida.date_and_time') }}
  138. </div>
  139. <div class="calendar-custom shadow-card">
  140. <q-date
  141. v-model="selectedDate"
  142. minimal
  143. square
  144. color="purple"
  145. class="full-width figma-date"
  146. :options="dateOptions"
  147. />
  148. </div>
  149. </div>
  150. </q-page>
  151. </template>
  152. <script setup>
  153. import { ref, computed, watch, onMounted } from 'vue'
  154. import { useQuasar, date } from 'quasar'
  155. import { useRouter } from 'vue-router'
  156. import ServiceSelectionSheet from 'src/pages/search/components/ServiceSelectionSheet.vue'
  157. import ServiceTimeSelectionDialog from 'src/pages/search/components/ServiceTimeSelectionDialog.vue'
  158. import { createCustomSchedule } from 'src/api/customSchedules'
  159. import { getPrimaryAddress } from 'src/api/address'
  160. import { getPublicServiceTypes } from 'src/api/serviceTypes'
  161. import { getPublicSpecialties } from 'src/api/specialties'
  162. import {useI18n} from 'vue-i18n'
  163. import { userStore } from 'src/stores/user'
  164. import { calculateDailyPrices } from 'src/helpers/utils'
  165. const $q = useQuasar()
  166. const router = useRouter()
  167. const user = userStore()
  168. const { t } = useI18n()
  169. const serviceTypes = ref([])
  170. const specialties = ref([])
  171. const address = ref(null)
  172. const selectedServiceType = ref(null)
  173. const selectedSpecialties = ref([])
  174. const selectedOption = ref('home')
  175. const description = ref('')
  176. const selectedDate = ref(null)
  177. const quantity = ref(1)
  178. const PRICE_LIMITS = Object.freeze({
  179. min: 100,
  180. max: 500
  181. })
  182. const priceRange = ref({
  183. min: 150,
  184. max: 300
  185. })
  186. const addressTypesOptions = computed(() => [
  187. { label: t('sob_medida.residential'), value: 'home' },
  188. { label: t('sob_medida.commercial'), value: 'commercial' }
  189. ])
  190. const providerPrices = computed(() =>
  191. calculateDailyPrices(priceRange.value.max * quantity.value)
  192. )
  193. const minPosition = computed(() =>
  194. ((priceRange.value.min - PRICE_LIMITS.min) /
  195. (PRICE_LIMITS.max - PRICE_LIMITS.min)) * 100
  196. )
  197. const maxPosition = computed(() =>
  198. ((priceRange.value.max - PRICE_LIMITS.min) /
  199. (PRICE_LIMITS.max - PRICE_LIMITS.min)) * 100
  200. )
  201. const openServiceSelection = () => {
  202. $q.dialog({
  203. component: ServiceSelectionSheet,
  204. componentProps: {
  205. provider: providerPrices.value,
  206. selectedDate: selectedDate.value
  207. }
  208. }).onOk((payload) => {
  209. if (payload?.serviceType) {
  210. openServiceTimeSelection(payload.serviceType)
  211. }
  212. })
  213. }
  214. const openServiceTimeSelection = (serviceType) => {
  215. $q.dialog({
  216. component: ServiceTimeSelectionDialog,
  217. componentProps: {
  218. serviceType,
  219. provider: providerPrices.value,
  220. selectedDate: selectedDate.value
  221. }
  222. }).onOk(saveFinalOrder)
  223. }
  224. const saveFinalOrder = async (payloadFinal) => {
  225. let [startHour, endHour] = payloadFinal.slot.value.split('-')
  226. startHour = String(startHour).padStart(2, '0')
  227. endHour = String(endHour).padStart(2, '0')
  228. const payload = {
  229. client_id: user.user.client.id,
  230. address_id: address.value?.id,
  231. quantity: quantity.value,
  232. date: payloadFinal.date,
  233. period_type: String(payloadFinal.serviceType.hoursCount),
  234. start_time: `${startHour}:00`,
  235. end_time: `${endHour}:00`,
  236. address_type: selectedOption.value,
  237. service_type_id: selectedServiceType.value,
  238. description: description.value,
  239. min_price: priceRange.value.min,
  240. max_price: priceRange.value.max,
  241. offers_meal: payloadFinal.meal === 'offer',
  242. speciality_ids: selectedSpecialties.value
  243. }
  244. await createCustomSchedule(payload)
  245. $q.notify({
  246. type: 'positive',
  247. message: t('sob_medida.success_message')
  248. })
  249. router.push('/#showSuccessModal')
  250. }
  251. const increaseQuantity = () => {
  252. quantity.value++
  253. }
  254. const decreaseQuantity = () => {
  255. if (quantity.value > 1) quantity.value--
  256. }
  257. const dateOptions = (d) => {
  258. const today = date.formatDate(new Date(), 'YYYY/MM/DD')
  259. return d >= today
  260. }
  261. watch(selectedDate, (newDate, oldDate) => {
  262. if (!newDate || newDate === oldDate) return
  263. openServiceSelection()
  264. })
  265. onMounted(async () => {
  266. const { data } = await getPrimaryAddress(user.user.client.id, 'client')
  267. address.value = data.payload
  268. serviceTypes.value = await getPublicServiceTypes()
  269. specialties.value = await getPublicSpecialties()
  270. })
  271. </script>
  272. <style scoped lang="scss">
  273. .sob-medida-page {
  274. background: #f6f5fb;
  275. min-height: 100vh;
  276. padding: 16px;
  277. }
  278. .page-shell {
  279. max-width: 420px;
  280. margin: 0 auto;
  281. }
  282. .page-title {
  283. display: block;
  284. text-align: center;
  285. margin: 0 0 20px;
  286. width: 100%;
  287. margin-top: 16px;
  288. }
  289. .figma-card {
  290. background: #fff;
  291. padding: 18px;
  292. margin-bottom: 20px;
  293. box-shadow: none;
  294. }
  295. .card-title,
  296. .section-title {
  297. color: #7b61ff;
  298. margin-bottom: 16px;
  299. margin-left: 16px;
  300. margin-right: 16px;
  301. }
  302. .options-grid {
  303. display: grid;
  304. grid-template-columns: repeat(2, 1fr);
  305. gap: 12px 20px;
  306. justify-items: center;
  307. margin-bottom: 12px;
  308. :deep(.q-radio__label),
  309. :deep(.q-checkbox__label) {
  310. color: #000 !important;
  311. }
  312. :deep(.q-checkbox__inner),
  313. :deep(.q-radio__inner) {
  314. color: #a78bfa !important;
  315. }
  316. }
  317. .section-title {
  318. margin-left: 16px;
  319. }
  320. .field-label {
  321. color: #7b61ff;
  322. margin: 18px 0 10px;
  323. }
  324. .optional {
  325. color: #9e9e9e;
  326. }
  327. .option-col {
  328. min-width: 0;
  329. }
  330. .description-box {
  331. margin-top: 8px;
  332. }
  333. .description-box :deep(textarea) {
  334. min-height: 90px;
  335. resize: none;
  336. }
  337. .compact-card {
  338. padding: 28px 22px;
  339. background: #fafafa;
  340. box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06);
  341. }
  342. .date-card {
  343. display: flex;
  344. justify-content: center;
  345. align-items: center;
  346. width: fit-content;
  347. min-width: 320px;
  348. max-width: 360px;
  349. margin: 0 auto 20px;
  350. padding: 20px;
  351. border-radius: 24px;
  352. background: #fff;
  353. }
  354. .calendar-custom {
  355. border-radius: 20px;
  356. overflow: hidden;
  357. background: white;
  358. margin: 16px 20px;
  359. }
  360. .range-container {
  361. position: relative;
  362. padding-top: 58px;
  363. padding-left: 10px;
  364. padding-right: 10px;
  365. }
  366. .price-pin {
  367. position: absolute;
  368. top: 0;
  369. width: 46px;
  370. height: 46px;
  371. background: linear-gradient(180deg, #8b7cff, #6f57db);
  372. border-radius: 50% 50% 50% 0;
  373. transform: translateX(-50%) rotate(-45deg);
  374. display: flex;
  375. align-items: center;
  376. justify-content: center;
  377. box-shadow: 0 6px 14px rgba(111, 87, 219, 0.28);
  378. z-index: 5;
  379. }
  380. .price-pin span {
  381. transform: rotate(45deg);
  382. color: #fff;
  383. }
  384. .price-range {
  385. padding: 8px 0 0;
  386. }
  387. .price-range :deep(.q-slider__track-container) {
  388. height: 6px;
  389. border-radius: 30px;
  390. background: #ddd7ea;
  391. }
  392. .price-range :deep(.q-slider__track) {
  393. height: 6px;
  394. border-radius: 30px;
  395. background: linear-gradient(90deg, #d95cff, #7a5cff);
  396. }
  397. .price-range :deep(.q-slider__thumb) {
  398. width: 24px;
  399. height: 24px;
  400. min-width: 24px;
  401. min-height: 24px;
  402. border-radius: 50%;
  403. background: #7a5cff !important;
  404. border: 4px solid #ffffff;
  405. box-shadow: none;
  406. }
  407. .price-range :deep(.q-slider__thumb-shape) {
  408. display: none;
  409. }
  410. .price-range :deep(.q-slider__focus-ring) {
  411. display: none;
  412. }
  413. .range-helper {
  414. margin-top: 18px;
  415. line-height: 1.45;
  416. text-align: center;
  417. color: #6b6b6b;
  418. }
  419. .price-pin,
  420. .price-range :deep(.q-slider__thumb) {
  421. transition:
  422. background 0.25s ease,
  423. box-shadow 0.25s ease,
  424. filter 0.25s ease;
  425. }
  426. .quantity-stepper {
  427. display: flex;
  428. justify-content: center;
  429. align-items: center;
  430. gap: 18px;
  431. margin: 12px 0 20px;
  432. }
  433. .quantity-btn {
  434. background: #f5f0ff;
  435. color: #7b61ff;
  436. width: 28px;
  437. height: 28px;
  438. }
  439. .quantity-value {
  440. min-width: 40px;
  441. text-align: center;
  442. }
  443. .service-type-inline {
  444. display: grid;
  445. grid-template-columns: repeat(2, 160px);
  446. gap: 12px 24px;
  447. justify-content: center;
  448. margin: 0 auto 16px;
  449. }
  450. .specialties-grid {
  451. justify-items: center;
  452. gap: 10px 18px;
  453. margin: 0 auto 16px;
  454. }
  455. .specialty-col {
  456. min-width: 0;
  457. display: flex;
  458. justify-content: center;
  459. }
  460. .specialties-grid :deep(.q-checkbox) {
  461. justify-content: flex-start;
  462. }
  463. .specialties-grid :deep(.q-checkbox__label) {
  464. color: #000 !important;
  465. line-height: 1.2;
  466. white-space: normal;
  467. }
  468. .price-pin:hover {
  469. background: linear-gradient(180deg, #7b68ff, #5f46d8);
  470. box-shadow: none;
  471. }
  472. .price-pin:active {
  473. background: linear-gradient(180deg, #6d52ff, #5438d6);
  474. box-shadow: none;
  475. }
  476. .price-range :deep(.q-slider__thumb:hover) {
  477. background: #6d52ff !important;
  478. box-shadow: 0 0 0 18px rgba(109, 82, 255, 0.22);
  479. }
  480. .price-range :deep(.q-slider__thumb:active) {
  481. background: #5e3fff !important;
  482. box-shadow: 0 0 0 22px rgba(94, 63, 255, 0.28);
  483. }
  484. .price-range:hover :deep(.q-slider__track) {
  485. filter: brightness(0.95);
  486. }
  487. .calendar-custom {
  488. :deep(.q-date) {
  489. background: white;
  490. width: 100%;
  491. }
  492. :deep(.q-date__calendar-item--out) {
  493. color: #CBD5E1 !important;
  494. opacity: 1 !important;
  495. }
  496. :deep(.q-date__calendar-item--passive) {
  497. color: #CBD5E1 !important;
  498. opacity: 1 !important;
  499. }
  500. :deep(.q-date__calendar-days .q-btn.disabled .q-btn__content),
  501. :deep(.q-date__calendar-days .q-btn[disabled] .q-btn__content) {
  502. color: #CBD5E1 !important;
  503. opacity: 1 !important;
  504. }
  505. :deep(.q-date__calendar-days .q-btn.disabled),
  506. :deep(.q-date__calendar-days .q-btn[disabled]) {
  507. opacity: 1 !important;
  508. }
  509. :deep(.q-date__calendar-days .q-btn__content) {
  510. font-weight: 500;
  511. color: #1E293B !important;
  512. }
  513. :deep(.q-date__calendar-weekdays > div) {
  514. color: #6366F1;
  515. font-weight: 700;
  516. opacity: 0.8;
  517. }
  518. :deep(.q-date__navigation) {
  519. .q-btn { color: #6366F1 !important; }
  520. .q-btn__content { color: #6366F1 !important; }
  521. }
  522. :deep(.q-date__event) {
  523. bottom: 4px;
  524. height: 6px;
  525. width: 6px;
  526. border-radius: 50%;
  527. }
  528. :deep(.q-date__today) {
  529. .q-btn__content {
  530. color: #7c4dff !important;
  531. background: #7c4dff15;
  532. border-radius: 50%;
  533. }
  534. }
  535. :deep(.q-date__selected) {
  536. .q-btn__content {
  537. background: #6366F1 !important;
  538. color: white !important;
  539. border-radius: 50%;
  540. box-shadow: 0 4px 10px rgba(99, 102, 241, 0.4);
  541. }
  542. }
  543. :deep(.q-date__view--months .q-btn),
  544. :deep(.q-date__view--years .q-btn) {
  545. color: #6366F1 !important;
  546. }
  547. }
  548. /* labels pretas */
  549. :deep(.q-radio__label),
  550. :deep(.q-checkbox__label) {
  551. color: #000 !important;
  552. }
  553. :deep(.q-checkbox__inner),
  554. :deep(.q-radio__inner) {
  555. color: #a78bfa !important;
  556. }
  557. @media (max-width: 480px) {
  558. .page-shell {
  559. max-width: 100%;
  560. }
  561. .figma-card {
  562. padding: 16px;
  563. }
  564. .figma-date {
  565. max-width: 100%;
  566. }
  567. }
  568. @media (min-width: 768px) {
  569. .page-shell {
  570. max-width: 460px;
  571. }
  572. }
  573. </style>