ProfileFavoritesDialog.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. <!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
  2. <template>
  3. <q-dialog
  4. ref="dialogRef"
  5. maximized
  6. persistent
  7. transition-hide="slide-right"
  8. transition-show="slide-left"
  9. >
  10. <div class="bg-page full-height column no-shadow">
  11. <div
  12. class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface"
  13. >
  14. <q-btn
  15. v-close-popup
  16. class="header-back-btn"
  17. color="primary"
  18. dense
  19. flat
  20. icon="mdi-chevron-left"
  21. round
  22. />
  23. <q-space />
  24. <span
  25. class="text-subtitle1 text-primary font16 fontbold gradient-diarista"
  26. >
  27. {{ $t("profile.favorites.title") }}
  28. </span>
  29. <q-space />
  30. <div style="width: 32px" />
  31. </div>
  32. <div
  33. v-if="loading"
  34. class="col flex flex-center"
  35. >
  36. <q-spinner
  37. color="primary"
  38. size="3em"
  39. />
  40. </div>
  41. <div
  42. v-else-if="favorites.length === 0"
  43. class="col column items-center justify-center q-px-xl q-pb-xl"
  44. >
  45. <q-img
  46. class="q-mb-lg"
  47. fit="contain"
  48. style="width: 220px; height: 220px"
  49. :src="diarinho"
  50. />
  51. <p class="text-text text-center q-mb-xs">
  52. {{ $t("profile.favorites.empty_message") }}
  53. </p>
  54. <p class="text-primary text-center q-mb-xl">
  55. {{ $t("profile.favorites.empty_cta") }}
  56. </p>
  57. <q-btn
  58. v-close-popup
  59. class="full-width q-py-sm"
  60. color="primary"
  61. no-caps
  62. padding="10px 16px"
  63. rounded
  64. unelevated
  65. :label="$t('profile.favorites.search_btn')"
  66. @click="goToSearch"
  67. />
  68. <q-btn
  69. class="full-width q-py-sm q-mt-sm"
  70. color="primary"
  71. no-caps
  72. outline
  73. padding="10px 16px"
  74. rounded
  75. :label="$t('profile.favorites.code_btn')"
  76. @click="openCodeDialog"
  77. />
  78. </div>
  79. <div
  80. v-else
  81. class="col overflow-auto q-pb-xl"
  82. >
  83. <div class="q-mx-md q-mt-md q-pa-md">
  84. <p class="gradient-diarista text-center q-mb-md font12 fontbold">
  85. {{ $t("profile.favorites.indicate_title") }}
  86. </p>
  87. <div class="row justify-center q-mb-md">
  88. <q-btn
  89. color="primary"
  90. dense
  91. outline
  92. no-caps
  93. padding="6px 16px"
  94. rounded
  95. size="sm"
  96. icon="mdi-ticket-confirmation-outline"
  97. :label="$t('profile.favorites.code_btn')"
  98. @click="openCodeDialog"
  99. />
  100. </div>
  101. <div class="row items-center">
  102. <div class="col column items-center">
  103. <q-avatar
  104. class="text-h5 q-mb-sm shadow-1"
  105. size="72px"
  106. :style="avatarStyle(highlightedFavorite)"
  107. >
  108. <img
  109. v-if="getProfileMediaUrl(highlightedFavorite)"
  110. class="profile-image"
  111. :src="getProfileMediaUrl(highlightedFavorite)"
  112. />
  113. <span v-else>
  114. {{
  115. highlightedFavorite?.provider_name
  116. ?.charAt(0)
  117. ?.toUpperCase() ?? "?"
  118. }}
  119. </span>
  120. </q-avatar>
  121. <span class="font14 fontbold text-dark">
  122. {{ highlightedFavorite?.provider_name ?? "—" }}
  123. </span>
  124. <span
  125. v-if="getProviderGenderLabel(highlightedFavorite, t)"
  126. class="font10 fontmedium text-grey-6"
  127. >
  128. {{ getProviderGenderLabel(highlightedFavorite, t) }}
  129. </span>
  130. <span class="font12 fontmedium text-grey-6">
  131. {{ highlightedFavorite?.city_name ?? "—" }}
  132. </span>
  133. <div class="row items-center q-gutter-sm q-mt-sm">
  134. <q-btn
  135. color="primary"
  136. dense
  137. flat
  138. icon="mdi-chevron-left"
  139. round
  140. :disable="favorites.length <= 1"
  141. @click="prevHighlight"
  142. />
  143. <q-btn
  144. color="primary"
  145. no-caps
  146. padding="4px 20px"
  147. rounded
  148. size="sm"
  149. unelevated
  150. :label="$t('profile.favorites.indicate_btn')"
  151. @click="openIndicateDialog(highlightedFavorite)"
  152. />
  153. <q-btn
  154. color="primary"
  155. dense
  156. flat
  157. icon="mdi-chevron-right"
  158. round
  159. :disable="favorites.length <= 1"
  160. @click="nextHighlight"
  161. />
  162. </div>
  163. </div>
  164. </div>
  165. </div>
  166. <div class="q-mx-md q-mt-lg">
  167. <div class="row items-center justify-between q-mb-md">
  168. <span class="font16 fontbold gradient-diarista">
  169. {{ $t("profile.favorites.providers_title") }}
  170. </span>
  171. <div class="row items-center">
  172. <q-icon
  173. :class="selectedHours === '2h' ? '' : 'cursor-pointer'"
  174. :color="selectedHours === '2h' ? 'grey-4' : 'grey-6'"
  175. name="mdi-chevron-left"
  176. size="20px"
  177. @click="nextSelectedHours"
  178. />
  179. <span
  180. class="font12 fontbold text-grey-6 q-px-xs"
  181. style="user-select: none"
  182. >
  183. {{ $t("profile.favorites.until") }} {{ selectedHours }}
  184. </span>
  185. <q-icon
  186. :class="selectedHours === '8h' ? '' : 'cursor-pointer'"
  187. :color="selectedHours === '8h' ? 'grey-4' : 'grey-6'"
  188. name="mdi-chevron-right"
  189. size="20px"
  190. @click="prevSelectedHours"
  191. />
  192. </div>
  193. </div>
  194. <div
  195. v-for="(item, index) in favorites"
  196. :key="item.id"
  197. :class="{ 'item-separator': index < favorites.length - 1 }"
  198. class="favorite-item row items-center no-wrap q-py-md"
  199. >
  200. <div
  201. class="relative-position q-mr-md"
  202. style="margin-right: 16px"
  203. >
  204. <q-avatar
  205. class="shadow-1"
  206. size="60px"
  207. :style="avatarStyle(item)"
  208. >
  209. <img
  210. v-if="getProfileMediaUrl(item)"
  211. class="profile-image"
  212. :src="getProfileMediaUrl(item)"
  213. />
  214. <span v-else>
  215. {{ item.provider_name?.charAt(0)?.toUpperCase() ?? "?" }}
  216. </span>
  217. </q-avatar>
  218. <q-icon
  219. class="absolute cursor-pointer"
  220. name="mdi-heart"
  221. size="26px"
  222. style="bottom: -4px; right: -4px; color: #f518e3; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2))"
  223. @click.stop="confirmRemove(item)"
  224. />
  225. </div>
  226. <div
  227. class="col column justify-between"
  228. style="min-height: 60px"
  229. >
  230. <div class="row items-start justify-between">
  231. <div class="column">
  232. <span
  233. class="provider-name font12 fontmedium"
  234. style="color: #4a4a4a; line-height: 1.1"
  235. >
  236. {{ item.provider_name ?? "—" }}
  237. </span>
  238. <span
  239. v-if="getProviderGenderLabel(item, t)"
  240. class="font9 fontmedium text-grey-7 q-pt-xs"
  241. >
  242. {{ getProviderGenderLabel(item, t) }}
  243. </span>
  244. <span
  245. class="provider-city font9 fontbold text-grey-7 q-pt-xs"
  246. >
  247. {{ item.city_name ?? "—" }}
  248. </span>
  249. </div>
  250. <span
  251. class="provider-price font12 fontregular text-grey-7"
  252. style="line-height: 1.1"
  253. >
  254. R${{
  255. getPriceByHours(item)
  256. ? formatPrice(getPriceByHours(item))
  257. : "—"
  258. }}
  259. </span>
  260. </div>
  261. <div class="row items-end justify-between q-mt-xs">
  262. <div
  263. class="row items-center text-grey-5"
  264. style="margin-bottom: 2px"
  265. >
  266. <q-icon
  267. color="warning"
  268. name="mdi-star"
  269. size="12px"
  270. />
  271. <span
  272. class="q-ml-xs font9 fontregular"
  273. style="color: #666"
  274. >
  275. {{ item.average_rating ?? "—" }}
  276. </span>
  277. <span
  278. class="q-ml-xs font9 fontregular"
  279. style="margin-right: 2px"
  280. >
  281. ({{ item.total_services ?? 0 }})
  282. </span>
  283. <q-icon
  284. class="q-mx-xs"
  285. color="grey-4"
  286. name="mdi-circle-small"
  287. size="12px"
  288. />
  289. <q-icon
  290. name="mdi-broom"
  291. size="12px"
  292. style="color: #f518e3; transform: scaleX(-1)"
  293. />
  294. <span
  295. class="q-ml-xs font9 fontregular"
  296. style="color: #666"
  297. >
  298. {{ item.total_services ?? "0" }}
  299. </span>
  300. <q-icon
  301. class="q-mx-xs"
  302. color="grey-4"
  303. name="mdi-circle-small"
  304. size="16px"
  305. />
  306. <span class="font9 fontregular"> 0Km </span>
  307. </div>
  308. <q-btn
  309. class=""
  310. no-caps
  311. padding="4px 20px"
  312. rounded
  313. size="sm"
  314. style="background: #a63df7; color: white"
  315. unelevated
  316. :label="$t('dashboard_client.favorites.view_schedule')"
  317. @click="goToScheduling(item)"
  318. />
  319. </div>
  320. </div>
  321. </div>
  322. </div>
  323. </div>
  324. </div>
  325. </q-dialog>
  326. </template>
  327. <script setup>
  328. import diarinho from "src/assets/diarinho_perfil_cliente_favoritos.svg";
  329. import { avatarColorsWithPurple as avatarColors } from "src/helpers/avatarColors";
  330. import { Share } from "@capacitor/share";
  331. import { computed, onMounted, ref } from "vue";
  332. import {
  333. deleteClientFavoriteProvider,
  334. getClientFavoriteProviders,
  335. } from "src/api/clientFavoriteProvider";
  336. import { getProfileMediaUrl } from "src/helpers/profileMedia";
  337. import { getProviderGenderLabel } from "src/helpers/provider";
  338. import { useDialogPluginComponent, useQuasar } from "quasar";
  339. import { useI18n } from "vue-i18n";
  340. import { useRouter } from "vue-router";
  341. import { userStore } from "src/stores/user";
  342. import ProfileFavoriteCodeDialog from "./ProfileFavoriteCodeDialog.vue";
  343. import ProfileFavoriteRemoveDialog from "./ProfileFavoriteRemoveDialog.vue";
  344. import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
  345. defineEmits([...useDialogPluginComponent.emits]);
  346. const { dialogRef } = useDialogPluginComponent();
  347. const { t } = useI18n();
  348. const $q = useQuasar();
  349. const router = useRouter();
  350. const store = userStore();
  351. const CLIENT_APP_STORE_URL =
  352. "https://play.google.com/store/apps/details?id=br.com.diarista.client";
  353. const favorites = ref([]);
  354. const highlightIndex = ref(0);
  355. const loading = ref(false);
  356. const selectedHours = ref("6h");
  357. const highlightedFavorite = computed(
  358. () => favorites.value[highlightIndex.value] ?? null,
  359. );
  360. const hoursOptions = ["8h", "6h", "4h", "2h"];
  361. const avatarStyle = (item) => {
  362. const index = (item?.id ?? 0) % avatarColors.length;
  363. return avatarColors[index];
  364. };
  365. const confirmRemove = (item) => {
  366. $q.dialog({
  367. component: ProfileFavoriteRemoveDialog,
  368. }).onOk(async () => {
  369. try {
  370. await deleteClientFavoriteProvider(item.id);
  371. favorites.value = favorites.value.filter(
  372. (favorite) => favorite.id !== item.id,
  373. );
  374. if (highlightIndex.value >= favorites.value.length) {
  375. highlightIndex.value = Math.max(0, favorites.value.length - 1);
  376. }
  377. } catch (error) {
  378. console.error("Erro ao remover favorito:", error);
  379. }
  380. });
  381. };
  382. const formatPrice = (value) => {
  383. return Number(value).toLocaleString("pt-BR", {
  384. maximumFractionDigits: 2,
  385. minimumFractionDigits: 2,
  386. });
  387. };
  388. const getPriceByHours = (item) => {
  389. const priceKey = `daily_price_${selectedHours.value}`;
  390. return item[priceKey] ?? item.daily_price_8h ?? 0;
  391. };
  392. const goToSearch = () => {
  393. router.push({
  394. name: "SearchPage",
  395. });
  396. };
  397. const nextHighlight = () => {
  398. highlightIndex.value = (highlightIndex.value + 1) % favorites.value.length;
  399. };
  400. const nextSelectedHours = () => {
  401. const currentIndex = hoursOptions.indexOf(selectedHours.value);
  402. if (currentIndex < hoursOptions.length - 1) {
  403. selectedHours.value = hoursOptions[currentIndex + 1];
  404. }
  405. };
  406. const openCodeDialog = () => {
  407. $q.dialog({
  408. component: ProfileFavoriteCodeDialog,
  409. }).onOk(async () => {
  410. await loadFavorites();
  411. highlightIndex.value = 0;
  412. });
  413. };
  414. const openIndicateDialog = async (item) => {
  415. const message = t("profile.favorites.indicate_whatsapp_message", {
  416. city: item?.city_name ?? "",
  417. name: item?.provider_name ?? "",
  418. code: item?.share_code ?? "",
  419. link: CLIENT_APP_STORE_URL,
  420. button: t("profile.favorites.code_btn"),
  421. });
  422. try {
  423. await Share.share({
  424. title: t("profile.favorites.indicate_title"),
  425. text: message,
  426. dialogTitle: t("profile.favorites.indicate_btn"),
  427. });
  428. } catch (error) {
  429. if (error?.name === "AbortError") {
  430. return;
  431. }
  432. console.error("Erro ao indicar diarista:", error);
  433. }
  434. };
  435. const prevHighlight = () => {
  436. highlightIndex.value =
  437. highlightIndex.value === 0
  438. ? favorites.value.length - 1
  439. : highlightIndex.value - 1;
  440. };
  441. const prevSelectedHours = () => {
  442. const currentIndex = hoursOptions.indexOf(selectedHours.value);
  443. if (currentIndex > 0) {
  444. selectedHours.value = hoursOptions[currentIndex - 1];
  445. }
  446. };
  447. const loadFavorites = async () => {
  448. loading.value = true;
  449. try {
  450. const clientId = store.user?.client_id;
  451. if (clientId) {
  452. favorites.value = await getClientFavoriteProviders(clientId);
  453. }
  454. } catch (error) {
  455. console.error("Erro ao carregar favoritos:", error);
  456. } finally {
  457. loading.value = false;
  458. }
  459. };
  460. const goToScheduling = (provider) => {
  461. $q.dialog({
  462. component: SchedulingDialog,
  463. componentProps: { provider },
  464. });
  465. dialogRef.value.hide();
  466. };
  467. onMounted(loadFavorites);
  468. </script>
  469. <style scoped lang="scss">
  470. .heart-badge {
  471. position: absolute;
  472. bottom: -2px;
  473. right: -2px;
  474. background: white;
  475. border-radius: 50%;
  476. padding: 1px;
  477. }
  478. .favorite-item {
  479. background: transparent;
  480. }
  481. .item-separator {
  482. border-bottom: 1px solid rgba(0, 0, 0, 0.07);
  483. }
  484. .provider-name {
  485. color: #2c3e50;
  486. line-height: 1.3;
  487. }
  488. .provider-city {
  489. color: #888;
  490. line-height: 1.2;
  491. }
  492. .provider-metrics {
  493. gap: 3px;
  494. }
  495. .metric-value {
  496. color: #3a3a4a;
  497. line-height: 1;
  498. }
  499. .metric-meta {
  500. color: #999;
  501. line-height: 1;
  502. }
  503. .metric-dot {
  504. color: #ccc;
  505. line-height: 1;
  506. padding: 0 1px;
  507. }
  508. .provider-price {
  509. color: #2c3e50;
  510. }
  511. .profile-image {
  512. height: 100%;
  513. object-fit: cover;
  514. width: 100%;
  515. }
  516. .shadow-profile {
  517. box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.1);
  518. }
  519. </style>