Ver Fonte

WIP - guias dos agendamentos de consultas

Gustavo Zanatta há 2 dias atrás
pai
commit
0964f71f69

+ 41 - 0
src/api/appointment.js

@@ -1,5 +1,46 @@
 import api from "src/api";
 
+const blobErrorMessage = async (blob) => {
+  try {
+    return JSON.parse(await blob.text())?.message ?? null;
+  } catch {
+    return null;
+  }
+};
+
+const downloadGuide = async (url, orderNumber) => {
+  let response;
+
+  try {
+    response = await api.get(url, { responseType: "blob" });
+  } catch (error) {
+    const payload = error?.response?.data;
+    const message = payload instanceof Blob ? await blobErrorMessage(payload) : null;
+
+    throw message ? new Error(message) : error;
+  }
+
+  const blobUrl = window.URL.createObjectURL(
+    new Blob([response.data], { type: "application/pdf" }),
+  );
+  const link = document.createElement("a");
+  link.href = blobUrl;
+  link.setAttribute("download", `guia_${orderNumber}.pdf`);
+  document.body.appendChild(link);
+  link.click();
+  document.body.removeChild(link);
+  window.URL.revokeObjectURL(blobUrl);
+};
+
+export const downloadAppointmentGuide = (id, orderNumber) =>
+  downloadGuide(`/appointment/${id}/guide`, orderNumber);
+
+export const downloadMyAppointmentGuide = (id, orderNumber) =>
+  downloadGuide(`/associado/appointment/${id}/guide`, orderNumber);
+
+export const downloadPartnerAppointmentGuide = (id, orderNumber) =>
+  downloadGuide(`/parceiro/appointment/${id}/guide`, orderNumber);
+
 // ─── Rotas do Associado ───────────────────────────────────────────────────────
 
 export const getMyAppointments = async () => {

+ 68 - 0
src/components/selects/DependenteSelect.vue

@@ -0,0 +1,68 @@
+<template>
+  <DefaultSelect
+    v-model="selected"
+    v-bind="$attrs"
+    clearable
+    :options="options"
+    :label
+    :loading
+  >
+    <template #no-option>
+      <q-item>
+        <q-item-section class="text-grey">
+          {{ $t("associado.no_approved_dependents") }}
+        </q-item-section>
+      </q-item>
+    </template>
+  </DefaultSelect>
+</template>
+
+<script setup>
+import { ref, watch } from "vue";
+import { useI18n } from "vue-i18n";
+import { getDependentsByUser } from "src/api/profile";
+import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
+
+const { userId, label } = defineProps({
+  userId: {
+    type: Number,
+    default: null,
+  },
+  label: {
+    type: String,
+    default: () => useI18n().t("associado.dependent"),
+  },
+});
+
+const selected = defineModel({ type: Object });
+
+const loading = ref(false);
+const options = ref([]);
+
+const loadDependents = async (id) => {
+  options.value = [];
+
+  if (!id) return;
+
+  loading.value = true;
+  try {
+    const dependents = await getDependentsByUser(id);
+    options.value = dependents
+      .filter((d) => d.status === "approved")
+      .map((d) => ({ label: d.name, value: d.id, data: d }));
+  } catch (e) {
+    console.error(e);
+  } finally {
+    loading.value = false;
+  }
+};
+
+watch(
+  () => userId,
+  (id) => {
+    selected.value = null;
+    loadDependents(id);
+  },
+  { immediate: true },
+);
+</script>

+ 9 - 0
src/i18n/locales/en.json

@@ -467,6 +467,7 @@
       "stats": "Statistics",
       "about": "About Us",
       "contacts": "Contacts",
+      "guide": "Appointment Guide",
       "benefits": "Benefits",
       "events": "Events"
     },
@@ -495,6 +496,9 @@
       "location": "Location",
       "location_placeholder": "Ex: Toledo - PR"
     },
