SchedulingDialog.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871
  1. <template>
  2. <q-dialog
  3. ref="dialogRef"
  4. maximized
  5. transition-hide="slide-down"
  6. transition-show="slide-up"
  7. >
  8. <div class="dialog-root">
  9. <div
  10. class="dialog-header row items-center q-px-md q-pt-md q-pb-sm bg-white"
  11. >
  12. <q-btn
  13. v-close-popup
  14. class="header-back-btn"
  15. color="primary"
  16. dense
  17. flat
  18. icon="mdi-chevron-left"
  19. round
  20. />
  21. <div
  22. class="col text-center font16 fontbold text-primary gradient-diarista q-mb-xs"
  23. >
  24. {{ $t("scheduling_page.title") }}
  25. </div>
  26. <div style="width: 36px"></div>
  27. </div>
  28. <div class="dialog-body">
  29. <div class="q-px-md q-pt-md">
  30. <div class="font16 fontbold gradient-diarista q-mb-xs">
  31. {{ $t("scheduling_page.about_provider") }}
  32. </div>
  33. <q-card
  34. class="card-border shadow-card bg-surface text-text"
  35. :flat="false"
  36. >
  37. <q-card-section class="q-pa-md">
  38. <div class="row items-center no-wrap q-gutter-x-md">
  39. <div class="col-2">
  40. <q-avatar
  41. :style="
  42. avatarColors[
  43. providerInfo.provider_id % avatarColors.length
  44. ]
  45. "
  46. >
  47. <img
  48. v-if="getProfileMediaUrl(providerInfo)"
  49. style="object-fit: cover; border-radius: 50%"
  50. :src="getProfileMediaUrl(providerInfo)"
  51. />
  52. <span v-else>
  53. {{
  54. getFirstName(providerInfo.provider_name)
  55. ?.slice(0, 2)
  56. .toUpperCase() ?? "??"
  57. }}
  58. </span>
  59. </q-avatar>
  60. </div>
  61. <div class="col-3 column">
  62. <div class="font12 fontbold text-text">
  63. {{ getFirstName(providerInfo?.provider_name) ?? "—" }}
  64. </div>
  65. <div class="font10 fontmedium text-grey-7">
  66. {{ providerDistrict ?? "—" }}
  67. </div>
  68. </div>
  69. <div class="col-3">
  70. <div class="row items-center q-gutter-x-md q-mt-xs">
  71. <div class="row items-center">
  72. <q-icon
  73. color="warning"
  74. name="mdi-star"
  75. size="12px"
  76. />
  77. <span class="font9 fontmedium q-ml-xs">
  78. {{
  79. (providerInfo?.average_rating != null
  80. ? Number(providerInfo.average_rating).toFixed(1)
  81. : "") +
  82. " (" +
  83. (providerInfo?.total_reviews ?? 0) +
  84. ")"
  85. }}
  86. </span>
  87. </div>
  88. <div class="row items-center">
  89. <q-icon
  90. color="secondary"
  91. name="mdi-broom"
  92. size="14px"
  93. />
  94. <span class="font9 fontmedium q-ml-xs">
  95. {{ providerInfo?.total_services ?? 0 }}
  96. </span>
  97. </div>
  98. </div>
  99. </div>
  100. <div class="col-2 column items-center q-gutter-y-xs">
  101. <q-btn
  102. color="pink-4"
  103. dense
  104. flat
  105. round
  106. size="sm"
  107. :icon="isFavorite ? 'mdi-heart' : 'mdi-heart-outline'"
  108. :loading="favoriteLoading"
  109. @click="toggleFavorite"
  110. />
  111. <q-btn
  112. color="grey-5"
  113. dense
  114. flat
  115. icon="mdi-information-outline"
  116. round
  117. size="sm"
  118. @click.stop="openProviderInfo"
  119. />
  120. </div>
  121. </div>
  122. </q-card-section>
  123. </q-card>
  124. </div>
  125. <div class="q-px-md q-pt-lg">
  126. <div class="font16 fontbold gradient-diarista q-mb-xs">
  127. {{ $t("scheduling_page.schedule_service") }}
  128. </div>
  129. <div
  130. v-if="loadingAvailability || loadingProvider"
  131. class="row items-center justify-center q-py-lg"
  132. >
  133. <q-spinner-dots
  134. color="primary"
  135. size="36px"
  136. />
  137. </div>
  138. <div
  139. v-else
  140. class="calendar-wrapper shadow-card q-mb-md"
  141. >
  142. <q-date
  143. v-model="selectedDate"
  144. class="full-width"
  145. minimal
  146. square
  147. :first-day-of-week="0"
  148. :options="dateOptions"
  149. @update:model-value="onDateSelected"
  150. />
  151. </div>
  152. </div>
  153. <div class="q-px-md q-pt-sm q-pb-xl">
  154. <div class="row items-center justify-between q-mb-sm">
  155. <div class="font16 fontbold gradient-diarista">
  156. {{ $t("scheduling_page.reviews_title") }}
  157. </div>
  158. <span class="text-text cursor-pointer">
  159. {{ $t("scheduling_page.see_all") }}
  160. <q-icon
  161. class="text-text"
  162. name="mdi-chevron-right"
  163. />
  164. </span>
  165. </div>
  166. <div
  167. v-if="loadingReviews"
  168. class="row items-center justify-center q-py-md"
  169. >
  170. <q-spinner-dots
  171. color="primary"
  172. size="36px"
  173. />
  174. </div>
  175. <div
  176. v-else-if="reviews.length === 0"
  177. class="text-center text-grey-6 text-body2 q-py-md"
  178. >
  179. {{ $t("scheduling_page.no_reviews") }}
  180. </div>
  181. <div
  182. v-else
  183. class="reviews-scroll"
  184. >
  185. <q-card
  186. v-for="review in reviews"
  187. :key="review.id"
  188. :flat="false"
  189. class="review-card card-border bg-white q-mr-sm shadow-card"
  190. >
  191. <q-card-section class="q-pa-sm">
  192. <div class="row items-center no-wrap q-gutter-x-sm q-mb-xs">
  193. <q-avatar
  194. :style="
  195. avatarColors[
  196. review.schedule?.client?.id % avatarColors.length
  197. ]
  198. "
  199. >
  200. <img
  201. v-if="review.schedule?.client?.profile_photo"
  202. style="object-fit: cover; border-radius: 50%"
  203. :src="review.schedule?.client?.profile_photo"
  204. />
  205. <span v-else>
  206. {{ getFirstName(review.schedule?.client?.name)?.slice(0, 1) ?? "—" }}
  207. </span>
  208. </q-avatar>
  209. <div class="col-3 text-text font10 fontbold">
  210. {{
  211. getFirstName(review.schedule?.client?.name) ??
  212. $t("scheduling_page.unknown_client")
  213. }}
  214. </div>
  215. <div class="row items-center q-mb-xs q-my-auto">
  216. <q-icon
  217. v-for="s in 5"
  218. :key="s"
  219. :name="s <= review.stars ? 'mdi-star' : 'mdi-star-outline'"
  220. color="warning"
  221. size="14px"
  222. />
  223. </div>
  224. </div>
  225. <div class="font9 fontregular text-text review-comment">
  226. {{ review.comment ?? "--------" }}
  227. </div>
  228. </q-card-section>
  229. </q-card>
  230. </div>
  231. </div>
  232. </div>
  233. </div>
  234. </q-dialog>
  235. </template>
  236. <script setup>
  237. import { avatarColors } from "src/helpers/avatarColors";
  238. import { computed, onMounted, ref } from "vue";
  239. import {
  240. createClientFavoriteProvider,
  241. deleteClientFavoriteProvider,
  242. getClientFavoriteProviders,
  243. } from "src/api/clientFavoriteProvider";
  244. import { date, useDialogPluginComponent, useQuasar } from "quasar";
  245. import { getClientProviderBlocks } from "src/api/schedule";
  246. import { getFirstName } from "src/helpers/utils";
  247. import { getPerfilPrestador } from "src/api/dashboard";
  248. import { getProfileMediaUrl } from "src/helpers/profileMedia";
  249. import {
  250. getProviderBlockedDays,
  251. getProviderWorkingDays,
  252. } from "src/api/providerAvailability";
  253. import { getProviderReceivedReviews } from "src/api/review";
  254. import { useI18n } from "vue-i18n";
  255. import { useRouter } from "vue-router";
  256. import { userStore } from "src/stores/user";
  257. import { useServicePackageStore } from "src/stores/servicePackage";
  258. import ServicePackageSummaryDialog from "./ServicePackageSummaryDialog.vue";
  259. import ServiceSelectionSheet from "./ServiceSelectionSheet.vue";
  260. import ServiceTimeSelectionDialog from "./ServiceTimeSelectionDialog.vue";
  261. import ProfileInfoDialog from "src/components/profile/ProfileInfoDialog.vue";
  262. defineEmits([...useDialogPluginComponent.emits]);
  263. const props = defineProps({
  264. provider: { required: true, type: Object },
  265. });
  266. const { t } = useI18n();
  267. const $q = useQuasar();
  268. const router = useRouter();
  269. const { dialogRef } = useDialogPluginComponent();
  270. const servicePackage = useServicePackageStore();
  271. const store = userStore();
  272. const providerInfo = ref({ ...props.provider });
  273. const blockedDays = ref([]);
  274. const bookings = ref([]);
  275. const favoriteId = ref(null);
  276. const favoriteLoading = ref(false);
  277. const isFavorite = ref(false);
  278. const loadingAvailability = ref(true);
  279. const loadingProvider = ref(true);
  280. const loadingReviews = ref(true);
  281. const providerClientBlocks = ref({
  282. existing_schedules: [],
  283. fully_blocked_weeks: [],
  284. });
  285. const reviews = ref([]);
  286. const selectedDate = ref(null);
  287. const workingDays = ref([]);
  288. const availableWeekDays = computed(() => [
  289. ...new Set(workingDays.value.map((wd) => wd.day)),
  290. ]);
  291. const providerDistrict = computed(
  292. () => providerInfo.value?.district ?? providerInfo.value?.provider_district,
  293. );
  294. const blockedDateSet = computed(
  295. () =>
  296. new Set(
  297. blockedDays.value
  298. .filter((bd) => bd.period === "all")
  299. .map((bd) => bd.date),
  300. ),
  301. );
  302. const blockedWeekStartSet = computed(
  303. () => new Set(providerClientBlocks.value.fully_blocked_weeks ?? []),
  304. );
  305. const normalizeDate = (dateStr) => (dateStr ?? "").replace(/\//g, "-");
  306. const getWeekStart = (dateStr) => {
  307. const normalizedDate = normalizeDate(dateStr);
  308. const d = new Date(`${normalizedDate}T12:00:00`);
  309. d.setDate(d.getDate() - d.getDay());
  310. return d.toISOString().split("T")[0];
  311. };
  312. const cartHasAnotherProvider = () =>
  313. servicePackage.items.length > 0 &&
  314. Number(servicePackage.items[0].provider_id) !==
  315. Number(providerInfo.value.provider_id);
  316. const getServicePackageWeekCount = (selectedDate) => {
  317. const weekStart = getWeekStart(selectedDate);
  318. return servicePackage.items.filter(
  319. (item) =>
  320. Number(item.provider_id) === Number(providerInfo.value.provider_id) &&
  321. getWeekStart(item.date) === weekStart,
  322. ).length;
  323. };
  324. const getLocalWeekCount = (selectedDate) => {
  325. const weekStart = getWeekStart(selectedDate);
  326. return bookings.value.filter(
  327. (booking) => getWeekStart(booking.date) === weekStart,
  328. ).length;
  329. };
  330. const getServerWeekCount = (selectedDate) => {
  331. const weekStart = getWeekStart(selectedDate);
  332. return (providerClientBlocks.value.existing_schedules ?? []).filter(
  333. (schedule) => getWeekStart(schedule.date) === weekStart,
  334. ).length;
  335. };
  336. const wouldExceedWeekLimit = (selectedDate) => {
  337. const servicePackageCount = getServicePackageWeekCount(selectedDate);
  338. const localCount = getLocalWeekCount(selectedDate);
  339. const serverCount = getServerWeekCount(selectedDate);
  340. return serverCount + localCount + servicePackageCount >= 2;
  341. };
  342. const dateOptions = (d) => {
  343. const today = date.formatDate(new Date(), "YYYY/MM/DD");
  344. if (d < today) return false;
  345. const raw = normalizeDate(d);
  346. const parsed = new Date(`${raw}T12:00:00`);
  347. const dayOfWeek = parsed.getDay();
  348. const isWorkingDay = availableWeekDays.value.includes(dayOfWeek);
  349. const isBlocked = blockedDateSet.value.has(raw);
  350. const isWeekBlocked = blockedWeekStartSet.value.has(getWeekStart(raw));
  351. if (!isWorkingDay || isBlocked || isWeekBlocked) return false;
  352. if (wouldExceedWeekLimit(raw)) return false;
  353. return true;
  354. };
  355. const formatHour = (h) => `${String(h).padStart(2, "0")}:00`;
  356. const loadAvailability = async () => {
  357. loadingAvailability.value = true;
  358. try {
  359. const clientId = store.user?.client?.id;
  360. const defaultClientBlocks = {
  361. existing_schedules: [],
  362. fully_blocked_weeks: [],
  363. };
  364. const [wd, bd, clientBlocks] = await Promise.all([
  365. getProviderWorkingDays(providerInfo.value.provider_id),
  366. getProviderBlockedDays(providerInfo.value.provider_id),
  367. clientId
  368. ? getClientProviderBlocks(clientId, providerInfo.value.provider_id)
  369. : Promise.resolve(defaultClientBlocks),
  370. ]);
  371. blockedDays.value = bd ?? [];
  372. providerClientBlocks.value = clientBlocks ?? defaultClientBlocks;
  373. workingDays.value = wd ?? [];
  374. } catch {
  375. blockedDays.value = [];
  376. providerClientBlocks.value = {
  377. existing_schedules: [],
  378. fully_blocked_weeks: [],
  379. };
  380. workingDays.value = [];
  381. } finally {
  382. loadingAvailability.value = false;
  383. }
  384. };
  385. const loadProviderProfile = async () => {
  386. const providerId = props.provider?.provider_id ?? props.provider?.id;
  387. if (!providerId) {
  388. loadingProvider.value = false;
  389. return;
  390. }
  391. loadingProvider.value = true;
  392. try {
  393. const payload = await getPerfilPrestador(providerId);
  394. providerInfo.value = { ...props.provider, ...payload };
  395. } catch {
  396. providerInfo.value = { ...props.provider };
  397. } finally {
  398. loadingProvider.value = false;
  399. }
  400. const hasAnyPrice = [
  401. "daily_price_8h",
  402. "daily_price_6h",
  403. "daily_price_4h",
  404. "daily_price_2h",
  405. ].some((key) => providerInfo.value?.[key] != null);
  406. if (!hasAnyPrice) {
  407. $q.notify({
  408. message: t("scheduling_page.provider_unavailable"),
  409. type: "warning",
  410. });
  411. dialogRef.value?.hide();
  412. }
  413. };
  414. const loadFavoriteState = async () => {
  415. const clientId = store.user?.client?.id;
  416. if (!clientId) return;
  417. try {
  418. const favorites = await getClientFavoriteProviders(clientId);
  419. const match = (favorites ?? []).find(
  420. (f) => Number(f.provider_id) === Number(providerInfo.value.provider_id),
  421. );
  422. isFavorite.value = !!match;
  423. favoriteId.value = match?.id ?? null;
  424. } catch {
  425. isFavorite.value = false;
  426. favoriteId.value = null;
  427. }
  428. };
  429. const loadReviews = async () => {
  430. loadingReviews.value = true;
  431. try {
  432. const all = await getProviderReceivedReviews(providerInfo.value.provider_id);
  433. reviews.value = (all ?? []).slice(0, 10);
  434. } catch {
  435. reviews.value = [];
  436. } finally {
  437. loadingReviews.value = false;
  438. }
  439. };
  440. const onDateSelected = (val) => {
  441. if (!val) return;
  442. selectedDate.value = null;
  443. const valFormatted = normalizeDate(val);
  444. const blocksOfDate = blockedDays.value.filter(
  445. (bd) => bd.date === valFormatted && bd.period !== "all",
  446. );
  447. const dayOfWeek = new Date(`${valFormatted}T12:00:00`).getDay();
  448. const dayPeriods = workingDays.value
  449. .filter((wd) => wd.day === dayOfWeek)
  450. .map((wd) => wd.period);
  451. const workingDayBlocks = [];
  452. if (!dayPeriods.includes("afternoon")) {
  453. workingDayBlocks.push({ init_hour: "14:00:00", end_hour: "20:00:00" });
  454. }
  455. if (!dayPeriods.includes("morning")) {
  456. workingDayBlocks.push({ init_hour: "07:00:00", end_hour: "13:00:00" });
  457. }
  458. const localBookingBlocks = bookings.value
  459. .filter((booking) => booking.date.replace(/\//g, "-") === valFormatted)
  460. .map((booking) => ({
  461. end_hour: `${booking.slot.endHour}:00:00`,
  462. init_hour: `${booking.slot.startHour}:00:00`,
  463. }));
  464. const servicePackageBookingBlocks = servicePackage.items
  465. .filter(
  466. (item) =>
  467. Number(item.provider_id) === Number(providerInfo.value.provider_id) &&
  468. normalizeDate(item.date) === valFormatted,
  469. )
  470. .map((item) => ({
  471. end_hour: item.end_time,
  472. init_hour: item.start_time,
  473. }));
  474. const serverBookingBlocks = (
  475. providerClientBlocks.value.existing_schedules ?? []
  476. )
  477. .filter((schedule) => normalizeDate(schedule.date) === valFormatted)
  478. .map((schedule) => ({
  479. end_hour: schedule.end_time,
  480. init_hour: schedule.start_time,
  481. }));
  482. const partialBlocks = [
  483. ...blocksOfDate,
  484. ...workingDayBlocks,
  485. ...localBookingBlocks,
  486. ...serverBookingBlocks,
  487. ...servicePackageBookingBlocks,
  488. ];
  489. $q.dialog({
  490. component: ServiceSelectionSheet,
  491. componentProps: {
  492. partialBlocks,
  493. provider: providerInfo.value,
  494. selectedDate: val,
  495. scheduleType: 'default'
  496. },
  497. }).onOk(({ action, serviceType, date: date_, provider: prov }) => {
  498. $q.dialog({
  499. component: ServiceTimeSelectionDialog,
  500. componentProps: {
  501. partialBlocks,
  502. provider: prov,
  503. selectedDate: date_,
  504. serviceType,
  505. },
  506. }).onOk((booking) => {
  507. if (action === "servicePackage") {
  508. if (cartHasAnotherProvider()) {
  509. $q.notify({
  510. message: t("service_package.other_provider_blocked"),
  511. type: "warning",
  512. timeout: 10000,
  513. progress: true,
  514. actions: [
  515. { icon: "close", color: "white", round: true, dense: true },
  516. ],
  517. });
  518. return;
  519. }
  520. servicePackage.addItem({
  521. date: normalizeDate(booking.date),
  522. end_time: formatHour(booking.slot.endHour),
  523. local_id: `${prov.provider_id}-${booking.date}-${booking.slot.value}-${Date.now()}`,
  524. offers_meal:
  525. booking.meal === "offer"
  526. ? true
  527. : booking.meal === "no_offer"
  528. ? false
  529. : null,
  530. period_type: booking.serviceType.hoursCount,
  531. provider_id: prov.provider_id,
  532. provider_name: prov.provider_name,
  533. provider_photo: getProfileMediaUrl(prov),
  534. start_time: formatHour(booking.slot.startHour),
  535. base_amount:
  536. booking.serviceType.basePrice ?? booking.serviceType.price,
  537. });
  538. if (!servicePackage.provider) {
  539. servicePackage.setProvider({ ...providerInfo.value });
  540. }
  541. $q.notify({
  542. message: t("service_package.add_success"),
  543. type: "positive",
  544. });
  545. dialogRef.value.hide();
  546. router.push({ name: "ScheduleCartPage" });
  547. return;
  548. }
  549. bookings.value.push(booking);
  550. $q.dialog({
  551. component: ServicePackageSummaryDialog,
  552. componentProps: { initialBooking: booking, provider: providerInfo.value },
  553. }).onOk(() => {
  554. dialogRef.value.hide();
  555. });
  556. });
  557. });
  558. };
  559. const openProviderInfo = () => {
  560. $q.dialog({
  561. component: ProfileInfoDialog,
  562. componentProps: {
  563. provider: providerInfo.value,
  564. },
  565. });
  566. };
  567. const toggleFavorite = async () => {
  568. if (favoriteLoading.value) return;
  569. const clientId = store.user?.client?.id;
  570. if (!clientId) {
  571. $q.notify({
  572. message: t("scheduling_page.favorite_error"),
  573. type: "negative",
  574. });
  575. return;
  576. }
  577. favoriteLoading.value = true;
  578. try {
  579. if (isFavorite.value) {
  580. if (favoriteId.value) {
  581. await deleteClientFavoriteProvider(favoriteId.value);
  582. }
  583. favoriteId.value = null;
  584. isFavorite.value = false;
  585. $q.notify({
  586. message: t("scheduling_page.favorite_removed"),
  587. type: "positive",
  588. });
  589. } else {
  590. const created = await createClientFavoriteProvider({
  591. client_id: clientId,
  592. provider_id: providerInfo.value.provider_id,
  593. });
  594. favoriteId.value = created?.id ?? null;
  595. isFavorite.value = true;
  596. $q.notify({
  597. message: t("scheduling_page.favorite_added"),
  598. type: "positive",
  599. });
  600. }
  601. } catch {
  602. $q.notify({
  603. message: t("scheduling_page.favorite_error"),
  604. type: "negative",
  605. });
  606. } finally {
  607. favoriteLoading.value = false;
  608. }
  609. };
  610. onMounted(() => {
  611. loadProviderProfile();
  612. loadAvailability();
  613. loadFavoriteState();
  614. loadReviews();
  615. });
  616. </script>
  617. <style scoped lang="scss">
  618. .dialog-root {
  619. width: 100vw;
  620. max-width: 100vw;
  621. height: 100vh;
  622. max-height: 100vh;
  623. display: flex;
  624. flex-direction: column;
  625. overflow: hidden;
  626. background: #f9fafb;
  627. }
  628. .dialog-header {
  629. flex-shrink: 0;
  630. box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
  631. }
  632. .dialog-body {
  633. flex: 1;
  634. min-height: 0;
  635. overflow-y: auto;
  636. overflow-x: clip;
  637. }
  638. .calendar-wrapper {
  639. border-radius: 20px;
  640. overflow: hidden;
  641. background: white;
  642. :deep(.q-date) {
  643. background: white;
  644. width: 100%;
  645. }
  646. :deep(.q-date__calendar-days .q-btn__content) {
  647. color: #1e293b !important;
  648. }
  649. // dias desabilitados: visíveis mas opacos
  650. :deep(.q-date__calendar-days .q-btn.disabled .q-btn__content),
  651. :deep(.q-date__calendar-days .q-btn[disabled] .q-btn__content) {
  652. color: #000000 !important;
  653. opacity: 1 !important;
  654. }
  655. // o Quasar aplica opacity no elemento .q-btn quando disabled — reseta
  656. :deep(.q-date__calendar-days .q-btn.disabled),
  657. :deep(.q-date__calendar-days .q-btn[disabled]) {
  658. opacity: 1 !important;
  659. }
  660. // cabeçalho dos dias (dom, seg, ter, qua...)
  661. :deep(.q-date__calendar-weekdays > div) {
  662. color: #6366f1;
  663. font-weight: 700;
  664. opacity: 0.8;
  665. }
  666. :deep(.q-date__navigation) {
  667. .q-btn {
  668. color: #6366f1 !important;
  669. }
  670. .q-btn__content {
  671. color: #6366f1 !important;
  672. }
  673. }
  674. :deep(.q-date__event) {
  675. bottom: 4px;
  676. height: 6px;
  677. width: 6px;
  678. border-radius: 50%;
  679. }
  680. :deep(.q-date__today .q-btn__content) {
  681. color: #7c4dff !important;
  682. background: #7c4dff15;
  683. border-radius: 50%;
  684. }
  685. :deep(.q-date__selected .q-btn__content),
  686. :deep(.q-date__calendar-item .q-btn.q-date__selected .q-btn__content) {
  687. background: #6366f1 !important;
  688. color: #ffffff !important;
  689. border-radius: 50%;
  690. box-shadow: 0 4px 10px rgba(99, 102, 241, 0.4);
  691. }
  692. :deep(.q-date__view--months .q-btn),
  693. :deep(.q-date__view--years .q-btn) {
  694. color: #6366f1 !important;
  695. }
  696. :deep(.q-date__calendar-item--out) {
  697. color: #b9b9b9 !important;
  698. opacity: 0.8 !important;
  699. }
  700. }
  701. // Reviews scroll horizontal
  702. .reviews-scroll {
  703. display: flex;
  704. flex-direction: row;
  705. overflow-x: auto;
  706. -webkit-overflow-scrolling: touch;
  707. scrollbar-width: none;
  708. padding-bottom: 8px;
  709. &::-webkit-scrollbar {
  710. display: none;
  711. }
  712. }
  713. .review-card {
  714. flex-shrink: 0;
  715. min-width: 220px;
  716. max-width: 240px;
  717. border-radius: 12px;
  718. margin-right: 8px;
  719. }
  720. .review-comment {
  721. display: -webkit-box;
  722. -webkit-line-clamp: 3;
  723. line-clamp: 3;
  724. -webkit-box-orient: vertical;
  725. overflow: hidden;
  726. }
  727. .min-width-0 {
  728. min-width: 0;
  729. }
  730. </style>