CalendarPage.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. <template>
  2. <q-page class="bg-page q-pb-xl">
  3. <div
  4. class="calendar-header row items-center q-px-md q-pt-md q-pb-sm bg-white"
  5. >
  6. <q-btn
  7. class="header-back-btn"
  8. color="primary"
  9. dense
  10. flat
  11. icon="mdi-chevron-left"
  12. round
  13. @click="router.back()"
  14. />
  15. <div
  16. class="col text-center font16 fontbold gradient-diarista text-primary"
  17. >
  18. {{ $t("dashboard_client.agenda.title") }}
  19. </div>
  20. <div style="width: 36px" />
  21. </div>
  22. <template v-if="loading">
  23. <div
  24. class="row items-center justify-center full-width"
  25. style="height: 60vh"
  26. >
  27. <q-spinner-dots color="primary" />
  28. </div>
  29. </template>
  30. <template v-else>
  31. <div class="q-mt-md q-mx-md">
  32. <div class="section-title gradient-diarista q-mb-sm font16 fontbold">
  33. {{ $t("dashboard_client.agenda.upcoming_title") }}
  34. </div>
  35. <template v-if="upcomingSchedules.length > 0">
  36. <q-card
  37. v-for="item in upcomingSchedules"
  38. :key="item.id"
  39. class="calendar-card bg-surface shadow-card q-mb-sm"
  40. :flat="false"
  41. >
  42. <q-card-section class="q-pa-sm">
  43. <div class="row no-wrap items-start q-gutter-x-sm">
  44. <q-avatar size="44px">
  45. <img
  46. v-if="item.provider_photo"
  47. style="object-fit: cover"
  48. :src="item.provider_photo"
  49. />
  50. <span
  51. v-else
  52. class="full-width full-height flex flex-center"
  53. style="border-radius: 50%"
  54. :style="avatarColors[item.id % avatarColors.length]"
  55. >
  56. {{
  57. getFirstName(item.provider_name)
  58. ?.slice(0, 2)
  59. .toUpperCase() ?? "??"
  60. }}
  61. </span>
  62. </q-avatar>
  63. <div class="col column text-text">
  64. <span class="font12 fontbold">
  65. {{
  66. getFirstName(item.provider_name) ||
  67. $t("dashboard_client.agenda.waiting_proposals")
  68. }}
  69. </span>
  70. <div class="row items-center no-wrap">
  71. <span class="font10 fontbold">
  72. {{ formatWeekday(item.date) }}
  73. </span>
  74. <span class="font10 fontmedium">
  75. {{ ", " + formatDayMonth(item.date) }}
  76. </span>
  77. </div>
  78. <span class="">
  79. <span class="font10 fontmedium">
  80. {{ $t("dashboard_client.next_schedules.from") }}
  81. </span>
  82. <span class="font10 fontbold q-ml-xs">
  83. {{ item.start_time?.slice(0, 5) }}
  84. {{ $t("dashboard_client.next_schedules.to") }}
  85. {{ item.end_time?.slice(0, 5) }}
  86. </span>
  87. </span>
  88. </div>
  89. <div class="col-auto column items-end">
  90. <q-chip
  91. class="font10"
  92. dense
  93. square
  94. :color="statusBgColor(item.status)"
  95. :label="statusLabel(item.status)"
  96. :text-color="statusTextColor(item.status)"
  97. />
  98. <span class="text-price font12 fontbold">
  99. {{ formatScheduleTotal(item) }}
  100. </span>
  101. <span class="text-period font10 fontmedium">
  102. {{ periodLabel(item.period_type) }}
  103. </span>
  104. </div>
  105. </div>
  106. <div class="row items-center no-wrap q-mt-xs">
  107. <span class="type-label text-primary font10 fontmedium">
  108. {{
  109. item.schedule_type === "custom"
  110. ? $t("dashboard_client.agenda.type_custom")
  111. : $t("dashboard_client.agenda.type_default")
  112. }}
  113. </span>
  114. <q-space />
  115. <q-btn
  116. class=""
  117. color="primary"
  118. no-caps
  119. padding="4px 8px"
  120. rounded
  121. size="sm"
  122. :label="$t('dashboard_client.agenda.btn_view_details')"
  123. @click="openDetailsDialog(item)"
  124. />
  125. </div>
  126. </q-card-section>
  127. </q-card>
  128. </template>
  129. <div
  130. v-else
  131. class="text-center text-grey-5 q-py-lg text-body2"
  132. >
  133. {{ $t("dashboard_client.agenda.empty_upcoming") }}
  134. </div>
  135. </div>
  136. <div class="q-mt-lg q-mx-md">
  137. <div class="section-title gradient-diarista q-mb-sm font16 fontbold">
  138. {{ $t("dashboard_client.agenda.completed_title") }}
  139. </div>
  140. <template v-if="completedSchedules.length > 0">
  141. <q-card
  142. v-for="item in completedSchedules"
  143. :key="item.id"
  144. class="calendar-card bg-surface shadow-card q-mb-sm"
  145. :flat="false"
  146. >
  147. <q-card-section class="q-pa-sm">
  148. <div class="row no-wrap items-start q-gutter-x-sm">
  149. <q-avatar size="44px">
  150. <img
  151. v-if="item.provider_photo"
  152. style="object-fit: cover"
  153. :src="item.provider_photo"
  154. />
  155. <span
  156. v-else
  157. class="full-width full-height flex flex-center"
  158. style="border-radius: 50%"
  159. :style="avatarColors[item.id % avatarColors.length]"
  160. >
  161. {{
  162. getFirstName(item.provider_name)
  163. ?.slice(0, 2)
  164. .toUpperCase() ?? "??"
  165. }}
  166. </span>
  167. </q-avatar>
  168. <div class="col column text-text">
  169. <span class="font12 fontbold">
  170. {{
  171. getFirstName(item.provider_name) ||
  172. $t("dashboard_client.agenda.waiting_proposals")
  173. }}
  174. </span>
  175. <div class="row items-center no-wrap">
  176. <span class="font10 fontbold">
  177. {{ formatWeekday(item.date) }}
  178. </span>
  179. <span class="font10 fontmedium">
  180. {{ ", " + formatDayMonth(item.date) }}
  181. </span>
  182. </div>
  183. <span class="">
  184. <span class="font10 fontmedium">
  185. {{ $t("dashboard_client.next_schedules.from") }}
  186. </span>
  187. <span class="font10 fontbold q-ml-xs">
  188. {{ item.start_time?.slice(0, 5) }}
  189. {{ $t("dashboard_client.next_schedules.to") }}
  190. {{ item.end_time?.slice(0, 5) }}
  191. </span>
  192. </span>
  193. </div>
  194. <div class="col-auto column items-end">
  195. <q-chip
  196. class="font10"
  197. dense
  198. square
  199. :color="statusBgColor(item.status)"
  200. :label="statusLabel(item.status)"
  201. :text-color="statusTextColor(item.status)"
  202. />
  203. <span class="text-price font12 fontbold">
  204. {{ formatScheduleTotal(item) }}
  205. </span>
  206. <span class="text-period font10 fontmedium">
  207. {{ periodLabel(item.period_type) }}
  208. </span>
  209. </div>
  210. </div>
  211. <div class="row items-center no-wrap q-mt-xs">
  212. <span class="type-label text-primary font10 fontmedium">
  213. {{
  214. item.schedule_type === "custom"
  215. ? $t("dashboard_client.agenda.type_custom")
  216. : $t("dashboard_client.agenda.type_default")
  217. }}
  218. </span>
  219. <q-space />
  220. <q-rating
  221. v-if="item.client_reviewed || canReview(item)"
  222. class="q-mr-sm"
  223. color="amber"
  224. icon="mdi-star-outline"
  225. icon-selected="mdi-star"
  226. readonly
  227. size="14px"
  228. :max="5"
  229. :model-value="item.client_reviewed ? item.client_stars : 0"
  230. />
  231. <q-btn
  232. v-if="item.client_reviewed"
  233. class="btn-rate"
  234. color="secondary"
  235. no-caps
  236. padding="4px 8px"
  237. rounded
  238. size="sm"
  239. unelevated
  240. :label="$t('dashboard_client.agenda.btn_reschedule')"
  241. @click="openSchedulingDialog(item)"
  242. />
  243. <q-btn
  244. v-else-if="canReview(item)"
  245. class="btn-rate"
  246. color="secondary"
  247. no-caps
  248. padding="4px 8px"
  249. rounded
  250. size="sm"
  251. unelevated
  252. :label="$t('dashboard_client.agenda.btn_rate')"
  253. @click="openRatingDialog(item)"
  254. />
  255. </div>
  256. </q-card-section>
  257. </q-card>
  258. </template>
  259. <div
  260. v-else
  261. class="text-center text-grey-5 q-py-lg text-body2"
  262. >
  263. {{ $t("dashboard_client.agenda.empty_completed") }}
  264. </div>
  265. </div>
  266. </template>
  267. </q-page>
  268. </template>
  269. <script setup>
  270. import { avatarColors } from "src/helpers/avatarColors";
  271. import { formatCurrency, getFirstName } from "src/helpers/utils";
  272. import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
  273. import { getClientCalendar } from "src/api/clientCalendar";
  274. import {
  275. getSchedulePaymentType,
  276. getScheduleTotalWithPlatformFee,
  277. } from "src/helpers/paymentPlatformFees";
  278. import { nextTick, onMounted, ref } from "vue";
  279. import { useI18n } from "vue-i18n";
  280. import { usePaymentPlatformFees } from "src/composables/usePaymentPlatformFees";
  281. import { useQuasar } from "quasar";
  282. import { useRoute, useRouter } from "vue-router";
  283. import NextSchedulesDetailsDialog from "src/pages/dashboard/components/schedule/NextSchedulesDetailsDialog.vue";
  284. import ScheduleRatingDialog from "src/pages/dashboard/components/schedule/ScheduleRatingDialog.vue";
  285. import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
  286. const $q = useQuasar();
  287. const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
  288. const route = useRoute();
  289. const router = useRouter();
  290. const { t } = useI18n();
  291. const completedSchedules = ref([]);
  292. const loading = ref(true);
  293. const upcomingSchedules = ref([]);
  294. const canReview = (item) => item.status !== "cancelled";
  295. //
  296. const formatScheduleTotal = (item) =>
  297. formatCurrency(
  298. getScheduleTotalWithPlatformFee(
  299. item,
  300. getSchedulePaymentType(item),
  301. platformFees.value,
  302. ),
  303. );
  304. //
  305. const loadCalendar = async () => {
  306. const response = await getClientCalendar();
  307. if (response) {
  308. upcomingSchedules.value = response.upcomingSchedules ?? [];
  309. completedSchedules.value = response.completedSchedules ?? [];
  310. }
  311. };
  312. const openDetailsDialog = (schedule) => {
  313. $q.dialog({
  314. component: NextSchedulesDetailsDialog,
  315. componentProps: { schedule },
  316. }).onOk(async ({ action }) => {
  317. if (action === "cancelled") {
  318. await loadCalendar();
  319. }
  320. });
  321. };
  322. const openRatingDialog = (schedule) => {
  323. if (!canReview(schedule)) return;
  324. $q.dialog({
  325. component: ScheduleRatingDialog,
  326. componentProps: { schedule },
  327. }).onOk(() => {
  328. loadCalendar();
  329. });
  330. };
  331. const openSchedulingDialog = (item) => {
  332. $q.dialog({
  333. component: SchedulingDialog,
  334. componentProps: {
  335. provider: {
  336. provider_id: item.provider_id,
  337. provider_name: item.provider_name,
  338. average_rating: item.average_rating,
  339. total_reviews: item.total_reviews,
  340. total_services: item.total_services,
  341. },
  342. },
  343. });
  344. };
  345. const periodLabel = (periodType) => {
  346. const key = `period_types.${periodType}`;
  347. const translated = t(key);
  348. return translated !== key ? translated : "";
  349. };
  350. const statusBgColor = (status) => {
  351. const map = {
  352. pending: "warning-bg",
  353. accepted: "success-bg",
  354. paid: "success-bg",
  355. started: "info-bg",
  356. finished: "neutral-bg",
  357. cancelled: "secondary-bg",
  358. };
  359. return map[status] ?? "neutral-bg";
  360. };
  361. const statusLabel = (status) => {
  362. const map = {
  363. pending: t("dashboard_client.agenda.status_pending"),
  364. accepted: t("dashboard_client.agenda.status_accepted"),
  365. paid: t("dashboard_client.agenda.status_paid"),
  366. started: t("dashboard_client.agenda.status_started"),
  367. finished: t("dashboard_client.agenda.status_finished"),
  368. cancelled: t("dashboard_client.agenda.status_cancelled"),
  369. };
  370. return map[status] ?? status;
  371. };
  372. const statusTextColor = (status) => {
  373. const map = {
  374. pending: "warning",
  375. accepted: "success",
  376. paid: "success",
  377. started: "info",
  378. finished: "status-finished",
  379. cancelled: "secondary",
  380. };
  381. return map[status] ?? "text";
  382. };
  383. const openScheduleFromQuery = async () => {
  384. const scheduleId = Number(route.query.scheduleId);
  385. if (!scheduleId) return;
  386. await router.replace({ path: route.path, query: {} });
  387. await nextTick();
  388. const schedule = [...upcomingSchedules.value, ...completedSchedules.value].find(
  389. (item) => item.id === scheduleId,
  390. );
  391. if (schedule) openDetailsDialog(schedule);
  392. };
  393. onMounted(async () => {
  394. await Promise.all([loadCalendar(), loadPlatformFees().catch(() => {})]);
  395. loading.value = false;
  396. await openScheduleFromQuery();
  397. });
  398. </script>
  399. <style scoped lang="scss">
  400. .calendar-header {
  401. box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.08);
  402. }
  403. .calendar-card {
  404. border-radius: 12px;
  405. width: 100%;
  406. }
  407. .type-label {
  408. line-height: 1.2;
  409. }
  410. .text-price {
  411. color: #3a3a4a;
  412. white-space: nowrap;
  413. }
  414. .text-period {
  415. color: #888;
  416. text-align: right;
  417. white-space: nowrap;
  418. }
  419. .btn-rate {
  420. padding: 3px 10px;
  421. }
  422. </style>