CalendarPage.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. <template>
  2. <q-page class="bg-page q-pb-xl">
  3. <div class="calendar-header row items-center bg-white">
  4. <q-space />
  5. <span class="font16 fontbold gradient-diarista">{{ $t('provider.dashboard.agenda.title') }}</span>
  6. <q-space />
  7. </div>
  8. <template v-if="loading">
  9. <div class="row items-center justify-center full-width" style="height: 60vh">
  10. <q-spinner-dots color="primary" />
  11. </div>
  12. </template>
  13. <template v-else>
  14. <div class="q-mt-md q-mx-md">
  15. <div class="section-title font16 fontbold gradient-diarista q-mb-sm">{{ $t('provider.dashboard.agenda.upcoming_title') }}</div>
  16. <template v-if="upcomingSchedules.length > 0">
  17. <q-card
  18. v-for="item in upcomingSchedules"
  19. :key="item.id"
  20. class="calendar-card bg-surface shadow-card q-mb-sm"
  21. :flat="false"
  22. >
  23. <q-card-section class="q-pa-sm">
  24. <div class="row no-wrap items-start q-gutter-x-sm">
  25. <q-avatar size="44px">
  26. <img v-if="item.customer_photo" :src="item.customer_photo" style="object-fit:cover" />
  27. <span v-else class="full-width full-height flex flex-center" :style="avatarColors[item.id % avatarColors.length]" style="border-radius:50%">{{ item.client_name?.slice(0,2).toUpperCase() ?? '??' }}</span>
  28. </q-avatar>
  29. <div class="col column text-text">
  30. <span class="font12 fontmedium ellipsis">{{ item.client_name }}</span>
  31. <div class="row items-center no-wrap">
  32. <span class="font9 fontbold">{{ formatWeekday(item.date) }}</span>
  33. <span class="font9 fontmedium">{{ ', ' + formatDayMonth(item.date) }}</span>
  34. </div>
  35. <span class="font9 fontmedium">
  36. {{ $t('common.from') }}
  37. <span class="font9 fontbold">{{ item.start_time?.slice(0, 5) }}</span>
  38. {{ $t('common.to') }}
  39. <span class="font9 fontbold">{{ item.end_time?.slice(0, 5) }}</span>
  40. </span>
  41. </div>
  42. <div class="col-auto column items-end">
  43. <q-chip
  44. dense
  45. square
  46. :color="statusBgColor(item.status)"
  47. :text-color="statusTextColor(item.status)"
  48. :label="statusLabel(item.status)"
  49. class="status-chip font9"
  50. />
  51. <span class="text-price font10 fontbold">{{ formatCurrency(item.total_amount) }}</span>
  52. <span class="text-period font9 fontmedium">{{ periodLabel(item.period_type) }}</span>
  53. </div>
  54. </div>
  55. <div class="row items-center no-wrap q-mt-xs">
  56. <span class="type-label text-primary font10 fontmedium">
  57. {{ item.schedule_type === 'custom' ? $t('provider.dashboard.agenda.type_custom') : $t('provider.dashboard.agenda.type_default') }}
  58. </span>
  59. <q-space />
  60. <q-btn
  61. rounded
  62. unelevated
  63. padding="4px 12px"
  64. color="primary"
  65. size="xs"
  66. :label="$t('provider.dashboard.agenda.btn_view_details')"
  67. @click="openDetailsDialog(item)"
  68. />
  69. </div>
  70. </q-card-section>
  71. </q-card>
  72. </template>
  73. <div v-else class="font16 fontbold text-center text-grey-5 q-py-lg">
  74. {{ $t('provider.dashboard.agenda.empty_upcoming') }}
  75. </div>
  76. </div>
  77. <div class="q-mt-lg q-mx-md">
  78. <div class="section-title font16 fontbold gradient-diarista q-mb-sm">{{ $t('provider.dashboard.agenda.completed_title') }}</div>
  79. <template v-if="completedSchedules.length > 0">
  80. <q-card
  81. v-for="item in completedSchedules"
  82. :key="item.id"
  83. class="calendar-card bg-surface shadow-card q-mb-sm"
  84. :flat="false"
  85. >
  86. <q-card-section class="q-pa-sm">
  87. <div class="row no-wrap items-start q-gutter-x-sm">
  88. <q-avatar size="44px">
  89. <img v-if="item.customer_photo" :src="item.customer_photo" style="object-fit:cover" />
  90. <span v-else class="full-width full-height flex flex-center" :style="avatarColors[item.id % avatarColors.length]" style="border-radius:50%">{{ item.client_name?.slice(0,2).toUpperCase() ?? '??' }}</span>
  91. </q-avatar>
  92. <div class="col column text-text">
  93. <span class="font12 fontmedium ellipsis">{{ item.client_name }}</span>
  94. <div class="row items-center no-wrap">
  95. <span class="font9 fontbold">{{ formatWeekday(item.date) }}</span>
  96. <span class="font9 fontmedium">{{ ', ' + formatDayMonth(item.date) }}</span>
  97. </div>
  98. <span class="font9 fontmedium">
  99. {{ $t('common.from') }}
  100. <span class="font9 fontbold">{{ item.start_time?.slice(0, 5) }}</span>
  101. {{ $t('common.to') }}
  102. <span class="font9 fontbold">{{ item.end_time?.slice(0, 5) }}</span>
  103. </span>
  104. </div>
  105. <div class="col-auto column items-end">
  106. <q-chip
  107. dense
  108. square
  109. :color="statusBgColor(item.status)"
  110. :text-color="statusTextColor(item.status)"
  111. :label="statusLabel(item.status)"
  112. class="status-chip font9"
  113. />
  114. <span class="text-price font10 fontbold">{{ formatCurrency(item.total_amount) }}</span>
  115. <span class="text-period font9 fontmedium">{{ periodLabel(item.period_type) }}</span>
  116. </div>
  117. </div>
  118. <div class="row items-center no-wrap q-mt-xs">
  119. <span class="type-label text-primary font10 fontmedium">
  120. {{ item.schedule_type === 'custom' ? $t('provider.dashboard.agenda.type_custom') : $t('provider.dashboard.agenda.type_default') }}
  121. </span>
  122. <q-space />
  123. <q-rating
  124. :model-value="item.provider_reviewed ? item.provider_stars : 0"
  125. :max="5"
  126. size="14px"
  127. color="amber"
  128. icon="mdi-star-outline"
  129. icon-selected="mdi-star"
  130. readonly
  131. class="q-mr-sm"
  132. />
  133. <q-btn
  134. v-if="!item.provider_reviewed"
  135. unelevated
  136. rounded
  137. padding="4px 12px"
  138. no-caps
  139. color="secondary"
  140. size="xs"
  141. class="btn-rate"
  142. :label="$t('provider.dashboard.agenda.btn_rate')"
  143. @click="openRatingDialog(item)"
  144. />
  145. </div>
  146. </q-card-section>
  147. </q-card>
  148. </template>
  149. <div v-else class="text-center text-grey-5 q-py-lg">
  150. {{ $t('provider.dashboard.agenda.empty_completed') }}
  151. </div>
  152. </div>
  153. </template>
  154. </q-page>
  155. </template>
  156. <script setup>
  157. import { ref, onMounted } from 'vue';
  158. import { useQuasar } from 'quasar';
  159. import { useI18n } from 'vue-i18n';
  160. import { getProviderCalendar } from 'src/api/providerCalendar';
  161. import { formatCurrency } from 'src/helpers/utils';
  162. import { labelsPeriodTypes } from 'src/helpers/arraysOptions/labelsPeriodTypes.js';
  163. import NextSchedulesDetailsDialog from 'src/components/dashboard/NextSchedulesDetailsDialog.vue';
  164. import ScheduleRatingDialog from 'src/components/dashboard/ScheduleRatingDialog.vue';
  165. const $q = useQuasar();
  166. const { t } = useI18n();
  167. const avatarColors = [
  168. { background: '#ffd5df', color: '#932e57' },
  169. { background: '#d7e8ff', color: '#2158a8' },
  170. { background: '#dfd', color: '#2a7a3b' },
  171. { background: '#ffe5cc', color: '#8a4500' },
  172. ];
  173. const loading = ref(true);
  174. const upcomingSchedules = ref([]);
  175. const completedSchedules = ref([]);
  176. const parseLocalDate = (dateStr) => {
  177. if (!dateStr) return null;
  178. const s = String(dateStr);
  179. const iso = s.match(/^(\d{4})-(\d{2})-(\d{2})/);
  180. if (iso) return new Date(+iso[1], +iso[2] - 1, +iso[3]);
  181. const dmy = s.match(/^(\d{2})\/(\d{2})\/(\d{4})/);
  182. if (dmy) return new Date(+dmy[3], +dmy[2] - 1, +dmy[1]);
  183. return null;
  184. };
  185. const formatWeekday = (dateStr) => {
  186. const d = parseLocalDate(dateStr);
  187. if (!d) return '';
  188. const w = d.toLocaleDateString('pt-BR', { weekday: 'long' });
  189. return w.charAt(0).toUpperCase() + w.slice(1);
  190. };
  191. const formatDayMonth = (dateStr) => {
  192. const d = parseLocalDate(dateStr);
  193. if (!d) return '';
  194. return d.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
  195. };
  196. const periodLabel = (periodType) => {
  197. const found = labelsPeriodTypes.find(l => l.value == periodType);
  198. return found ? t(found.label) : '';
  199. };
  200. const statusLabel = (status) => {
  201. const map = {
  202. pending: t('provider.dashboard.agenda.status_pending'),
  203. accepted: t('provider.dashboard.agenda.status_accepted'),
  204. paid: t('provider.dashboard.agenda.status_paid'),
  205. started: t('provider.dashboard.agenda.status_started'),
  206. finished: t('provider.dashboard.agenda.status_finished'),
  207. cancelled: t('provider.dashboard.agenda.status_cancelled'),
  208. };
  209. return map[status] ?? status;
  210. };
  211. const statusBgColor = (status) => {
  212. const map = {
  213. pending: 'warning-bg',
  214. accepted: 'success-bg',
  215. paid: 'success-bg',
  216. started: 'info-bg',
  217. finished: 'neutral-bg',
  218. cancelled: 'secondary-bg',
  219. };
  220. return map[status] ?? 'neutral-bg';
  221. };
  222. const statusTextColor = (status) => {
  223. const map = {
  224. pending: 'warning',
  225. accepted: 'success',
  226. paid: 'success',
  227. started: 'info',
  228. finished: 'status-finished',
  229. cancelled: 'secondary',
  230. };
  231. return map[status] ?? 'text';
  232. };
  233. const loadCalendar = async () => {
  234. const response = await getProviderCalendar();
  235. if (response) {
  236. upcomingSchedules.value = response.upcomingSchedules ?? [];
  237. completedSchedules.value = response.completedSchedules ?? [];
  238. }
  239. };
  240. const openDetailsDialog = (schedule) => {
  241. $q.dialog({
  242. component: NextSchedulesDetailsDialog,
  243. componentProps: { schedule },
  244. }).onOk(async ({ action }) => {
  245. if (action === 'cancelled') {
  246. await loadCalendar();
  247. }
  248. });
  249. };
  250. const openRatingDialog = (schedule) => {
  251. $q.dialog({
  252. component: ScheduleRatingDialog,
  253. componentProps: { schedule },
  254. }).onOk(() => {
  255. loadCalendar();
  256. });
  257. };
  258. onMounted(async () => {
  259. await loadCalendar();
  260. loading.value = false;
  261. });
  262. </script>
  263. <style scoped lang="scss">
  264. .calendar-header {
  265. padding-top: 12px;
  266. padding-bottom: 12px;
  267. box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.08);
  268. }
  269. .calendar-card {
  270. border-radius: 12px;
  271. width: 100%;
  272. }
  273. .type-label {
  274. line-height: 1.2;
  275. }
  276. .text-price {
  277. color: #3a3a4a;
  278. white-space: nowrap;
  279. }
  280. .text-period {
  281. color: #888;
  282. text-align: right;
  283. white-space: nowrap;
  284. }
  285. .status-chip {
  286. height: auto;
  287. }
  288. .btn-rate {
  289. padding: 3px 10px;
  290. }
  291. </style>