DashboardPendingSchedules.vue 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  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 data"
  7. :key="item.id"
  8. :flat="false"
  9. class="pending-card card-border shadow-card bg-surface"
  10. @click="seeDetails(item)"
  11. >
  12. <q-card-section class="q-pa-md">
  13. <div class="row no-wrap items-start q-mb-sm">
  14. <q-avatar
  15. class="q-mr-sm flex-shrink-0"
  16. size="40px"
  17. :style="avatarColors[item.id % avatarColors.length]"
  18. >
  19. <img
  20. v-if="providerPhoto(item)"
  21. :src="providerPhoto(item)"
  22. style="object-fit: cover; border-radius: 50%"
  23. />
  24. <span v-else>
  25. {{
  26. providerName(item).slice(0, 2).toUpperCase() || "??"
  27. }}
  28. </span>
  29. </q-avatar>
  30. <div class="col column no-wrap overflow-hidden">
  31. <span class="font12 fontmedium text-text">
  32. <span v-if="type === 'servicePackage'">
  33. {{
  34. $t("dashboard_client.pending_schedules.pay_to_provider")
  35. }}
  36. </span>
  37. <span v-else-if="item.status == 'pending'">
  38. {{
  39. $t("dashboard_client.pending_schedules.requesting_with")
  40. }}
  41. </span>
  42. <span v-else-if="item.status == 'accepted'">
  43. {{
  44. $t("dashboard_client.pending_schedules.pay_to_provider")
  45. }}
  46. </span>
  47. <span class="font12 fontbold">
  48. {{ " " + (getFirstName(providerName(item)) || "—") }}</span
  49. >
  50. </span>
  51. <div class="row items-center q-mt-xs">
  52. <q-icon
  53. class="q-mr-xs"
  54. color="grey-5"
  55. name="mdi-clock-outline"
  56. size="13px"
  57. />
  58. <span class="font9 fontmedium text-grey-5">
  59. {{ displayTime(item) }}
  60. </span>
  61. </div>
  62. </div>
  63. <div
  64. class="clock-badge-col q-ml-sm flex-shrink-0 column items-center"
  65. >
  66. <div class="clock-badge">
  67. <q-icon
  68. color="white"
  69. name="mdi-clock-outline"
  70. size="14px"
  71. />
  72. </div>
  73. <span
  74. class="font10 fontmedium text-primary q-mt-xs badge-status-text"
  75. >
  76. {{
  77. type === 'servicePackage'
  78. ? $t("dashboard_client.pending_schedules.status.accepted")
  79. : $t(
  80. `dashboard_client.pending_schedules.status.${item.status ?? "pending"}`,
  81. )
  82. }}
  83. </span>
  84. </div>
  85. </div>
  86. <div class="progress-track q-mb-md">
  87. <div
  88. class="progress-fill"
  89. :style="{ width: progressPercent(item.status) + '%' }"
  90. />
  91. </div>
  92. <div class="row items-center no-wrap">
  93. <q-btn
  94. v-if="type !== 'servicePackage'"
  95. class="q-mr-sm flex-shrink-0"
  96. color="primary"
  97. dense
  98. flat
  99. no-caps
  100. size="xs"
  101. :label="$t('dashboard_client.pending_schedules.cancel_btn')"
  102. @click="$emit('cancel', item)"
  103. />
  104. <q-space />
  105. <span
  106. class="font9 fontregular text-grey-6 col ellipsis text-right"
  107. >
  108. <q-icon
  109. class="q-mr-xs flex-shrink-0"
  110. color="grey-6"
  111. name="mdi-map-marker-outline"
  112. size="13px"
  113. />
  114. {{
  115. displayAddress(item)
  116. }}
  117. </span>
  118. </div>
  119. </q-card-section>
  120. </q-card>
  121. </div>
  122. </div>
  123. </div>
  124. </template>
  125. <script setup>
  126. import { computed } from "vue";
  127. import { avatarColors } from "src/helpers/avatarColors";
  128. import { getFirstName } from "src/helpers/utils";
  129. const props = defineProps({
  130. data: { type: Array, default: () => [] },
  131. type: { type: String, default: 'schedule' },
  132. });
  133. const emit = defineEmits(["view-details", "cancel"]);
  134. const isServicePackage = computed(() => props.type === 'servicePackage');
  135. const providerPhoto = (item) => {
  136. if (isServicePackage.value) {
  137. return item.provider?.profile_media?.url ?? item.provider_photo ?? null;
  138. }
  139. return item.provider_photo;
  140. };
  141. const providerName = (item) => {
  142. if (isServicePackage.value) {
  143. return item.provider?.user?.name ?? item.provider_name ?? '';
  144. }
  145. return item.provider_name ?? '';
  146. };
  147. const displayAddress = (item) => {
  148. if (isServicePackage.value) {
  149. const firstSchedule = item.schedules?.[0];
  150. const addr = firstSchedule?.address;
  151. if (addr) {
  152. return [addr.address, addr.number, addr.district].filter(Boolean).join(', ') || '—';
  153. }
  154. return '—';
  155. }
  156. return [
  157. item.address?.address,
  158. item.address?.number,
  159. item.address?.district,
  160. ].filter(Boolean).join(', ') || '—';
  161. };
  162. const displayTime = (item) => {
  163. if (isServicePackage.value) {
  164. return item.schedules?.length
  165. ? `${item.schedules.length} agendamentos`
  166. : '—';
  167. }
  168. return item.time_since_request;
  169. };
  170. const statusProgressMap = {
  171. pending: 20,
  172. accepted: 40,
  173. paid: 60,
  174. started: 80,
  175. finished: 100,
  176. };
  177. const progressPercent = (status) => statusProgressMap[status] ?? 20;
  178. const seeDetails = (item) => {
  179. if (isServicePackage.value || item.status === "accepted") {
  180. emit("view-details", item);
  181. }
  182. };
  183. </script>
  184. <style scoped lang="scss">
  185. .scroll-wrapper {
  186. overflow: hidden;
  187. }
  188. .scroll-track {
  189. display: flex;
  190. flex-direction: row;
  191. gap: 12px;
  192. overflow-x: auto;
  193. overscroll-behavior-x: contain;
  194. scroll-snap-type: x proximity;
  195. padding-bottom: 8px;
  196. &::-webkit-scrollbar {
  197. display: none;
  198. }
  199. &::after {
  200. content: "";
  201. flex: 0 0 1px;
  202. }
  203. }
  204. .pending-card {
  205. min-width: 80%;
  206. scroll-snap-align: start;
  207. }
  208. .clock-badge-col {
  209. min-width: 52px;
  210. align-items: center;
  211. }
  212. .clock-badge {
  213. width: 24px;
  214. height: 24px;
  215. border-radius: 50%;
  216. background: linear-gradient(135deg, #8b5cf6, #ec4899);
  217. display: flex;
  218. align-items: center;
  219. justify-content: center;
  220. }
  221. .badge-status-text {
  222. white-space: nowrap;
  223. }
  224. .progress-track {
  225. width: 100%;
  226. height: 6px;
  227. background: #e2e8f0;
  228. border-radius: 999px;
  229. overflow: hidden;
  230. }
  231. .progress-fill {
  232. position: relative;
  233. height: 100%;
  234. border-radius: inherit;
  235. background: linear-gradient(
  236. 90deg,
  237. #3b5cff,
  238. #7a4dff,
  239. #d94cff
  240. );
  241. transition: width .4s ease;
  242. overflow: hidden;
  243. }
  244. .progress-fill::after {
  245. content: "";
  246. position: absolute;
  247. top: 0;
  248. bottom: 0;
  249. left: -35%;
  250. width: 35%;
  251. background: linear-gradient(
  252. 90deg,
  253. transparent,
  254. rgba(255,255,255,.45),
  255. transparent
  256. );
  257. transform: skewX(-20deg);
  258. animation: progressShine 2s linear infinite;
  259. }
  260. @keyframes progressShine {
  261. from {
  262. left: -35%;
  263. }
  264. to {
  265. left: 135%;
  266. }
  267. }
  268. </style>