DashboardNextSchedules.vue 7.6 KB

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