AgendamentosParceiroPage.vue 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. <template>
  2. <div>
  3. <DefaultHeaderPage />
  4. <div class="q-pa-md">
  5. <div v-if="loading" class="flex flex-center q-py-xl">
  6. <q-spinner color="violet-normal" size="48px" />
  7. </div>
  8. <template v-else>
  9. <div class="counters-row q-mb-md">
  10. <div class="counter-card">
  11. <span class="counter-value">
  12. <q-icon name="mdi-clock-outline" color="warning" />
  13. {{ counters.pendentes }}
  14. </span>
  15. <span class="counter-label">{{ $t("agendamento.status.pendente") }}</span>
  16. </div>
  17. <div class="counter-card">
  18. <span class="counter-value">
  19. <q-icon name="mdi-check-circle-outline" color="positive" />
  20. {{ counters.confirmados }}
  21. </span>
  22. <span class="counter-label">{{ $t("agendamento.status.confirmado") }}</span>
  23. </div>
  24. <div class="counter-card">
  25. <span class="counter-value">
  26. <q-icon name="mdi-close-circle-outline" color="negative" />
  27. {{ counters.recusados }}
  28. </span>
  29. <span class="counter-label">{{ $t("agendamento.status.recusado") }}</span>
  30. </div>
  31. </div>
  32. <div v-if="appointments.length === 0" class="text-center text-grey q-py-xl">
  33. {{ $t("http.errors.no_records_found") }}
  34. </div>
  35. <q-table
  36. v-else
  37. class="softpar-table q-pa-sm"
  38. :rows="pagedAppointments"
  39. :columns="columns"
  40. row-key="id"
  41. hide-pagination
  42. :rows-per-page-options="[0]"
  43. >
  44. <template #body-cell-servico="props">
  45. <q-td :props="props">
  46. {{ excerpt(props.row.partner_agreement_service?.name || '—', excerptSize) }}
  47. <q-tooltip v-if="props.row.partner_agreement_service?.name">
  48. {{ props.row.partner_agreement_service.name }}
  49. </q-tooltip>
  50. </q-td>
  51. </template>
  52. <template #body-cell-status="props">
  53. <q-td :props="props">
  54. <q-chip
  55. outline
  56. :color="statusColor(props.row.status)"
  57. :label="$t(`agendamento.status.${props.row.status}`)"
  58. size="sm"
  59. />
  60. </q-td>
  61. </template>
  62. <template #body-cell-acoes="props">
  63. <q-td :props="props">
  64. <div class="row no-wrap items-center" style="gap: 4px">
  65. <q-btn
  66. dense
  67. round
  68. icon="mdi-check"
  69. color="positive"
  70. size="sm"
  71. :unelevated="props.row.status === 'confirmado'"
  72. :outline="props.row.status !== 'confirmado'"
  73. :loading="actionId === props.row.id && actionType === 'approve'"
  74. @click.prevent.stop="onApprove(props.row)"
  75. />
  76. <q-btn
  77. dense
  78. round
  79. icon="mdi-close"
  80. color="negative"
  81. size="sm"
  82. :unelevated="props.row.status === 'recusado' || props.row.status === 'cancelado'"
  83. :outline="props.row.status !== 'recusado' && props.row.status !== 'cancelado'"
  84. :loading="actionId === props.row.id && actionType === 'reject'"
  85. @click.prevent.stop="onReject(props.row)"
  86. />
  87. </div>
  88. </q-td>
  89. </template>
  90. </q-table>
  91. <div v-if="totalPages > 1" class="flex flex-center q-mt-lg">
  92. <q-pagination
  93. v-model="currentPage"
  94. :max="totalPages"
  95. boundary-numbers
  96. color="violet-normal"
  97. active-color="violet-normal"
  98. direction-links
  99. />
  100. </div>
  101. </template>
  102. </div>
  103. </div>
  104. </template>
  105. <script setup>
  106. import { ref, computed, onMounted } from "vue";
  107. import { useQuasar } from "quasar";
  108. import { useI18n } from "vue-i18n";
  109. import {
  110. getPartnerAppointments,
  111. approveAppointmentParceiro,
  112. rejectAppointmentParceiro,
  113. } from "src/api/appointment";
  114. import { excerpt } from "src/helpers/utils";
  115. import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
  116. import ApproveAppointmentDialog from "src/components/ApproveAppointmentDialog.vue";
  117. const $q = useQuasar();
  118. const { t } = useI18n();
  119. const excerptSize = computed(() => {
  120. if ($q.screen.lt.md) return 20;
  121. if ($q.screen.md) return 30;
  122. return 45;
  123. });
  124. const loading = ref(true);
  125. const appointments = ref([]);
  126. const currentPage = ref(1);
  127. const PER_PAGE = 15;
  128. const actionId = ref(null);
  129. const actionType = ref(null);
  130. const columns = computed(() => [
  131. { name: "pedido", label: t("agendamento.col.pedido"), field: "order_number", align: "left" },
  132. { name: "associado", label: t("agendamento.associado"), field: (row) => row.user?.name || "—", align: "left" },
  133. { name: "servico", label: t("agendamento.col.servico"), field: (row) => row.partner_agreement_service?.name || "—", align: "left" },
  134. { name: "solicitacao",label: t("agendamento.col.solicitacao"), field: (row) => formatDate(row.created_at), align: "left" },
  135. { name: "horario", label: t("common.terms.hour2"), field: (row) => formatDateTime(row.date, row.time), align: "left" },
  136. { name: "acoes", label: t("common.terms.actions"), field: "id", align: "center" },
  137. { name: "status", label: t("common.terms.status"), field: "status", align: "center" },
  138. ]);
  139. const counters = computed(() => ({
  140. pendentes: appointments.value.filter((a) => a.status === "pendente").length,
  141. confirmados: appointments.value.filter((a) => a.status === "confirmado").length,
  142. recusados: appointments.value.filter((a) => a.status === "recusado" || a.status === "cancelado").length,
  143. }));
  144. const totalPages = computed(() => Math.max(1, Math.ceil(appointments.value.length / PER_PAGE)));
  145. const pagedAppointments = computed(() => {
  146. const start = (currentPage.value - 1) * PER_PAGE;
  147. return appointments.value.slice(start, start + PER_PAGE);
  148. });
  149. const statusColor = (status) => {
  150. const map = { pendente: "warning", confirmado: "positive", cancelado: "negative", recusado: "negative", concluido: "grey" };
  151. return map[status] ?? "grey";
  152. };
  153. const formatDate = (isoStr) => {
  154. if (!isoStr) return "—";
  155. const d = new Date(isoStr);
  156. if (isNaN(d)) return "—";
  157. return d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric" });
  158. };
  159. const formatDateTime = (date, time) => {
  160. if (!date) return "—";
  161. const d = new Date(date + "T00:00:00");
  162. if (isNaN(d)) return "—";
  163. const dateStr = d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric" });
  164. return time ? `${dateStr} ${time}` : dateStr;
  165. };
  166. const onApprove = (row) => {
  167. if (row.status !== "pendente") return;
  168. $q.dialog({ component: ApproveAppointmentDialog }).onOk(async ({ date, time }) => {
  169. actionId.value = row.id;
  170. actionType.value = "approve";
  171. try {
  172. await approveAppointmentParceiro(row.id, { date, time });
  173. row.status = "confirmado";
  174. $q.notify({ type: "positive", message: t("http.success") });
  175. } catch {
  176. $q.notify({ type: "negative", message: t("http.errors.failed") });
  177. } finally {
  178. actionId.value = null;
  179. actionType.value = null;
  180. }
  181. });
  182. };
  183. const onReject = (row) => {
  184. if (row.status !== "pendente") return;
  185. $q.dialog({
  186. title: t("common.ui.messages.confirm_action"),
  187. message: t("agendamento.confirm_reject"),
  188. cancel: true,
  189. persistent: true,
  190. }).onOk(async () => {
  191. actionId.value = row.id;
  192. actionType.value = "reject";
  193. try {
  194. await rejectAppointmentParceiro(row.id);
  195. row.status = "recusado";
  196. $q.notify({ type: "positive", message: t("http.success") });
  197. } catch {
  198. $q.notify({ type: "negative", message: t("http.errors.failed") });
  199. } finally {
  200. actionId.value = null;
  201. actionType.value = null;
  202. }
  203. });
  204. };
  205. onMounted(async () => {
  206. try {
  207. appointments.value = await getPartnerAppointments();
  208. } catch (e) {
  209. console.error(e);
  210. } finally {
  211. loading.value = false;
  212. }
  213. });
  214. </script>
  215. <style lang="scss">
  216. @import "src/css/table.scss";
  217. </style>
  218. <style scoped lang="scss">
  219. @use "src/css/quasar.variables.scss" as vars;
  220. .counters-row {
  221. display: flex;
  222. gap: 12px;
  223. }
  224. .counter-card {
  225. flex: 1;
  226. display: flex;
  227. flex-direction: column;
  228. align-items: center;
  229. padding: 16px;
  230. border-radius: 8px;
  231. border: 1.5px solid vars.$color-border;
  232. background: vars.$surface;
  233. color: #661d75;
  234. }
  235. .counter-value {
  236. font-size: 1.75rem;
  237. font-weight: 700;
  238. line-height: 1;
  239. margin-bottom: 4px;
  240. display: flex;
  241. align-items: center;
  242. gap: 6px;
  243. }
  244. .counter-label {
  245. font-size: 0.85rem;
  246. }
  247. </style>