Ver Fonte

feat: ✨ feat (agendamentos associado) adicionada informacoes dos agendamentos do associado

na gestao associados, ao clicar no icone de agenda, abre tela com informacoes de todos os agendamentos do associado, com acoes de aprovar e recusar

fase:dev | origin:escopo
Gustavo Zanatta há 1 dia atrás
pai
commit
4bc09aa312

+ 5 - 0
src/api/appointment.js

@@ -39,6 +39,11 @@ export const rejectAppointmentParceiro = async (id) => {
   return data.payload;
 };
 
+export const getAppointmentsByUser = async (userId) => {
+  const { data } = await api.get(`/appointment/admin/user/${userId}`);
+  return data.payload;
+};
+
 export const getAdminCounters = async () => {
   const { data } = await api.get("/appointment/admin/counters");
   return data.payload;

+ 4 - 1
src/i18n/locales/en.json

@@ -494,6 +494,7 @@
     "dependent_approve_confirm": "Approve",
     "dependent_refuse_confirm": "Refuse",
     "pending_approval_dialog_title": "Associate Approval",
+    "appointments_dialog_title": "{name}'s Appointments",
     "pending_approve": "Approve",
     "pending_refuse": "Refuse",
     "import_associados": "Associates",
@@ -740,7 +741,9 @@
       "pedido": "Order",
       "parceiro": "Partner",
       "servico": "Service",
-      "solicitacao": "Request"
+      "solicitacao": "Request",
+      "data_hora": "Date/Time",
+      "observacoes": "Notes"
     }
   },
   "notification": {

+ 4 - 1
src/i18n/locales/es.json

@@ -495,6 +495,7 @@
     "dependent_approve_confirm": "Aprobar",
     "dependent_refuse_confirm": "Rechazar",
     "pending_approval_dialog_title": "Aprobación de Asociado",
+    "appointments_dialog_title": "Citas de {name}",
     "pending_approve": "Aprobar",
     "pending_refuse": "Rechazar",
     "import_associados": "Asociados",
@@ -740,7 +741,9 @@
       "pedido": "Pedido",
       "parceiro": "Socio",
       "servico": "Servicio",
-      "solicitacao": "Solicitud"
+      "solicitacao": "Solicitud",
+      "data_hora": "Fecha/Hora",
+      "observacoes": "Observaciones"
     }
   },
   "notification": {

+ 4 - 1
src/i18n/locales/pt.json

@@ -495,6 +495,7 @@
     "dependent_approve_confirm": "Aprovar",
     "dependent_refuse_confirm": "Recusar",
     "pending_approval_dialog_title": "Aprovação de Associado",
+    "appointments_dialog_title": "Agendamentos de {name}",
     "pending_approve": "Aprovar",
     "pending_refuse": "Recusar",
     "import_associados": "Associados",
@@ -741,7 +742,9 @@
       "pedido": "Pedido",
       "parceiro": "Parceiro",
       "servico": "Serviço",
-      "solicitacao": "Solicitação"
+      "solicitacao": "Solicitação",
+      "data_hora": "Data/Horário",
+      "observacoes": "Observações"
     }
   },
   "notification": {

+ 15 - 2
src/pages/gestao-associados/GestaoAssociadosPage.vue

@@ -95,8 +95,7 @@
             :disable="row.status === 'on_leave' || row.status?.value === 'on_leave'"
             @click.stop="onSetOnLeave(row)"
           />
-          <q-btn flat round dense color="violet-normal" icon="mdi-calendar" @click.stop />
-          <q-btn flat round dense color="violet-normal" icon="mdi-sofa" @click.stop />
+          <q-btn flat round dense color="violet-normal" icon="mdi-calendar" @click.stop="onOpenAppointments(row)" />
           <q-btn
             v-if="row.status === 'pending' || row.status?.value === 'pending'"
             flat 
@@ -159,6 +158,9 @@ const AddEditAssociadoDialog = defineAsyncComponent(
 const ImportHistoryDialog = defineAsyncComponent(
   () => import("src/components/ImportHistoryDialog.vue"),
 );
+const AssociadoAppointmentsDialog = defineAsyncComponent(
+  () => import("./components/AssociadoAppointmentsDialog.vue"),
+);
 
 const permission_store = permissionStore();
 const $q = useQuasar();
@@ -309,6 +311,17 @@ const onApprove = (row) => {
   });
 };
 
+const onOpenAppointments = (row) => {
+  if (!permission_store.getAccess("agendamento", "view")) {
+    $q.notify({ type: "negative", message: t("validation.permissions.view") });
+    return;
+  }
+  $q.dialog({
+    component: AssociadoAppointmentsDialog,
+    componentProps: { associado: row },
+  });
+};
+
 const onOpenImportHistory = () => {
   $q.dialog({ component: ImportHistoryDialog, componentProps: { type: 'associado' } });
 };

+ 232 - 0
src/pages/gestao-associados/components/AssociadoAppointmentsDialog.vue

@@ -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>