+    "guide": {
+      "validity_days": "Guide validity (days)"
+    },
     "benefit": {
       "title": "Title",
       "title_singular": "Benefit",
@@ -562,6 +566,7 @@
     "filter_by_position": "Filter by position",
     "filter_by_sector": "Filter by sector",
     "no_dependents": "No dependents registered",
+    "no_approved_dependents": "No approved dependents",
     "remove_photo": "Do you want to remove your profile photo?",
     "kinship": "Kinship",
     "dependent_statuses": {
@@ -806,6 +811,10 @@
     "nova_solicitacao": "New Request",
     "visao_geral": "Overview",
     "aprovados_automaticamente": "Auto Approved",
+    "for_dependent": "Appointment for a dependent",
+    "for_dependent_self": "This appointment is for a dependent",
+    "guia": "Guide",
+    "gerar_guia": "Generate guide",
     "definir_data_hora": "Set date and time",
     "confirm_reject": "Confirm rejection of this appointment?",
     "status": {

+ 9 - 0
src/i18n/locales/es.json

@@ -468,6 +468,7 @@
       "stats": "Estadísticas",
       "about": "Sobre Nosotros",
       "contacts": "Contactos",
+      "guide": "Guía de Atención",
       "benefits": "Beneficios",
       "events": "Eventos"
     },
@@ -496,6 +497,9 @@
       "location": "Ubicación",
       "location_placeholder": "Ej: Toledo - PR"
     },
+    "guide": {
+      "validity_days": "Validez de la guía (días)"
+    },
     "benefit": {
       "title": "Título",
       "title_singular": "Beneficio",
@@ -563,6 +567,7 @@
     "filter_by_position": "Filtrar por cargo",
     "filter_by_sector": "Filtrar por sector",
     "no_dependents": "Ningún dependiente registrado",
+    "no_approved_dependents": "Ningún dependiente aprobado",
     "remove_photo": "¿Deseas eliminar tu foto de perfil?",
     "kinship": "Parentesco",
     "dependent_statuses": {
@@ -806,6 +811,10 @@
     "nova_solicitacao": "Nueva Solicitud",
     "visao_geral": "Vista General",
     "aprovados_automaticamente": "Aprobados Automáticamente",
+    "for_dependent": "Cita para dependiente",
+    "for_dependent_self": "Esta cita es para un dependiente",
+    "guia": "Guía",
+    "gerar_guia": "Generar guía",
     "definir_data_hora": "Definir fecha y hora",
     "confirm_reject": "¿Confirmar rechazo de esta cita?",
     "status": {

+ 9 - 0
src/i18n/locales/pt.json

@@ -468,6 +468,7 @@
       "stats": "Estatísticas",
       "about": "Sobre Nós",
       "contacts": "Contatos",
+      "guide": "Guia de Atendimento",
       "benefits": "Benefícios",
       "events": "Eventos"
     },
@@ -496,6 +497,9 @@
       "location": "Localização",
       "location_placeholder": "Ex: Toledo - PR"
     },
+    "guide": {
+      "validity_days": "Validade da guia (dias)"
+    },
     "benefit": {
       "title": "Título",
       "title_singular": "Benefício",
@@ -563,6 +567,7 @@
     "filter_by_position": "Filtrar por cargo",
     "filter_by_sector": "Filtrar por setor",
     "no_dependents": "Nenhum dependente cadastrado",
+    "no_approved_dependents": "Nenhum dependente aprovado",
     "remove_photo": "Deseja remover sua foto de perfil?",
     "kinship": "Parentesco",
     "dependent_statuses": {
@@ -807,6 +812,10 @@
     "nova_solicitacao": "Nova Solicitação",
     "visao_geral": "Visão Geral",
     "aprovados_automaticamente": "Aprovados Automaticamente",
+    "for_dependent": "Agendamento para dependente",
+    "for_dependent_self": "Este agendamento é para um dependente",
+    "guia": "Guia",
+    "gerar_guia": "Gerar guia",
     "definir_data_hora": "Definir data e horário",
     "confirm_reject": "Confirmar recusa deste agendamento?",
     "status": {

+ 91 - 1
src/pages/agendamentos/AppointmentsAdminPage.vue

@@ -27,6 +27,21 @@
                 :rules="[inputRules.required]"
                 class="col-12 input-violet"
               />
+              <div class="col-12">
+                <q-checkbox
+                  v-model="form.forDependent"
+                  color="primary"
+                  :disable="!form.associado"
+                  :label="$t('agendamento.for_dependent')"
+                />
+              </div>
+              <DependenteSelect
+                v-if="form.forDependent"
+                v-model="form.dependent"
+                :user-id="form.associado?.value ?? null"
+                :rules="[inputRules.required]"
+                class="col-12 input-violet"
+              />
               <PartnerAgreementSelect
                 v-model="form.partner"
                 :label="$t('ui.navigation.convenios')"
@@ -134,6 +149,9 @@
             />
           </q-td>
         </template>
+        <template #body-cell-dependent_name="{ row }">
+          <q-td>{{ row.dependent_name || '—' }}</q-td>
+        </template>
         <template #body-cell-acoes="{ row }">
           <q-td auto-width>
             <div class="row no-wrap items-center" style="gap: 4px">
@@ -159,6 +177,19 @@
                 :loading="actionId === row.id && actionType === 'reject'"
                 @click.prevent.stop="onReject(row)"
               />
+              <q-btn
+                dense
+                round
+                outline
+                icon="mdi-file-document-outline"
+                color="primary"
+                size="sm"
+                :disable="!row.can_issue_guide"
+                :loading="actionId === row.id && actionType === 'guide'"
+                @click.prevent.stop="onDownloadGuide(row)"
+              >
+                <q-tooltip>{{ $t('agendamento.gerar_guia') }}</q-tooltip>
+              </q-btn>
             </div>
           </q-td>
         </template>
@@ -179,6 +210,26 @@
             <q-tooltip v-if="row.service_name">{{ row.service_name }}</q-tooltip>
           </q-td>
         </template>
+        <template #body-cell-dependent_name="{ row }">
+          <q-td>{{ row.dependent_name || '—' }}</q-td>
+        </template>
+        <template #body-cell-guia="{ row }">
+          <q-td auto-width class="text-center">
+            <q-btn
+              dense
+              round
+              outline
+              icon="mdi-file-document-outline"
+              color="primary"
+              size="sm"
+              :disable="!row.can_issue_guide"
+              :loading="actionId === row.id && actionType === 'guide'"
+              @click.prevent.stop="onDownloadGuide(row)"
+            >
+              <q-tooltip>{{ $t('agendamento.gerar_guia') }}</q-tooltip>
+            </q-btn>
+          </q-td>
+        </template>
         <template #body-cell-status>
           <q-td class="text-center">
             <q-chip
@@ -195,7 +246,7 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted, useTemplateRef, nextTick } from "vue";
+import { ref, computed, onMounted, useTemplateRef, nextTick, watch } from "vue";
 import { useQuasar } from "quasar";
 import { useI18n } from "vue-i18n";
 import { useInputRules } from "src/composables/useInputRules";
@@ -206,6 +257,7 @@ import DefaultTableServerSide from "src/components/defaults/DefaultTableServerSi
 import DefaultInput from "src/components/defaults/DefaultInput.vue";
 import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
 import AssociadoSelect from "src/components/selects/AssociadoSelect.vue";
+import DependenteSelect from "src/components/selects/DependenteSelect.vue";
 import PartnerAgreementSelect from "src/components/selects/PartnerAgreementSelect.vue";
 import PartnerAgreementServiceSelect from "src/components/selects/PartnerAgreementServiceSelect.vue";
 import ApproveAppointmentDialog from "src/components/ApproveAppointmentDialog.vue";
@@ -216,6 +268,7 @@ import {
   createAppointment,
   approveAppointment,
   rejectAppointment,
+  downloadAppointmentGuide,
 } from "src/api/appointment";
 
 const $q = useQuasar();
@@ -237,6 +290,8 @@ const counters = ref({
 
 const form = ref({
   associado: null,
+  forDependent: false,
+  dependent: null,
   partner: null,
   service: null,
   date: "",
@@ -244,6 +299,21 @@ const form = ref({
   observations: "",
 });
 
+watch(
+  () => form.value.forDependent,
+  (forDependent) => {
+    if (!forDependent) form.value.dependent = null;
+  },
+);
+
+watch(
+  () => form.value.associado,
+  () => {
+    form.value.forDependent = false;
+    form.value.dependent = null;
+  },
+);
+
 const tabs = computed(() => [
   {
     name: "nova-solicitacao",
@@ -269,6 +339,7 @@ const columnsVisaoGeral = computed(() => [
   { name: "order_number", label: t("agendamento.col.pedido"), field: "order_number", align: "left" },
   { name: "cracha", label: t("associado.cracha"), field: "registration", align: "left" },
   { name: "user_name", label: t("common.terms.name"), field: "user_name", align: "left" },
+  { name: "dependent_name", label: t("associado.dependent"), field: "dependent_name", align: "left" },
   { name: "partner_name", label: t("agendamento.col.parceiro"), field: "partner_name", align: "left" },
   { name: "service_name", label: t("agendamento.col.servico"), field: "service_name", align: "left" },
   { name: "requested_at", label: t("agendamento.col.solicitacao"), field: "requested_at", align: "left" },
@@ -279,9 +350,11 @@ const columnsVisaoGeral = computed(() => [
 const columnsAprovados = computed(() => [
   { name: "order_number", label: t("agendamento.col.pedido"), field: "order_number", align: "left" },
   { name: "user_name", label: t("common.terms.name"), field: "user_name", align: "left" },
+  { name: "dependent_name", label: t("associado.dependent"), field: "dependent_name", align: "left" },
   { name: "partner_name", label: t("agendamento.col.parceiro"), field: "partner_name", align: "left" },
   { name: "service_name", label: t("agendamento.col.servico"), field: "service_name", align: "left" },
   { name: "requested_at", label: t("agendamento.col.solicitacao"), field: "requested_at", align: "left" },
+  { name: "guia", label: t("agendamento.guia"), field: "guia", align: "center" },
   { name: "status", label: t("common.terms.status"), field: "status", align: "center" },
 ]);
 
@@ -316,6 +389,8 @@ const loadCounters = async () => {
 const resetForm = async () => {
   form.value = {
     associado: null,
+    forDependent: false,
+    dependent: null,
     partner: null,
     service: null,
     date: "",
@@ -333,6 +408,7 @@ const submitAppointment = async () => {
   try {
     await createAppointment({
       user_id: form.value.associado.value,
+      user_dependent_id: form.value.dependent?.value ?? null,
       partner_agreement_id: form.value.partner.value,
       partner_agreement_service_id: form.value.service.value,
       time: form.value.time,
@@ -390,6 +466,20 @@ const onReject = (row) => {
   });
 };
 
+const onDownloadGuide = async (row) => {
+  if (!row.can_issue_guide) return;
+  actionId.value = row.id;
+  actionType.value = "guide";
+  try {
+    await downloadAppointmentGuide(row.id, row.order_number);
+  } catch (e) {
+    $q.notify({ type: "negative", message: e?.message || t("http.errors.failed") });
+  } finally {
+    actionId.value = null;
+    actionType.value = null;
+  }
+};
+
 onMounted(() => {
   loadCounters();
 });

+ 57 - 4
src/pages/associado/agendamentos/AgendamentosPage.vue

@@ -28,6 +28,20 @@
           <q-card-section>
             <q-form ref="appointmentFormRef" @submit="submitAppointment">
               <div class="row q-col-gutter-sm">
+                <div class="col-12">
+                  <q-checkbox
+                    v-model="forDependent"
+                    color="violet-normal"
+                    :label="$t('agendamento.for_dependent_self')"
+                  />
+                </div>
+                <DependenteSelect
+                  v-if="forDependent"
+                  v-model="selectedDependent"
+                  :user-id="user.user?.id ?? null"
+                  :rules="[inputRules.required]"
+                  class="col-12 input-violet"
+                />
                 <PartnerAgreementSelect
                   v-model="selectedPartner"
                   :label="$t('ui.navigation.convenios')"
@@ -111,11 +125,15 @@
                 dense
                 round
                 flat
-                icon="mdi-calendar"
+                icon="mdi-file-document-outline"
                 color="violet-normal"
                 size="sm"
-                disable
-              />
+                :disable="!props.row.can_issue_guide"
+                :loading="guideId === props.row.id"
+                @click="onDownloadGuide(props.row)"
+              >
+                <q-tooltip>{{ $t('agendamento.gerar_guia') }}</q-tooltip>
+              </q-btn>
             </q-td>
           </template>
         </q-table>
@@ -138,14 +156,22 @@
 
 <script setup>
 import { ref, reactive, computed, watch, useTemplateRef } from "vue";
+import { useQuasar } from "quasar";
 import { useI18n } from "vue-i18n";
-import { createAppointment, getMyAppointments, updateAppointment } from "src/api/appointment";
+import {
+  createAppointment,
+  getMyAppointments,
+  updateAppointment,
+  downloadMyAppointmentGuide,
+} from "src/api/appointment";
 import { useInputRules } from "src/composables/useInputRules";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import DefaultInput from "src/components/defaults/DefaultInput.vue";
 import PartnerAgreementSelect from "src/components/selects/PartnerAgreementSelect.vue";
+import DependenteSelect from "src/components/selects/DependenteSelect.vue";
 import PartnerAgreementServiceSelect from "src/components/selects/PartnerAgreementServiceSelect.vue";
 import { userStore } from "src/stores/user";
+const $q = useQuasar();
 const { t } = useI18n();
 const { inputRules } = useInputRules();
 const appointmentFormRef = useTemplateRef("appointmentFormRef");
@@ -168,14 +194,22 @@ 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: "dependente", label: t("associado.dependent"), field: (row) => row.user_dependent?.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 forDependent = ref(false);
+const selectedDependent = ref(null);
 const selectedPartner = ref(null);
 const selectedService = ref(null);
+const guideId = ref(null);
+
+watch(forDependent, (value) => {
+  if (!value) selectedDependent.value = null;
+});
 const appointmentForm = reactive({ observations: "" });
 const submitting = ref(false);
 const editingId = ref(null);
@@ -227,6 +261,8 @@ watch(activeTab, (val) => {
 });
 
 const resetForm = () => {
+  forDependent.value = false;
+  selectedDependent.value = null;
   selectedPartner.value = null;
   selectedService.value = null;
   appointmentForm.observations = "";
@@ -240,6 +276,7 @@ const submitAppointment = async () => {
   const payload = {
     partner_agreement_id: selectedPartner.value.value,
     partner_agreement_service_id: selectedService.value.value,
+    user_dependent_id: selectedDependent.value?.value ?? null,
     observations: appointmentForm.observations,
   };
   try {
@@ -258,8 +295,24 @@ const submitAppointment = async () => {
   }
 };
 
+const onDownloadGuide = async (apt) => {
+  if (!apt.can_issue_guide) return;
+  guideId.value = apt.id;
+  try {
+    await downloadMyAppointmentGuide(apt.id, apt.order_number);
+  } catch (e) {
+    $q.notify({ type: "negative", message: e?.message || t("http.errors.failed") });
+  } finally {
+    guideId.value = null;
+  }
+};
+
 const onEditAppointment = (apt) => {
   editingId.value = apt.id;
+  forDependent.value = !!apt.user_dependent_id;
+  selectedDependent.value = apt.user_dependent_id
+    ? { value: apt.user_dependent_id, label: apt.user_dependent?.name || "" }
+    : null;
   selectedPartner.value = {
     value: apt.partner_agreement_id,
     label: apt.partner_agreement?.trade_name || apt.partner_agreement?.company_name || "",

+ 15 - 0
src/pages/company-settings/CompanySettingsPage.vue

@@ -175,6 +175,20 @@
                 class="col-md-4 col-12"
               />
 
+              <div class="col-12">
+                <div class="text-subtitle2 q-mb-sm">{{ $t("company_settings.section.guide") }}</div>
+              </div>
+
+              <DefaultInput
+                v-model="form.guide_validity_days"
+                v-model:error="validationErrors.guide_validity_days"
+                :label="$t('company_settings.guide.validity_days')"
+                type="number"
+                min="1"
+                max="365"
+                class="col-md-4 col-12"
+              />
+
               <div class="col-12 flex justify-end">
                 <q-btn
                   dense
@@ -413,6 +427,7 @@ const { form, getUpdatedFields, hasUpdatedFields } = useFormUpdateTracker({
   contact_email: "",
   contact_phone: "",
   contact_location: "",
+  guide_validity_days: 30,
 });
 
 const { loading: savingSettings, validationErrors, execute: submitForm } = useSubmitHandler({

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

@@ -90,6 +90,19 @@
                   :loading="actionId === props.row.id && actionType === 'reject'"
                   @click="onReject(props.row)"
                 />
+                <q-btn
+                  dense
+                  round
+                  outline
+                  icon="mdi-file-document-outline"
+                  color="primary"
+                  size="sm"
+                  :disable="!props.row.can_issue_guide"
+                  :loading="actionId === props.row.id && actionType === 'guide'"
+                  @click="onDownloadGuide(props.row)"
+                >
+                  <q-tooltip>{{ $t('agendamento.gerar_guia') }}</q-tooltip>
+                </q-btn>
               </div>
             </q-td>
           </template>
@@ -107,6 +120,7 @@ import {
   getAppointmentsByUser,
   approveAppointment,
   rejectAppointment,
+  downloadAppointmentGuide,
 } from "src/api/appointment";
 import { permissionStore } from "src/stores/permission";
 import { excerpt } from "src/helpers/utils";
@@ -134,6 +148,7 @@ 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: "dependente",   label: t("associado.dependent"),         field: (row) => row.user_dependent?.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" },
@@ -179,6 +194,7 @@ const onApprove = (row) => {
       row.status = "confirmado";
       row.date   = date;
       row.time   = time;
+      row.can_issue_guide = true;
       $q.notify({ type: "positive", message: t("http.success") });
     } catch {
       $q.notify({ type: "negative", message: t("http.errors.failed") });
@@ -216,6 +232,20 @@ const onReject = (row) => {
   });
 };
 
+const onDownloadGuide = async (row) => {
+  if (!row.can_issue_guide) return;
+  actionId.value   = row.id;
+  actionType.value = "guide";
+  try {
+    await downloadAppointmentGuide(row.id, row.order_number);
+  } catch (e) {
+    $q.notify({ type: "negative", message: e?.message || t("http.errors.failed") });
+  } finally {
+    actionId.value   = null;
+    actionType.value = null;
+  }
+};
+
 onMounted(async () => {
   try {
     appointments.value = await getAppointmentsByUser(associado.id);

+ 32 - 0
src/pages/parceiros-convenios/AgendamentosParceiroPage.vue

@@ -93,6 +93,19 @@
                   :loading="actionId === props.row.id && actionType === 'reject'"
                   @click.prevent.stop="onReject(props.row)"
                 />
+                <q-btn
+                  dense
+                  round
+                  outline
+                  icon="mdi-file-document-outline"
+                  color="primary"
+                  size="sm"
+                  :disable="!props.row.can_issue_guide"
+                  :loading="actionId === props.row.id && actionType === 'guide'"
+                  @click.prevent.stop="onDownloadGuide(props.row)"
+                >
+                  <q-tooltip>{{ $t('agendamento.gerar_guia') }}</q-tooltip>
+                </q-btn>
               </div>
             </q-td>
           </template>
@@ -122,6 +135,7 @@ import {
   getPartnerAppointments,
   approveAppointmentParceiro,
   rejectAppointmentParceiro,
+  downloadPartnerAppointmentGuide,
 } from "src/api/appointment";
 import { excerpt } from "src/helpers/utils";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
@@ -146,6 +160,7 @@ 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: "dependente", label: t("associado.dependent"),         field: (row) => row.user_dependent?.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" },
@@ -206,6 +221,9 @@ const onApprove = (row) => {
     try {
       await approveAppointmentParceiro(row.id, { date, time });
       row.status = "confirmado";
+      row.date   = date;
+      row.time   = time;
+      row.can_issue_guide = true;
       $q.notify({ type: "positive", message: t("http.success") });
     } catch {
       $q.notify({ type: "negative", message: t("http.errors.failed") });
@@ -239,6 +257,20 @@ const onReject = (row) => {
   });
 };
 
+const onDownloadGuide = async (row) => {
+  if (!row.can_issue_guide) return;
+  actionId.value   = row.id;
+  actionType.value = "guide";
+  try {
+    await downloadPartnerAppointmentGuide(row.id, row.order_number);
+  } catch (e) {
+    $q.notify({ type: "negative", message: e?.message || t("http.errors.failed") });
+  } finally {
+    actionId.value   = null;
+    actionType.value = null;
+  }
+};
+
 onMounted(async () => {
   try {
     appointments.value = await getPartnerAppointments();