|
@@ -0,0 +1,232 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <q-dialog ref="dialogRef" @hide="onDialogHide">
|
|
|
|
|
+ <q-card class="q-dialog-plugin" style="width: 1200px; max-width: 95vw">
|
|
|
|
|
+ <DefaultDialogHeader :title="() => $t('associado.appointments_dialog_title', { name: associado.name })" @close="onDialogCancel" />
|
|
|
|
|
+
|
|
|
|
|
+ <q-card-section class="q-pt-none">
|
|
|
|
|
+ <div v-if="loading" class="flex flex-center q-py-xl">
|
|
|
|
|
+ <q-spinner color="violet-normal" size="48px" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-else-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"
|
|
|
|
|
+ :rows="appointments"
|
|
|
|
|
+ :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 ?? "—", 20) }}
|
|
|
|
|
+ <q-tooltip v-if="props.row.partner_agreement_service?.name">
|
|
|
|
|
+ {{ props.row.partner_agreement_service.name }}
|
|
|
|
|
+ </q-tooltip>
|
|
|
|
|
+ </q-td>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <template #body-cell-parceiro="props">
|
|
|
|
|
+ <q-td :props="props">
|
|
|
|
|
+ {{ excerpt(props.row.partner_agreement?.trade_name ?? props.row.partner_agreement?.company_name ?? "—", 20) }}
|
|
|
|
|
+ <q-tooltip v-if="props.row.partner_agreement?.trade_name ?? props.row.partner_agreement?.company_name">
|
|
|
|
|
+ {{ props.row.partner_agreement?.trade_name ?? props.row.partner_agreement?.company_name }}
|
|
|
|
|
+ </q-tooltip>
|
|
|
|
|
+ </q-td>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <template #body-cell-data_hora="props">
|
|
|
|
|
+ <q-td :props="props">
|
|
|
|
|
+ {{ formatDateTime(props.row) }}
|
|
|
|
|
+ </q-td>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <template #body-cell-observacoes="props">
|
|
|
|
|
+ <q-td :props="props">
|
|
|
|
|
+ {{ excerpt(props.row.observations ?? "—", 20) }}
|
|
|
|
|
+ <q-tooltip v-if="props.row.observations">
|
|
|
|
|
+ {{ props.row.observations }}
|
|
|
|
|
+ </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
|
|
|
|
|
+ outline
|
|
|
|
|
+ icon="mdi-check"
|
|
|
|
|
+ color="positive"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ :disable="!canApprove(props.row)"
|
|
|
|
|
+ :loading="actionId === props.row.id && actionType === 'approve'"
|
|
|
|
|
+ @click="onApprove(props.row)"
|
|
|
|
|
+ />
|
|
|
|
|
+ <q-btn
|
|
|
|
|
+ dense
|
|
|
|
|
+ round
|
|
|
|
|
+ outline
|
|
|
|
|
+ icon="mdi-close"
|
|
|
|
|
+ color="negative"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ :disable="!canReject(props.row)"
|
|
|
|
|
+ :loading="actionId === props.row.id && actionType === 'reject'"
|
|
|
|
|
+ @click="onReject(props.row)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </q-td>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </q-table>
|
|
|
|
|
+ </q-card-section>
|
|
|
|
|
+ </q-card>
|
|
|
|
|
+ </q-dialog>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { ref, computed, onMounted } from "vue";
|
|
|
|
|
+import { useDialogPluginComponent, useQuasar } from "quasar";
|
|
|
|
|
+import { useI18n } from "vue-i18n";
|
|
|
|
|
+import {
|
|
|
|
|
+ getAppointmentsByUser,
|
|
|
|
|
+ approveAppointment,
|
|
|
|
|
+ rejectAppointment,
|
|
|
|
|
+} from "src/api/appointment";
|
|
|
|
|
+import { permissionStore } from "src/stores/permission";
|
|
|
|
|
+import { excerpt } from "src/helpers/utils";
|
|
|
|
|
+
|
|
|
|
|
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
|
|
|
|
|
+import ApproveAppointmentDialog from "src/components/ApproveAppointmentDialog.vue";
|
|
|
|
|
+
|
|
|
|
|
+defineEmits([...useDialogPluginComponent.emits]);
|
|
|
|
|
+
|
|
|
|
|
+const { associado } = defineProps({
|
|
|
|
|
+ associado: { type: Object, required: true },
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const { t } = useI18n();
|
|
|
|
|
+const $q = useQuasar();
|
|
|
|
|
+const permission_store = permissionStore();
|
|
|
|
|
+const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
|
|
|
|
|
+
|
|
|
|
|
+const loading = ref(true);
|
|
|
|
|
+const appointments = ref([]);
|
|
|
|
|
+const actionId = ref(null);
|
|
|
|
|
+const actionType = ref(null);
|
|
|
|
|
+
|
|
|
|
|
+const columns = computed(() => [
|
|
|
|
|
+ { name: "pedido", label: t("agendamento.col.pedido"), field: "order_number", align: "left" },
|
|
|
|
|
+ { name: "parceiro", label: t("agendamento.col.parceiro"), field: (row) => row.partner_agreement?.trade_name ?? row.partner_agreement?.company_name ?? "—", align: "left" },
|
|
|
|
|
+ { name: "servico", label: t("agendamento.col.servico"), field: (row) => row.partner_agreement_service?.name ?? "—", align: "left" },
|
|
|
|
|
+ { name: "data_hora", label: t("agendamento.col.data_hora"), field: (row) => formatDateTime(row), align: "left" },
|
|
|
|
|
+ { name: "observacoes", label: t("agendamento.col.observacoes"), field: (row) => row.observations ?? "—", 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 statusColor = (status) => {
|
|
|
|
|
+ const map = { pendente: "warning-light", confirmado: "positive", cancelado: "negative", recusado: "negative", concluido: "grey" };
|
|
|
|
|
+ return map[status] ?? "grey";
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const formatDateTime = (row) => {
|
|
|
|
|
+ if (!row.date) return "—";
|
|
|
|
|
+ const d = new Date(`${row.date}T00:00:00`);
|
|
|
|
|
+ if (Number.isNaN(d.getTime())) return "—";
|
|
|
|
|
+ const dateStr = d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric" });
|
|
|
|
|
+ return row.time ? `${dateStr} ${row.time}` : dateStr;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const isFutureDateTime = (row) => {
|
|
|
|
|
+ if (!row.date) return false;
|
|
|
|
|
+ const dt = new Date(`${row.date}T${row.time || "00:00"}`);
|
|
|
|
|
+ if (Number.isNaN(dt.getTime())) return false;
|
|
|
|
|
+ return dt.getTime() > Date.now();
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const canApprove = (row) => row.status === "pendente";
|
|
|
|
|
+
|
|
|
|
|
+const canReject = (row) =>
|
|
|
|
|
+ row.status === "pendente" || (row.status === "confirmado" && isFutureDateTime(row));
|
|
|
|
|
+
|
|
|
|
|
+const onApprove = (row) => {
|
|
|
|
|
+ if (!canApprove(row)) return;
|
|
|
|
|
+ if (!permission_store.getAccess("agendamento", "edit")) {
|
|
|
|
|
+ $q.notify({ type: "negative", message: t("validation.permissions.edit") });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ $q.dialog({ component: ApproveAppointmentDialog }).onOk(async ({ date, time }) => {
|
|
|
|
|
+ actionId.value = row.id;
|
|
|
|
|
+ actionType.value = "approve";
|
|
|
|
|
+ try {
|
|
|
|
|
+ await approveAppointment(row.id, { date, time });
|
|
|
|
|
+ row.status = "confirmado";
|
|
|
|
|
+ row.date = date;
|
|
|
|
|
+ row.time = time;
|
|
|
|
|
+ $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 (!canReject(row)) return;
|
|
|
|
|
+ if (!permission_store.getAccess("agendamento", "edit")) {
|
|
|
|
|
+ $q.notify({ type: "negative", message: t("validation.permissions.edit") });
|
|
|
|
|
+ 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 rejectAppointment(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 getAppointmentsByUser(associado.id);
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ $q.notify({ type: "negative", message: t("http.errors.failed") });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss">
|
|
|
|
|
+@import "src/css/table.scss";
|
|
|
|
|
+</style>
|