| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- <template>
- <div>
- <DefaultHeaderPage />
- <div class="q-pa-md">
- <div v-if="loading" class="flex flex-center q-py-xl">
- <q-spinner color="violet-normal" size="48px" />
- </div>
- <template v-else>
- <div class="counters-row q-mb-md">
- <div class="counter-card">
- <span class="counter-value">
- <q-icon name="mdi-clock-outline" color="warning" />
- {{ counters.pendentes }}
- </span>
- <span class="counter-label">{{ $t("agendamento.status.pendente") }}</span>
- </div>
- <div class="counter-card">
- <span class="counter-value">
- <q-icon name="mdi-check-circle-outline" color="positive" />
- {{ counters.confirmados }}
- </span>
- <span class="counter-label">{{ $t("agendamento.status.confirmado") }}</span>
- </div>
- <div class="counter-card">
- <span class="counter-value">
- <q-icon name="mdi-close-circle-outline" color="negative" />
- {{ counters.recusados }}
- </span>
- <span class="counter-label">{{ $t("agendamento.status.recusado") }}</span>
- </div>
- </div>
- <div v-if="appointments.length === 0" class="text-center text-grey q-py-xl">
- {{ $t("http.errors.no_records_found") }}
- </div>
- <q-table
- v-else
- class="softpar-table q-pa-sm"
- :rows="pagedAppointments"
- :columns="columns"
- row-key="id"
- hide-pagination
- :rows-per-page-options="[0]"
- >
- <template #body-cell-servico="props">
- <q-td :props="props">
- {{ excerpt(props.row.partner_agreement_service?.name || '—', excerptSize) }}
- <q-tooltip v-if="props.row.partner_agreement_service?.name">
- {{ props.row.partner_agreement_service.name }}
- </q-tooltip>
- </q-td>
- </template>
- <template #body-cell-status="props">
- <q-td :props="props">
- <q-chip
- outline
- :color="statusColor(props.row.status)"
- :label="$t(`agendamento.status.${props.row.status}`)"
- size="sm"
- />
- </q-td>
- </template>
- <template #body-cell-acoes="props">
- <q-td :props="props">
- <div class="row no-wrap items-center" style="gap: 4px">
- <q-btn
- dense
- round
- icon="mdi-check"
- color="positive"
- size="sm"
- :unelevated="props.row.status === 'confirmado'"
- :outline="props.row.status !== 'confirmado'"
- :loading="actionId === props.row.id && actionType === 'approve'"
- @click.prevent.stop="onApprove(props.row)"
- />
- <q-btn
- dense
- round
- icon="mdi-close"
- color="negative"
- size="sm"
- :unelevated="props.row.status === 'recusado' || props.row.status === 'cancelado'"
- :outline="props.row.status !== 'recusado' && props.row.status !== 'cancelado'"
- :loading="actionId === props.row.id && actionType === 'reject'"
- @click.prevent.stop="onReject(props.row)"
- />
- </div>
- </q-td>
- </template>
- </q-table>
- <div v-if="totalPages > 1" class="flex flex-center q-mt-lg">
- <q-pagination
- v-model="currentPage"
- :max="totalPages"
- boundary-numbers
- color="violet-normal"
- active-color="violet-normal"
- direction-links
- />
- </div>
- </template>
- </div>
- </div>
- </template>
- <script setup>
- import { ref, computed, onMounted } from "vue";
- import { useQuasar } from "quasar";
- import { useI18n } from "vue-i18n";
- import {
- getPartnerAppointments,
- approveAppointmentParceiro,
- rejectAppointmentParceiro,
- } from "src/api/appointment";
- import { excerpt } from "src/helpers/utils";
- import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
- import ApproveAppointmentDialog from "src/components/ApproveAppointmentDialog.vue";
- const $q = useQuasar();
- const { t } = useI18n();
- const excerptSize = computed(() => {
- if ($q.screen.lt.md) return 20;
- if ($q.screen.md) return 30;
- return 45;
- });
- const loading = ref(true);
- const appointments = ref([]);
- const currentPage = ref(1);
- const PER_PAGE = 15;
- const actionId = ref(null);
- const actionType = ref(null);
- const columns = computed(() => [
- { name: "pedido", label: t("agendamento.col.pedido"), field: "order_number", align: "left" },
- { name: "associado", label: t("agendamento.associado"), field: (row) => row.user?.name || "—", align: "left" },
- { name: "servico", label: t("agendamento.col.servico"), field: (row) => row.partner_agreement_service?.name || "—", align: "left" },
- { name: "solicitacao",label: t("agendamento.col.solicitacao"), field: (row) => formatDate(row.created_at), align: "left" },
- { name: "horario", label: t("common.terms.hour2"), field: (row) => formatDateTime(row.date, row.time), align: "left" },
- { name: "acoes", label: t("common.terms.actions"), field: "id", align: "center" },
- { name: "status", label: t("common.terms.status"), field: "status", align: "center" },
- ]);
- const counters = computed(() => ({
- pendentes: appointments.value.filter((a) => a.status === "pendente").length,
- confirmados: appointments.value.filter((a) => a.status === "confirmado").length,
- recusados: appointments.value.filter((a) => a.status === "recusado" || a.status === "cancelado").length,
- }));
- const totalPages = computed(() => Math.max(1, Math.ceil(appointments.value.length / PER_PAGE)));
- const pagedAppointments = computed(() => {
- const start = (currentPage.value - 1) * PER_PAGE;
- return appointments.value.slice(start, start + PER_PAGE);
- });
- const statusColor = (status) => {
- const map = { pendente: "warning", confirmado: "positive", cancelado: "negative", recusado: "negative", concluido: "grey" };
- return map[status] ?? "grey";
- };
- const formatDate = (isoStr) => {
- if (!isoStr) return "—";
- const d = new Date(isoStr);
- if (isNaN(d)) return "—";
- return d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric" });
- };
- const formatDateTime = (date, time) => {
- if (!date) return "—";
- const d = new Date(date + "T00:00:00");
- if (isNaN(d)) return "—";
- const dateStr = d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric" });
- return time ? `${dateStr} ${time}` : dateStr;
- };
- const onApprove = (row) => {
- if (row.status !== "pendente") return;
- $q.dialog({ component: ApproveAppointmentDialog }).onOk(async ({ date, time }) => {
- actionId.value = row.id;
- actionType.value = "approve";
- try {
- await approveAppointmentParceiro(row.id, { date, time });
- row.status = "confirmado";
- $q.notify({ type: "positive", message: t("http.success") });
- } catch {
- $q.notify({ type: "negative", message: t("http.errors.failed") });
- } finally {
- actionId.value = null;
- actionType.value = null;
- }
- });
- };
- const onReject = (row) => {
- if (row.status !== "pendente") return;
- $q.dialog({
- title: t("common.ui.messages.confirm_action"),
- message: t("agendamento.confirm_reject"),
- cancel: true,
- persistent: true,
- }).onOk(async () => {
- actionId.value = row.id;
- actionType.value = "reject";
- try {
- await rejectAppointmentParceiro(row.id);
- row.status = "recusado";
- $q.notify({ type: "positive", message: t("http.success") });
- } catch {
- $q.notify({ type: "negative", message: t("http.errors.failed") });
- } finally {
- actionId.value = null;
- actionType.value = null;
- }
- });
- };
- onMounted(async () => {
- try {
- appointments.value = await getPartnerAppointments();
- } catch (e) {
- console.error(e);
- } finally {
- loading.value = false;
- }
- });
- </script>
- <style lang="scss">
- @import "src/css/table.scss";
- </style>
- <style scoped lang="scss">
- @use "src/css/quasar.variables.scss" as vars;
- .counters-row {
- display: flex;
- gap: 12px;
- }
- .counter-card {
- flex: 1;
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 16px;
- border-radius: 8px;
- border: 1.5px solid vars.$color-border;
- background: vars.$surface;
- color: #661d75;
- }
- .counter-value {
- font-size: 1.75rem;
- font-weight: 700;
- line-height: 1;
- margin-bottom: 4px;
- display: flex;
- align-items: center;
- gap: 6px;
- }
- .counter-label {
- font-size: 0.85rem;
- }
- </style>
|