Quellcode durchsuchen

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 vor 1 Tag
Ursprung
Commit
4bc09aa312

+ 5 - 0
src/api/appointment.js

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

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

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

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

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

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

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

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

@@ -95,8 +95,7 @@
             :disable="row.status === 'on_leave' || row.status?.value === 'on_leave'"
             :disable="row.status === 'on_leave' || row.status?.value === 'on_leave'"
             @click.stop="onSetOnLeave(row)"
             @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
           <q-btn
             v-if="row.status === 'pending' || row.status?.value === 'pending'"
             v-if="row.status === 'pending' || row.status?.value === 'pending'"
             flat 
             flat 
@@ -159,6 +158,9 @@ const AddEditAssociadoDialog = defineAsyncComponent(
 const ImportHistoryDialog = defineAsyncComponent(
 const ImportHistoryDialog = defineAsyncComponent(
   () => import("src/components/ImportHistoryDialog.vue"),
   () => import("src/components/ImportHistoryDialog.vue"),
 );
 );
+const AssociadoAppointmentsDialog = defineAsyncComponent(
+  () => import("./components/AssociadoAppointmentsDialog.vue"),
+);
 
 
 const permission_store = permissionStore();
 const permission_store = permissionStore();
 const $q = useQuasar();
 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 = () => {
 const onOpenImportHistory = () => {
   $q.dialog({ component: ImportHistoryDialog, componentProps: { type: 'associado' } });
   $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>