ServiceSelectionSheet.vue 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. <template>
  2. <q-dialog
  3. ref="dialogRef"
  4. position="bottom"
  5. @hide="onDialogHide"
  6. >
  7. <q-card class="bg-surface text-text full-width sheet-card">
  8. <q-card-section class="row items-center q-pb-none">
  9. <q-space />
  10. <q-btn
  11. color="grey-6"
  12. dense
  13. flat
  14. icon="mdi-close-circle-outline"
  15. round
  16. @click="onDialogCancel"
  17. />
  18. </q-card-section>
  19. <q-separator class="q-mt-sm" />
  20. <q-card-section class="q-pt-sm q-pb-md">
  21. <div v-if="availableServiceTypes.length === 0" class="text-center text-grey-6 text-body2 q-py-md">
  22. {{ $t('scheduling_page.no_slots_available') }}
  23. </div>
  24. <div
  25. v-for="type in availableServiceTypes"
  26. :key="type.key"
  27. class="row items-center no-wrap q-py-sm"
  28. >
  29. <div class="col">
  30. <div class="row items-center no-wrap q-gutter-x-xs">
  31. <span class="font14 fontbold text-text">
  32. {{ type.label }}
  33. </span>
  34. <q-btn
  35. color="primary"
  36. dense
  37. flat
  38. icon="mdi-information-outline"
  39. size="xs"
  40. @click="openInfo(type)"
  41. />
  42. </div>
  43. <div class="font12 fontmedium text-grey-6">
  44. {{ type.hours }}
  45. </div>
  46. </div>
  47. <div class="font16 fontbold text-text q-mx-md" style="white-space: nowrap;">
  48. {{ type.price != null ? formatPrice(type.price) : $t('scheduling_page.no_price') }}
  49. </div>
  50. <div class="row no-wrap q-gutter-x-xs">
  51. <q-btn
  52. color="secondary"
  53. no-caps
  54. padding="2px 12px"
  55. rounded
  56. size="md"
  57. unelevated
  58. :disable="type.price == null"
  59. :label="$t('scheduling_page.book')"
  60. @click="onDialogOK({ action: 'book', serviceType: type, date: selectedDate, provider })"
  61. />
  62. <q-btn
  63. color="primary"
  64. icon="mdi-cart-plus"
  65. outline
  66. round
  67. size="md"
  68. :disable="type.price == null"
  69. @click="onDialogOK({ action: 'cart', serviceType: type, date: selectedDate, provider })"
  70. >
  71. <q-tooltip>
  72. {{ $t('schedule_cart.add_to_cart') }}
  73. </q-tooltip>
  74. </q-btn>
  75. </div>
  76. </div>
  77. </q-card-section>
  78. </q-card>
  79. </q-dialog>
  80. </template>
  81. <script setup>
  82. import { computed } from 'vue';
  83. import { useDialogPluginComponent, useQuasar } from 'quasar';
  84. import { useI18n } from 'vue-i18n';
  85. import ServiceTypeInfoDialog from './ServiceTypeInfoDialog.vue';
  86. const props = defineProps({
  87. partialBlocks: { type: Array, required: false, default: () => [] },
  88. provider: { type: Object, required: true },
  89. selectedDate: { type: String, required: true },
  90. });
  91. defineEmits([...useDialogPluginComponent.emits]);
  92. const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
  93. const $q = useQuasar();
  94. const { t } = useI18n();
  95. const availableServiceTypes = computed(() =>
  96. [
  97. {
  98. description: t('scheduling_page.service_types.integral.description'),
  99. hours: t('scheduling_page.service_types.integral.hours'),
  100. hoursCount: 8,
  101. key: 'integral',
  102. label: t('scheduling_page.service_types.integral.label'),
  103. price: props.provider?.daily_price_8h ?? null,
  104. },
  105. {
  106. description: t('scheduling_page.service_types.padrao.description'),
  107. hours: t('scheduling_page.service_types.padrao.hours'),
  108. hoursCount: 6,
  109. key: 'padrao',
  110. label: t('scheduling_page.service_types.padrao.label'),
  111. price: props.provider?.daily_price_6h ?? null,
  112. },
  113. {
  114. description: t('scheduling_page.service_types.meio_periodo.description'),
  115. hours: t('scheduling_page.service_types.meio_periodo.hours'),
  116. hoursCount: 4,
  117. key: 'meio_periodo',
  118. label: t('scheduling_page.service_types.meio_periodo.label'),
  119. price: props.provider?.daily_price_4h ?? null,
  120. },
  121. {
  122. description: t('scheduling_page.service_types.diaria_rapida.description'),
  123. hours: t('scheduling_page.service_types.diaria_rapida.hours'),
  124. hoursCount: 2,
  125. key: 'diaria_rapida',
  126. label: t('scheduling_page.service_types.diaria_rapida.label'),
  127. price: props.provider?.daily_price_2h ?? null,
  128. },
  129. ].filter(type => hasValidSlots(type.hoursCount))
  130. );
  131. const formatPrice = (value) =>
  132. Number(value).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
  133. const hasValidSlots = (hoursCount) => {
  134. for (let s = 7; s + hoursCount <= 20; s++) {
  135. if (!slotConflicts(s, s + hoursCount, props.partialBlocks)) return true;
  136. }
  137. return false;
  138. };
  139. const openInfo = (type) => {
  140. $q.dialog({ component: ServiceTypeInfoDialog, componentProps: { serviceType: type } });
  141. };
  142. const slotConflicts = (slotStart, slotEnd, blocks) =>
  143. blocks.some(b => {
  144. const blockEnd = parseInt(b.end_hour);
  145. const blockStart = parseInt(b.init_hour);
  146. return slotEnd >= blockStart && slotStart <= blockEnd;
  147. });
  148. </script>
  149. <style scoped lang="scss">
  150. .sheet-card {
  151. border-radius: 20px 20px 0 0;
  152. }
  153. </style>