CalendarPage.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  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="text-subtitle1 text-weight-bold gradient-diarista">{{ $t('dashboard_client.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 gradient-diarista q-mb-sm">{{ $t('dashboard_client.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 :src="item.provider_photo || defaultAvatar">
  27. </q-avatar>
  28. <div class="col column">
  29. <span class="text-name ellipsis">{{ item.provider_name }}</span>
  30. <div class="row items-center no-wrap">
  31. <span class="text-date-bold">{{ formatWeekday(item.date) }}</span>
  32. <span class="text-date-regular">{{ ', ' + formatDayMonth(item.date) }}</span>
  33. </div>
  34. <span class="text-date-regular">
  35. {{ $t('dashboard_client.next_schedules.from') }}
  36. <span class="text-date-bold">{{ item.start_time?.slice(0, 5) }}</span>
  37. {{ $t('dashboard_client.next_schedules.to') }}
  38. <span class="text-date-bold">{{ item.end_time?.slice(0, 5) }}</span>
  39. </span>
  40. </div>
  41. <div class="col-auto column items-end">
  42. <q-chip
  43. dense
  44. square
  45. :color="statusBgColor(item.status)"
  46. :text-color="statusTextColor(item.status)"
  47. :label="statusLabel(item.status)"
  48. class="status-chip"
  49. />
  50. <span class="text-price">{{ formatCurrency(item.total_amount) }}</span>
  51. <span class="text-period">{{ periodLabel(item.period_type) }}</span>
  52. </div>
  53. </div>
  54. <div class="row items-center no-wrap q-mt-xs">
  55. <span class="type-label" :class="item.schedule_type === 'custom' ? 'type-custom' : 'type-default'">
  56. {{ item.schedule_type === 'custom' ? $t('dashboard_client.agenda.type_custom') : $t('dashboard_client.agenda.type_default') }}
  57. </span>
  58. <q-space />
  59. <q-btn
  60. flat
  61. no-caps
  62. color="primary"
  63. size="xs"
  64. class="btn-action"
  65. :label="$t('dashboard_client.agenda.btn_view_details')"
  66. @click="openDetailsDialog(item)"
  67. />
  68. </div>
  69. </q-card-section>
  70. </q-card>
  71. </template>
  72. <div v-else class="text-center text-grey-5 q-py-lg text-body2">
  73. {{ $t('dashboard_client.agenda.empty_upcoming') }}
  74. </div>
  75. </div>
  76. <div class="q-mt-lg q-mx-md">
  77. <div class="section-title gradient-diarista q-mb-sm">{{ $t('dashboard_client.agenda.completed_title') }}</div>
  78. <template v-if="completedSchedules.length > 0">
  79. <q-card
  80. v-for="item in completedSchedules"
  81. :key="item.id"
  82. class="calendar-card bg-surface shadow-card q-mb-sm"
  83. :flat="false"
  84. >
  85. <q-card-section class="q-pa-sm">
  86. <div class="row no-wrap items-start q-gutter-x-sm">
  87. <q-avatar size="44px">
  88. <img :src="item.provider_photo || defaultAvatar">
  89. </q-avatar>
  90. <div class="col column">
  91. <span class="text-name ellipsis">{{ item.provider_name }}</span>
  92. <div class="row items-center no-wrap">
  93. <span class="text-date-bold">{{ formatWeekday(item.date) }}</span>
  94. <span class="text-date-regular">{{ ', ' + formatDayMonth(item.date) }}</span>
  95. </div>
  96. <span class="text-date-regular">
  97. {{ $t('dashboard_client.next_schedules.from') }}
  98. <span class="text-date-bold">{{ item.start_time?.slice(0, 5) }}</span>
  99. {{ $t('dashboard_client.next_schedules.to') }}
  100. <span class="text-date-bold">{{ item.end_time?.slice(0, 5) }}</span>
  101. </span>
  102. </div>
  103. <div class="col-auto column items-end">
  104. <q-chip
  105. dense
  106. square
  107. :color="statusBgColor(item.status)"
  108. :text-color="statusTextColor(item.status)"
  109. :label="statusLabel(item.status)"
  110. class="status-chip"
  111. />
  112. <span class="text-price">{{ formatCurrency(item.total_amount) }}</span>
  113. <span class="text-period">{{ periodLabel(item.period_type) }}</span>
  114. </div>
  115. </div>
  116. <div class="row items-center no-wrap q-mt-xs">
  117. <span class="type-label" :class="item.schedule_type === 'custom' ? 'type-custom' : 'type-default'">
  118. {{ item.schedule_type === 'custom' ? $t('dashboard_client.agenda.type_custom') : $t('dashboard_client.agenda.type_default') }}
  119. </span>
  120. <q-space />
  121. <q-rating
  122. :model-value="item.client_reviewed ? item.client_stars : 0"
  123. :max="5"
  124. size="14px"
  125. color="amber"
  126. icon="mdi-star-outline"
  127. icon-selected="mdi-star"
  128. readonly
  129. class="q-mr-sm"
  130. />
  131. <q-btn
  132. v-if="item.client_reviewed"
  133. unelevated
  134. rounded
  135. no-caps
  136. color="secondary"
  137. size="xs"
  138. class="btn-rate"
  139. :label="$t('dashboard_client.agenda.btn_reschedule')"
  140. @click="openSchedulingDialog(item)"
  141. />
  142. <q-btn
  143. v-else
  144. unelevated
  145. rounded
  146. no-caps
  147. color="secondary"
  148. size="xs"
  149. class="btn-rate"
  150. :label="$t('dashboard_client.agenda.btn_rate')"
  151. @click="openRatingDialog(item)"
  152. />
  153. </div>
  154. </q-card-section>
  155. </q-card>
  156. </template>
  157. <div v-else class="text-center text-grey-5 q-py-lg text-body2">
  158. {{ $t('dashboard_client.agenda.empty_completed') }}
  159. </div>
  160. </div>
  161. </template>
  162. </q-page>
  163. </template>
  164. <script setup>
  165. import { ref, onMounted } from 'vue';
  166. import { useQuasar } from 'quasar';
  167. import { useI18n } from 'vue-i18n';
  168. import { getClientCalendar } from 'src/api/clientCalendar';
  169. import { formatCurrency } from 'src/helpers/utils';
  170. import NextSchedulesDetailsDialog from 'src/components/dashboard/NextSchedulesDetailsDialog.vue';
  171. import ScheduleRatingDialog from 'src/components/dashboard/ScheduleRatingDialog.vue';
  172. import SchedulingDialog from 'src/pages/search/components/SchedulingDialog.vue';
  173. const $q = useQuasar();
  174. const { t } = useI18n();
  175. const defaultAvatar = 'https://cdn.quasar.dev/img/avatar.png';
  176. const loading = ref(true);
  177. const upcomingSchedules = ref([]);
  178. const completedSchedules = ref([]);
  179. const parseLocalDate = (dateStr) => {
  180. if (!dateStr) return null;
  181. const s = String(dateStr);
  182. const iso = s.match(/^(\d{4})-(\d{2})-(\d{2})/);
  183. if (iso) return new Date(+iso[1], +iso[2] - 1, +iso[3]);
  184. const dmy = s.match(/^(\d{2})\/(\d{2})\/(\d{4})/);
  185. if (dmy) return new Date(+dmy[3], +dmy[2] - 1, +dmy[1]);
  186. return null;
  187. };
  188. const formatWeekday = (dateStr) => {
  189. const d = parseLocalDate(dateStr);
  190. if (!d) return '';
  191. const w = d.toLocaleDateString('pt-BR', { weekday: 'long' });
  192. return w.charAt(0).toUpperCase() + w.slice(1);
  193. };
  194. const formatDayMonth = (dateStr) => {
  195. const d = parseLocalDate(dateStr);
  196. if (!d) return '';
  197. return d.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
  198. };
  199. const periodLabel = (periodType) => {
  200. const key = `period_types.${periodType}`;
  201. const translated = t(key);
  202. return translated !== key ? translated : '';
  203. };
  204. const statusLabel = (status) => {
  205. const map = {
  206. pending: t('dashboard_client.agenda.status_pending'),
  207. accepted: t('dashboard_client.agenda.status_accepted'),
  208. paid: t('dashboard_client.agenda.status_paid'),
  209. started: t('dashboard_client.agenda.status_started'),
  210. finished: t('dashboard_client.agenda.status_finished'),
  211. cancelled: t('dashboard_client.agenda.status_cancelled'),
  212. };
  213. return map[status] ?? status;
  214. };
  215. const statusBgColor = (status) => {
  216. const map = {
  217. pending: 'warning-bg',
  218. accepted: 'success-bg',
  219. paid: 'success-bg',
  220. started: 'info-bg',
  221. finished: 'neutral-bg',
  222. cancelled: 'secondary-bg',
  223. };
  224. return map[status] ?? 'neutral-bg';
  225. };
  226. const statusTextColor = (status) => {
  227. const map = {
  228. pending: 'warning',
  229. accepted: 'success',
  230. paid: 'success',
  231. started: 'info',
  232. finished: 'status-finished',
  233. cancelled: 'secondary',
  234. };
  235. return map[status] ?? 'text';
  236. };
  237. const loadCalendar = async () => {
  238. const response = await getClientCalendar();
  239. if (response) {
  240. upcomingSchedules.value = response.upcomingSchedules ?? [];
  241. completedSchedules.value = response.completedSchedules ?? [];
  242. }
  243. };
  244. const openDetailsDialog = (schedule) => {
  245. $q.dialog({
  246. component: NextSchedulesDetailsDialog,
  247. componentProps: { schedule },
  248. }).onOk(async ({ action }) => {
  249. if (action === 'cancelled') {
  250. await loadCalendar();
  251. }
  252. });
  253. };
  254. const openRatingDialog = (schedule) => {
  255. $q.dialog({
  256. component: ScheduleRatingDialog,
  257. componentProps: { schedule },
  258. }).onOk(() => {
  259. loadCalendar();
  260. });
  261. };
  262. const openSchedulingDialog = (item) => {
  263. $q.dialog({
  264. component: SchedulingDialog,
  265. componentProps: {
  266. provider: {
  267. provider_id: item.provider_id,
  268. provider_name: item.provider_name,
  269. average_rating: item.average_rating,
  270. total_reviews: item.total_reviews,
  271. total_services: item.total_services,
  272. },
  273. },
  274. });
  275. };
  276. onMounted(async () => {
  277. await loadCalendar();
  278. loading.value = false;
  279. });
  280. </script>
  281. <style scoped lang="scss">
  282. .calendar-header {
  283. padding-top: calc(env(safe-area-inset-top) + 12px);
  284. padding-bottom: 12px;
  285. box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.08);
  286. }
  287. .calendar-card {
  288. border-radius: 12px;
  289. width: 100%;
  290. }
  291. .type-label {
  292. font-size: 10px;
  293. font-weight: 600;
  294. line-height: 1.2;
  295. }
  296. .type-default {
  297. color: #8B5CF6;
  298. }
  299. .type-custom {
  300. color: #EC4899;
  301. }
  302. .text-name {
  303. font-size: 13px;
  304. font-weight: 700;
  305. color: #3a3a4a;
  306. max-width: 130px;
  307. overflow: hidden;
  308. text-overflow: ellipsis;
  309. white-space: nowrap;
  310. }
  311. .text-date-bold {
  312. font-family: 'Inter', sans-serif;
  313. font-size: 11px;
  314. font-weight: 700;
  315. color: #3a3a4a;
  316. }
  317. .text-date-regular {
  318. font-family: 'Inter', sans-serif;
  319. font-size: 11px;
  320. font-weight: 400;
  321. color: #666;
  322. }
  323. .text-price {
  324. font-size: 13px;
  325. font-weight: 700;
  326. color: #3a3a4a;
  327. white-space: nowrap;
  328. }
  329. .text-period {
  330. font-size: 10px;
  331. color: #888;
  332. text-align: right;
  333. white-space: nowrap;
  334. }
  335. .status-chip {
  336. font-size: 11px !important;
  337. font-weight: 700;
  338. height: auto;
  339. padding: 2px 2px;
  340. }
  341. .btn-action {
  342. font-size: 11px;
  343. font-weight: 700;
  344. }
  345. .btn-rate {
  346. font-size: 11px;
  347. font-weight: 700;
  348. padding: 3px 10px;
  349. }
  350. </style>