DashboardTodaySchedules.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. <template>
  2. <div class="q-mx-md q-mb-md">
  3. <div class="scroll-wrapper">
  4. <div class="scroll-track">
  5. <q-card
  6. v-for="item in visibleItems"
  7. :key="item.id"
  8. :flat="false"
  9. class="today-card card-border shadow-card bg-surface"
  10. >
  11. <q-card-section class="q-pa-md">
  12. <div class="row no-wrap items-start q-mb-xs">
  13. <q-avatar
  14. class="flex-shrink-0 q-mr-sm"
  15. size="40px"
  16. >
  17. <img
  18. v-if="item.provider_photo"
  19. :src="item.provider_photo"
  20. />
  21. <span
  22. v-else
  23. :style="avatarColors[item.id % avatarColors.length]"
  24. class="full-width full-height flex flex-center"
  25. style="border-radius: 50%"
  26. >
  27. {{ item.provider_name?.slice(0, 2).toUpperCase() ?? "??" }}
  28. </span>
  29. </q-avatar>
  30. <div class="col-7 column no-wrap overflow-hidden justify-center">
  31. <span class="font12 fontmedium text-text leading-tight">
  32. <template v-if="cardState(item) === 'awaiting_code'">
  33. {{ $t("dashboard_client.today_schedules.start_with") }}
  34. </template>
  35. <template v-else-if="cardState(item) === 'in_progress'">
  36. {{ $t("dashboard_client.today_schedules.started_by") }}
  37. </template>
  38. <template v-else-if="cardState(item) === 'finished'">
  39. {{ $t("dashboard_client.today_schedules.finished_by") }}
  40. </template>
  41. <template v-else-if="cardState(item) === 'cancelled'">
  42. {{ $t("dashboard_client.today_schedules.cancelled") }}
  43. </template>
  44. <span class="fontbold">
  45. {{ " " + (getFirstName(item.provider_name) || "—") }}
  46. </span>
  47. </span>
  48. <div class="row items-center q-mt-xs no-wrap">
  49. <q-icon
  50. class="q-mr-xs gradient-diarista"
  51. name="mdi-clock-outline"
  52. size="13px"
  53. />
  54. <span class="font9 fontregular text-grey-6">
  55. {{ $t("dashboard_client.next_schedules.from") }}
  56. {{ item.start_time?.slice(0, 5) }}
  57. {{ $t("dashboard_client.next_schedules.to") }}
  58. {{ item.end_time?.slice(0, 5) }}
  59. </span>
  60. </div>
  61. </div>
  62. <div
  63. class="flex-shrink-0 q-ml-sm column items-center justify-start"
  64. >
  65. <template v-if="cardState(item) === 'awaiting_code'">
  66. <div class="column items-center">
  67. <span class="font9 fontbold text-primary q-mb-xs">
  68. {{ $t("dashboard_client.today_schedules.code_label") }}
  69. </span>
  70. <div class="code-pill fontbold font10 bg-primary">
  71. {{ item.code }}
  72. </div>
  73. </div>
  74. </template>
  75. <template v-else-if="cardState(item) === 'in_progress'">
  76. <div class="column items-center">
  77. <div class="clock-badge">
  78. <q-icon
  79. color="white"
  80. name="mdi-clock-outline"
  81. size="15px"
  82. />
  83. </div>
  84. <span
  85. class="font10 fontregular badge-status-text text-primary q-mt-xs"
  86. >
  87. {{ $t("dashboard_client.today_schedules.in_progress") }}
  88. </span>
  89. </div>
  90. </template>
  91. <template v-else-if="cardState(item) === 'finished'">
  92. <div
  93. v-if="item.client_reviewed"
  94. class="rate-btn reviewed-badge"
  95. >
  96. <q-icon
  97. class="q-mr-xs"
  98. name="mdi-star"
  99. size="12px"
  100. />
  101. {{ $t("dashboard_client.schedule_rating.reviewed_badge") }}
  102. </div>
  103. <q-btn
  104. v-else
  105. class="rate-btn"
  106. no-caps
  107. unelevated
  108. @click.stop="emit('rate', item)"
  109. >
  110. <div class="column items-center">
  111. <q-icon
  112. class="q-mr-xs"
  113. name="mdi-star-outline"
  114. size="12px"
  115. />
  116. {{ $t("dashboard_client.today_schedules.rate_btn") }}
  117. </div>
  118. </q-btn>
  119. </template>
  120. <template v-else-if="cardState(item) === 'cancelled'">
  121. <div class="text-negative font10 fontbold text-center">
  122. {{ $t("dashboard_client.today_schedules.cancelled") }}
  123. </div>
  124. </template>
  125. </div>
  126. </div>
  127. <div class="progress-track q-mb-sm">
  128. <div
  129. class="progress-fill"
  130. :class="cardState(item) === 'finished' ? 'progress-fill--finished' : ''"
  131. :style="{ width: progressByState(item) + '%' }"
  132. />
  133. </div>
  134. <div class="row items-center no-wrap">
  135. <template v-if="cardState(item) !== 'finished'">
  136. <q-btn
  137. class="flex-shrink-0"
  138. color="primary"
  139. dense
  140. flat
  141. no-caps
  142. size="xs"
  143. :label="$t('dashboard_client.today_schedules.help_btn')"
  144. @click.stop="openHelp"
  145. />
  146. <q-space />
  147. <template v-if="cardState(item) === 'awaiting_code'">
  148. <span
  149. class="font9 fontregular text-grey-6 col ellipsis text-right"
  150. >
  151. <q-icon
  152. class="q-mr-xs flex-shrink-0"
  153. color="grey-6"
  154. name="mdi-map-marker-outline"
  155. size="13px"
  156. />
  157. {{
  158. [
  159. item.address?.address,
  160. item.address?.number,
  161. item.address?.district,
  162. ]
  163. .filter(Boolean)
  164. .join(", ") || "—"
  165. }}
  166. </span>
  167. </template>
  168. <template v-else>
  169. <q-icon
  170. class="q-mr-xs flex-shrink-0 gradient-diarista"
  171. name="mdi-clock-outline"
  172. size="13px"
  173. />
  174. <span
  175. class="font9 fontregular text-grey-6 text-right text-no-wrap"
  176. >
  177. {{ $t("dashboard_client.today_schedules.end_time_label") }}
  178. <span class="gradient-diarista">
  179. {{ item.end_time?.slice(0, 5) }}
  180. </span>
  181. </span>
  182. </template>
  183. </template>
  184. </div>
  185. </q-card-section>
  186. </q-card>
  187. </div>
  188. </div>
  189. </div>
  190. </template>
  191. <script setup>
  192. import { avatarColors } from "src/helpers/avatarColors";
  193. import { getFirstName } from "src/helpers/utils";
  194. import { useQuasar } from "quasar";
  195. import { ref, onMounted, onBeforeUnmount, computed } from "vue";
  196. import ProfileHelpDialog from "src/components/profile/ProfileHelpDialog.vue";
  197. const props = defineProps({
  198. data: {
  199. type: Array,
  200. default: () => [],
  201. },
  202. });
  203. const emit = defineEmits(["rate"]);
  204. const $q = useQuasar();
  205. const now = ref(Date.now());
  206. let timer = null;
  207. const cardState = (item) => {
  208. if (item.status === "cancelled") {
  209. return "cancelled";
  210. }
  211. if (item.client_reviewed) {
  212. return "finished";
  213. }
  214. if (
  215. item.status === "accepted" ||
  216. item.status === "paid"
  217. ) {
  218. return "awaiting_code";
  219. }
  220. if (
  221. item.status === "started" ||
  222. item.status === "finished"
  223. ) {
  224. const [hours, minutes] = (item.end_time || "23:59")
  225. .slice(0, 5)
  226. .split(":")
  227. .map(Number);
  228. const endTime = new Date();
  229. endTime.setHours(hours, minutes, 0, 0);
  230. return now.value >= endTime.getTime()
  231. ? "finished"
  232. : "in_progress";
  233. }
  234. return "awaiting_code";
  235. };
  236. const visibleItems = computed(() => {
  237. return props.data.filter((item) => !item.client_reviewed);
  238. });
  239. const progressByState = (item) => {
  240. const state = cardState(item);
  241. if (state === "awaiting_code") return 60;
  242. if (state === "in_progress") return 80;
  243. return 100;
  244. };
  245. //
  246. const openHelp = () => {
  247. $q.dialog({ component: ProfileHelpDialog });
  248. };
  249. onMounted(() => {
  250. timer = setInterval(() => {
  251. now.value = Date.now();
  252. }, 1000);
  253. });
  254. onBeforeUnmount(() => {
  255. clearInterval(timer);
  256. });
  257. </script>
  258. <style scoped lang="scss">
  259. .scroll-wrapper {
  260. overflow: hidden;
  261. }
  262. .scroll-track {
  263. display: flex;
  264. flex-direction: row;
  265. gap: 12px;
  266. overflow-x: auto;
  267. overscroll-behavior-x: contain;
  268. scroll-snap-type: x proximity;
  269. padding-bottom: 8px;
  270. &::-webkit-scrollbar {
  271. display: none;
  272. }
  273. &::after {
  274. content: "";
  275. flex: 0 0 1px;
  276. }
  277. }
  278. .today-card {
  279. min-width: 80%;
  280. scroll-snap-align: start;
  281. border-radius: 12px;
  282. }
  283. .code-pill {
  284. color: white;
  285. letter-spacing: 1px;
  286. border-radius: 20px;
  287. padding: 4px 10px;
  288. }
  289. .clock-badge {
  290. width: 26px;
  291. height: 26px;
  292. border-radius: 50%;
  293. background: linear-gradient(135deg, #8b5cf6, #ec4899);
  294. display: flex;
  295. align-items: center;
  296. justify-content: center;
  297. }
  298. .badge-status-text {
  299. white-space: nowrap;
  300. }
  301. .rate-btn {
  302. background: #ec4899;
  303. color: white;
  304. white-space: nowrap;
  305. border-radius: 10px !important;
  306. min-width: 45px;
  307. min-height: 40px;
  308. padding: 3px 5px;
  309. display: flex;
  310. flex-direction: column;
  311. align-items: center;
  312. justify-content: center;
  313. }
  314. .reviewed-badge {
  315. cursor: default;
  316. opacity: 0.85;
  317. }
  318. .progress-track {
  319. width: 100%;
  320. height: 5px;
  321. background: #e2e8f0;
  322. border-radius: 3px;
  323. overflow: hidden;
  324. }
  325. .progress-fill {
  326. height: 100%;
  327. border-radius: 3px;
  328. background: linear-gradient(90deg, #8b5cf6, #ec4899);
  329. transition: width 0.4s ease;
  330. &--finished {
  331. background: #22c55e;
  332. }
  333. }
  334. </style>