DashboardNextSchedules.vue 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. <template>
  2. <div class="q-mx-md q-mb-md">
  3. <div class="font16 fontbold gradient-diarista q-mb-sm section-title">
  4. {{ $t("provider.dashboard.next_schedules.title") }}
  5. </div>
  6. <div v-if="!data.length" class="empty-alert row no-wrap items-start">
  7. <q-icon
  8. class="empty-alert__icon"
  9. name="mdi-alert-outline"
  10. size="28px"
  11. />
  12. <div class="empty-alert__text font10">
  13. <span class="fontbold">
  14. {{ $t("provider.dashboard.next_schedules.empty_alert_bold") }}
  15. </span>
  16. {{ $t("provider.dashboard.next_schedules.empty_alert_text") }}
  17. </div>
  18. </div>
  19. <div
  20. v-else
  21. class="scroll-wrapper"
  22. >
  23. <div
  24. ref="trackRef"
  25. class="scroll-track"
  26. :class="{ 'scroll-track--single': data.length === 1 }"
  27. @scroll="onTrackScroll"
  28. >
  29. <q-card
  30. v-for="item in data"
  31. :key="item.id"
  32. :class="{ 'schedule-card--single': data.length === 1 }"
  33. :flat="false"
  34. class="schedule-card card-border shadow-card bg-surface"
  35. >
  36. <q-card-section class="q-pa-sm">
  37. <div class="row no-wrap items-center q-gutter-x-sm">
  38. <q-avatar size="48px">
  39. <img
  40. v-if="item.customer_photo"
  41. :src="item.customer_photo"
  42. style="object-fit: cover"
  43. />
  44. <span
  45. v-else
  46. class="flex flex-center full-height full-width"
  47. style="border-radius: 50%"
  48. :style="avatarColors[item.id % avatarColors.length]"
  49. >
  50. {{
  51. getFirstName(item.customer_name ?? item.client_name)
  52. ?.slice(0, 2)
  53. .toUpperCase() ?? "??"
  54. }}
  55. </span>
  56. </q-avatar>
  57. <div class="column flex-1">
  58. <div class="row items-center q-gutter-x-xs">
  59. <span class="ellipsis font12 fontmedium text-name">
  60. {{ getFirstName(item.customer_name ?? item.client_name) || "—" }}
  61. </span>
  62. <div
  63. v-if="item.customer_rating"
  64. class="row items-center"
  65. >
  66. <q-icon
  67. color="warning"
  68. name="mdi-star"
  69. size="14px"
  70. />
  71. <span class="text-text font11">
  72. {{ item.customer_rating }}
  73. </span>
  74. </div>
  75. </div>
  76. <div class="row items-center no-wrap">
  77. <span class="font9 fontbold text-schedule-date-bold">
  78. {{ formatWeekday(item.date) }}
  79. </span>
  80. <span class="font9 text-schedule-date-regular">
  81. {{ ", " + formatDayMonth(item.date) }}
  82. </span>
  83. </div>
  84. <div class="text-schedule-date-regular font9">
  85. {{ $t("common.from") }}
  86. <span class="text-schedule-date-bold fontbold">
  87. {{ item.start_time?.slice(0, 5) }}
  88. </span>
  89. {{ $t("common.to") }}
  90. <span class="text-schedule-date-bold fontbold">
  91. {{ item.end_time?.slice(0, 5) }}
  92. </span>
  93. </div>
  94. </div>
  95. <div class="column items-end text-text">
  96. <div class="font14 fontbold text-price">
  97. {{ formatCurrency(item.total_amount) }}
  98. </div>
  99. <div class="font10 fontmedium text-type">
  100. {{ t(labelsPeriodTypes.find((label) => label.value == item.period_type)?.label) }}
  101. </div>
  102. <div class="font10 text-distance">
  103. {{ item.distance_km ?? 0 }}{{ $t("common.km") }}
  104. </div>
  105. </div>
  106. </div>
  107. <div
  108. class="font9 fontmedium items-center q-mt-md q-px-xs row text-text"
  109. >
  110. <div class="col ellipsis text-grey-7">
  111. {{ formatAddress(item.address) || "N/A" }}
  112. </div>
  113. <q-icon
  114. class="q-ml-xs"
  115. color="primary"
  116. name="mdi-content-copy"
  117. size="12px"
  118. @click="copyAddress(item.address)"
  119. />
  120. </div>
  121. <div class="row q-mt-sm items-center">
  122. <q-btn
  123. color="primary"
  124. flat
  125. no-caps
  126. padding="4px 8px"
  127. size="xs"
  128. unelevated
  129. :label="$t('provider.dashboard.next_schedules.details')"
  130. @click="emit('view-details', item)"
  131. />
  132. <q-space />
  133. <div class="font9 items-center q-gutter-x-xs row text-grey-7">
  134. <q-icon
  135. size="14px"
  136. :color="item.offers_meal ? 'secondary' : 'grey-5'"
  137. :name="item.offers_meal ? 'mdi-silverware' : 'mdi-close-outline'"
  138. />
  139. <span :class="item.offers_meal ? 'fontmedium' : ''">
  140. {{
  141. item.offers_meal
  142. ? $t("provider.dashboard.next_schedules.offers_meal")
  143. : $t("provider.dashboard.next_schedules.no_meal")
  144. }}
  145. </span>
  146. </div>
  147. </div>
  148. </q-card-section>
  149. </q-card>
  150. </div>
  151. </div>
  152. <DefaultCarouselIndicator
  153. :active="activeIndex"
  154. :count="pageCount"
  155. />
  156. </div>
  157. </template>
  158. <script setup>
  159. import { avatarColors, formatAddress, formatCurrency, getFirstName } from "src/helpers/utils";
  160. import { labelsPeriodTypes } from "src/helpers/labelsPeriodTypes.js";
  161. import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
  162. import { useCarouselIndicator } from "src/composables/useCarouselIndicator";
  163. import { useI18n } from "vue-i18n";
  164. import { useQuasar } from "quasar";
  165. import DefaultCarouselIndicator from "src/components/defaults/DefaultCarouselIndicator.vue";
  166. const props = defineProps({
  167. data: {
  168. default: () => [],
  169. type: Array,
  170. },
  171. });
  172. const emit = defineEmits(["view-details"]);
  173. const { activeIndex, onTrackScroll, pageCount, trackRef } =
  174. useCarouselIndicator(() => props.data);
  175. const $q = useQuasar();
  176. const { t } = useI18n();
  177. const copyAddress = (address) => {
  178. const formatted = formatAddress(address);
  179. if (formatted) {
  180. navigator.clipboard.writeText(formatted);
  181. }
  182. $q.notify({
  183. color: "positive",
  184. message: t("provider.dashboard.next_schedules.address_copied"),
  185. });
  186. };
  187. </script>
  188. <style scoped lang="scss">
  189. .scroll-wrapper {
  190. overflow: hidden;
  191. }
  192. .empty-alert {
  193. background: #e3efff;
  194. border-radius: 8px;
  195. gap: 12px;
  196. padding: 13px 14px;
  197. }
  198. .empty-alert__icon {
  199. color: #6554d9;
  200. flex: 0 0 auto;
  201. }
  202. .empty-alert__text {
  203. color: #6554d9;
  204. line-height: 1.2;
  205. padding-top: 1px;
  206. }
  207. .scroll-track {
  208. display: flex;
  209. flex-direction: row;
  210. gap: 12px;
  211. overscroll-behavior-x: contain;
  212. overflow-x: auto;
  213. padding-bottom: 8px;
  214. scroll-snap-type: x proximity;
  215. &::-webkit-scrollbar {
  216. display: none;
  217. }
  218. &::after {
  219. content: "";
  220. flex: 0 0 1px;
  221. }
  222. }
  223. .schedule-card {
  224. border-radius: 12px;
  225. min-width: 82vw;
  226. scroll-snap-align: start;
  227. }
  228. .schedule-card--single {
  229. min-width: 100%;
  230. width: 100%;
  231. }
  232. .text-name {
  233. color: #3a3a4a;
  234. }
  235. .text-schedule-date-bold {
  236. color: #3a3a4a;
  237. }
  238. .text-schedule-date-regular {
  239. color: #666;
  240. }
  241. .text-date,
  242. .text-time,
  243. .text-type,
  244. .text-region,
  245. .text-distance {
  246. color: #666;
  247. }
  248. .text-price {
  249. color: #3a3a4a;
  250. }
  251. .flex-1 {
  252. flex: 1;
  253. min-width: 0;
  254. }
  255. </style>