فهرست منبع

WIP - guias dos agendamentos de consultas

Gustavo Zanatta 2 روز پیش
والد
کامیت
9f3ace3e5f

+ 31 - 2
package-lock.json

@@ -1,17 +1,19 @@
 {
 {
   "name": "serprati-digital",
   "name": "serprati-digital",
-  "version": "1.0.2",
+  "version": "1.0.4",
   "lockfileVersion": 3,
   "lockfileVersion": 3,
   "requires": true,
   "requires": true,
   "packages": {
   "packages": {
     "": {
     "": {
       "name": "serprati-digital",
       "name": "serprati-digital",
-      "version": "1.0.2",
+      "version": "1.0.4",
       "dependencies": {
       "dependencies": {
         "@bufbuild/protobuf": "^2.5.1",
         "@bufbuild/protobuf": "^2.5.1",
         "@capacitor/app": "^8.1.0",
         "@capacitor/app": "^8.1.0",
         "@capacitor/core": "^8.3.4",
         "@capacitor/core": "^8.3.4",
+        "@capacitor/filesystem": "^8.1.2",
         "@capacitor/keyboard": "^8.0.3",
         "@capacitor/keyboard": "^8.0.3",
+        "@capacitor/share": "^8.0.1",
         "@capacitor/status-bar": "^8.0.2",
         "@capacitor/status-bar": "^8.0.2",
         "@quasar/cli": "^2.5.0",
         "@quasar/cli": "^2.5.0",
         "@quasar/extras": "^1.17.0",
         "@quasar/extras": "^1.17.0",
@@ -329,6 +331,18 @@
         "tslib": "^2.1.0"
         "tslib": "^2.1.0"
       }
       }
     },
     },
+    "node_modules/@capacitor/filesystem": {
+      "version": "8.1.2",
+      "resolved": "https://registry.npmjs.org/@capacitor/filesystem/-/filesystem-8.1.2.tgz",
+      "integrity": "sha512-doaaMfGoFR2hWU6aV6u83I+5ZsGyJVq+Gz4r9lMpJzUKMm1eMu0hLnFdV1aXZlU9FlK/RndFrVD8oRZfNOqWgQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@capacitor/synapse": "^1.0.4"
+      },
+      "peerDependencies": {
+        "@capacitor/core": ">=8.0.0"
+      }
+    },
     "node_modules/@capacitor/keyboard": {
     "node_modules/@capacitor/keyboard": {
       "version": "8.0.3",
       "version": "8.0.3",
       "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-8.0.3.tgz",
       "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-8.0.3.tgz",
@@ -338,6 +352,15 @@
         "@capacitor/core": ">=8.0.0"
         "@capacitor/core": ">=8.0.0"
       }
       }
     },
     },
+    "node_modules/@capacitor/share": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/@capacitor/share/-/share-8.0.1.tgz",
+      "integrity": "sha512-3cSBKBCJVon54rKDROP2rqGyeGks4pBh9TbaEk9S375Kbek/ZHe72N50zIa0Vn9Eac/SuhwgehO/mmA4CsUOiw==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@capacitor/core": ">=8.0.0"
+      }
+    },
     "node_modules/@capacitor/status-bar": {
     "node_modules/@capacitor/status-bar": {
       "version": "8.0.2",
       "version": "8.0.2",
       "resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-8.0.2.tgz",
       "resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-8.0.2.tgz",
@@ -347,6 +370,12 @@
         "@capacitor/core": ">=8.0.0"
         "@capacitor/core": ">=8.0.0"
       }
       }
     },
     },
+    "node_modules/@capacitor/synapse": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@capacitor/synapse/-/synapse-1.0.4.tgz",
+      "integrity": "sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==",
+      "license": "ISC"
+    },
     "node_modules/@esbuild/aix-ppc64": {
     "node_modules/@esbuild/aix-ppc64": {
       "version": "0.25.12",
       "version": "0.25.12",
       "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
       "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",

+ 2 - 0
package.json

@@ -22,7 +22,9 @@
     "@bufbuild/protobuf": "^2.5.1",
     "@bufbuild/protobuf": "^2.5.1",
     "@capacitor/app": "^8.1.0",
     "@capacitor/app": "^8.1.0",
     "@capacitor/core": "^8.3.4",
     "@capacitor/core": "^8.3.4",
+    "@capacitor/filesystem": "^8.1.2",
     "@capacitor/keyboard": "^8.0.3",
     "@capacitor/keyboard": "^8.0.3",
+    "@capacitor/share": "^8.0.1",
     "@capacitor/status-bar": "^8.0.2",
     "@capacitor/status-bar": "^8.0.2",
     "@quasar/cli": "^2.5.0",
     "@quasar/cli": "^2.5.0",
     "@quasar/extras": "^1.17.0",
     "@quasar/extras": "^1.17.0",

+ 31 - 2
src-capacitor/package-lock.json

@@ -1,19 +1,21 @@
 {
 {
   "name": "serprati-digital",
   "name": "serprati-digital",
-  "version": "1.0.3",
+  "version": "1.0.4",
   "lockfileVersion": 3,
   "lockfileVersion": 3,
   "requires": true,
   "requires": true,
   "packages": {
   "packages": {
     "": {
     "": {
       "name": "serprati-digital",
       "name": "serprati-digital",
-      "version": "1.0.3",
+      "version": "1.0.4",
       "dependencies": {
       "dependencies": {
         "@capacitor/android": "^8.3.4",
         "@capacitor/android": "^8.3.4",
         "@capacitor/app": "^8.1.0",
         "@capacitor/app": "^8.1.0",
         "@capacitor/cli": "8.3.4",
         "@capacitor/cli": "8.3.4",
         "@capacitor/core": "8.3.4",
         "@capacitor/core": "8.3.4",
+        "@capacitor/filesystem": "^8.1.2",
         "@capacitor/haptics": "^8.0.2",
         "@capacitor/haptics": "^8.0.2",
         "@capacitor/keyboard": "^8.0.3",
         "@capacitor/keyboard": "^8.0.3",
+        "@capacitor/share": "^8.0.1",
         "@capacitor/splash-screen": "^8.0.1",
         "@capacitor/splash-screen": "^8.0.1",
         "@capacitor/status-bar": "^8.0.2"
         "@capacitor/status-bar": "^8.0.2"
       }
       }
@@ -77,6 +79,18 @@
         "tslib": "^2.1.0"
         "tslib": "^2.1.0"
       }
       }
     },
     },
+    "node_modules/@capacitor/filesystem": {
+      "version": "8.1.2",
+      "resolved": "https://registry.npmjs.org/@capacitor/filesystem/-/filesystem-8.1.2.tgz",
+      "integrity": "sha512-doaaMfGoFR2hWU6aV6u83I+5ZsGyJVq+Gz4r9lMpJzUKMm1eMu0hLnFdV1aXZlU9FlK/RndFrVD8oRZfNOqWgQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@capacitor/synapse": "^1.0.4"
+      },
+      "peerDependencies": {
+        "@capacitor/core": ">=8.0.0"
+      }
+    },
     "node_modules/@capacitor/haptics": {
     "node_modules/@capacitor/haptics": {
       "version": "8.0.2",
       "version": "8.0.2",
       "resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-8.0.2.tgz",
       "resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-8.0.2.tgz",
@@ -95,6 +109,15 @@
         "@capacitor/core": ">=8.0.0"
         "@capacitor/core": ">=8.0.0"
       }
       }
     },
     },
+    "node_modules/@capacitor/share": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/@capacitor/share/-/share-8.0.1.tgz",
+      "integrity": "sha512-3cSBKBCJVon54rKDROP2rqGyeGks4pBh9TbaEk9S375Kbek/ZHe72N50zIa0Vn9Eac/SuhwgehO/mmA4CsUOiw==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@capacitor/core": ">=8.0.0"
+      }
+    },
     "node_modules/@capacitor/splash-screen": {
     "node_modules/@capacitor/splash-screen": {
       "version": "8.0.1",
       "version": "8.0.1",
       "resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-8.0.1.tgz",
       "resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-8.0.1.tgz",
@@ -113,6 +136,12 @@
         "@capacitor/core": ">=8.0.0"
         "@capacitor/core": ">=8.0.0"
       }
       }
     },
     },
+    "node_modules/@capacitor/synapse": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@capacitor/synapse/-/synapse-1.0.4.tgz",
+      "integrity": "sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==",
+      "license": "ISC"
+    },
     "node_modules/@ionic/cli-framework-output": {
     "node_modules/@ionic/cli-framework-output": {
       "version": "2.2.8",
       "version": "2.2.8",
       "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz",
       "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz",

+ 2 - 0
src-capacitor/package.json

@@ -9,8 +9,10 @@
     "@capacitor/app": "^8.1.0",
     "@capacitor/app": "^8.1.0",
     "@capacitor/cli": "8.3.4",
     "@capacitor/cli": "8.3.4",
     "@capacitor/core": "8.3.4",
     "@capacitor/core": "8.3.4",
+    "@capacitor/filesystem": "^8.1.2",
     "@capacitor/haptics": "^8.0.2",
     "@capacitor/haptics": "^8.0.2",
     "@capacitor/keyboard": "^8.0.3",
     "@capacitor/keyboard": "^8.0.3",
+    "@capacitor/share": "^8.0.1",
     "@capacitor/splash-screen": "^8.0.1",
     "@capacitor/splash-screen": "^8.0.1",
     "@capacitor/status-bar": "^8.0.2"
     "@capacitor/status-bar": "^8.0.2"
   }
   }

+ 65 - 0
src/api/appointment.js

@@ -1,4 +1,69 @@
 import api from "src/api";
 import api from "src/api";
+import { Capacitor } from "@capacitor/core";
+import { Filesystem, Directory } from "@capacitor/filesystem";
+import { Share } from "@capacitor/share";
+
+// ─── Guia do agendamento ──────────────────────────────────────────────────────
+
+const blobErrorMessage = async (blob) => {
+  try {
+    return JSON.parse(await blob.text())?.message ?? null;
+  } catch {
+    return null;
+  }
+};
+
+const blobToBase64 = (blob) =>
+  new Promise((resolve, reject) => {
+    const reader = new FileReader();
+    reader.onerror = reject;
+    reader.onload = () => resolve(String(reader.result).split(",")[1]);
+    reader.readAsDataURL(blob);
+  });
+
+const saveNative = async (blob, fileName) => {
+  const { uri } = await Filesystem.writeFile({
+    path: fileName,
+    data: await blobToBase64(blob),
+    directory: Directory.Cache,
+  });
+
+  await Share.share({ title: fileName, url: uri });
+};
+
+const saveWeb = (blob, fileName) => {
+  const blobUrl = window.URL.createObjectURL(blob);
+  const link = document.createElement("a");
+  link.href = blobUrl;
+  link.setAttribute("download", fileName);
+  document.body.appendChild(link);
+  link.click();
+  document.body.removeChild(link);
+  window.URL.revokeObjectURL(blobUrl);
+};
+
+export const downloadMyAppointmentGuide = async (id, orderNumber) => {
+  let response;
+
+  try {
+    response = await api.get(`/associado/appointment/${id}/guide`, { 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 blob = new Blob([response.data], { type: "application/pdf" });
+  const fileName = `guia_${orderNumber}.pdf`;
+
+  if (Capacitor.isNativePlatform()) {
+    await saveNative(blob, fileName);
+    return;
+  }
+
+  saveWeb(blob, fileName);
+};
 
 
 // ─── Rotas do Associado ───────────────────────────────────────────────────────
 // ─── Rotas do Associado ───────────────────────────────────────────────────────
 
 

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

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

@@ -480,6 +480,7 @@
     "import_afastamentos": "Absences",
     "import_afastamentos": "Absences",
     "search_placeholder": "Search Members",
     "search_placeholder": "Search Members",
     "no_dependents": "No dependents registered",
     "no_dependents": "No dependents registered",
+    "no_approved_dependents": "No approved dependents",
     "remove_photo": "Do you want to remove your profile photo?",
     "remove_photo": "Do you want to remove your profile photo?",
     "kinship": "Kinship",
     "kinship": "Kinship",
     "dependent_statuses": {
     "dependent_statuses": {
@@ -733,6 +734,10 @@
     "nova_solicitacao": "New Request",
     "nova_solicitacao": "New Request",
     "visao_geral": "Overview",
     "visao_geral": "Overview",
     "aprovados_automaticamente": "Auto Approved",
     "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",
     "associado": "Associate",
     "associado": "Associate",
     "solicitar": "Request",
     "solicitar": "Request",
     "confirm_approve": "Are you sure you want to approve this appointment?",
     "confirm_approve": "Are you sure you want to approve this appointment?",

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

@@ -480,6 +480,7 @@
     "import_afastamentos": "Ausencias",
     "import_afastamentos": "Ausencias",
     "search_placeholder": "Buscar por asociados",
     "search_placeholder": "Buscar por asociados",
     "no_dependents": "Ningún dependiente registrado",
     "no_dependents": "Ningún dependiente registrado",
+    "no_approved_dependents": "Ningún dependiente aprobado",
     "remove_photo": "¿Deseas eliminar tu foto de perfil?",
     "remove_photo": "¿Deseas eliminar tu foto de perfil?",
     "kinship": "Parentesco",
     "kinship": "Parentesco",
     "dependent_statuses": {
     "dependent_statuses": {
@@ -733,6 +734,10 @@
     "nova_solicitacao": "Nueva Solicitud",
     "nova_solicitacao": "Nueva Solicitud",
     "visao_geral": "Visión General",
     "visao_geral": "Visión General",
     "aprovados_automaticamente": "Aprobados Automáticamente",
     "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",
     "associado": "Asociado",
     "associado": "Asociado",
     "solicitar": "Solicitar",
     "solicitar": "Solicitar",
     "confirm_approve": "¿Estás seguro de que deseas aprobar esta cita?",
     "confirm_approve": "¿Estás seguro de que deseas aprobar esta cita?",

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

@@ -480,6 +480,7 @@
     "import_afastamentos": "Afastamentos",
     "import_afastamentos": "Afastamentos",
     "search_placeholder": "Buscar por associados",
     "search_placeholder": "Buscar por associados",
     "no_dependents": "Nenhum dependente cadastrado",
     "no_dependents": "Nenhum dependente cadastrado",
+    "no_approved_dependents": "Nenhum dependente aprovado",
     "remove_photo": "Deseja remover sua foto de perfil?",
     "remove_photo": "Deseja remover sua foto de perfil?",
     "kinship": "Parentesco",
     "kinship": "Parentesco",
     "dependent_statuses": {
     "dependent_statuses": {
@@ -732,6 +733,10 @@
     "nova_solicitacao": "Nova Solicitação",
     "nova_solicitacao": "Nova Solicitação",
     "visao_geral": "Visão Geral de Agendamentos",
     "visao_geral": "Visão Geral de Agendamentos",
     "aprovados_automaticamente": "Aprovados Automaticamente",
     "aprovados_automaticamente": "Aprovados Automaticamente",
+    "for_dependent": "Agendamento para dependente",
+    "for_dependent_self": "Este agendamento é para um dependente",
+    "guia": "Guia",
+    "gerar_guia": "Gerar guia",
     "associado": "Associado",
     "associado": "Associado",
     "solicitar": "Solicitar",
     "solicitar": "Solicitar",
     "status": {
     "status": {

+ 64 - 2
src/pages/associado/agendamentos/AgendamentosPage.vue

@@ -25,6 +25,18 @@
         <q-card v-else flat class="form-card">
         <q-card v-else flat class="form-card">
           <q-card-section>
           <q-card-section>
             <q-form ref="appointmentFormRef" class="form-column" @submit="submitAppointment">
             <q-form ref="appointmentFormRef" class="form-column" @submit="submitAppointment">
+              <q-checkbox
+                v-model="forDependent"
+                color="violet-normal"
+                :label="$t('agendamento.for_dependent_self')"
+              />
+              <DependenteSelect
+                v-if="forDependent"
+                v-model="selectedDependent"
+                :user-id="user.user?.id ?? null"
+                :rules="[inputRules.required]"
+                class="input-violet"
+              />
               <PartnerAgreementSelect
               <PartnerAgreementSelect
                 v-model="selectedPartner"
                 v-model="selectedPartner"
                 :label="$t('ui.navigation.convenios')"
                 :label="$t('ui.navigation.convenios')"
@@ -99,6 +111,12 @@
               <span class="apmt-card__value">{{ apt.partner_agreement_service?.name || '—' }}</span>
               <span class="apmt-card__value">{{ apt.partner_agreement_service?.name || '—' }}</span>
             </div>
             </div>
 
 
+            <div v-if="apt.user_dependent" class="apmt-card__row">
+              <q-icon name="mdi-account-child-outline" size="15px" class="apmt-card__icon" />
+              <span class="apmt-card__label">{{ $t('associado.dependent') }}:</span>
+              <span class="apmt-card__value">{{ apt.user_dependent.name }}</span>
+            </div>
+
             <div class="apmt-card__row">
             <div class="apmt-card__row">
               <q-icon name="mdi-calendar-clock-outline" size="15px" class="apmt-card__icon" />
               <q-icon name="mdi-calendar-clock-outline" size="15px" class="apmt-card__icon" />
               <span class="apmt-card__value">{{ formatDateTime(apt.date, apt.time) }}</span>
               <span class="apmt-card__value">{{ formatDateTime(apt.date, apt.time) }}</span>
@@ -110,8 +128,9 @@
               <span class="apmt-card__value">{{ formatDate(apt.created_at) }}</span>
               <span class="apmt-card__value">{{ formatDate(apt.created_at) }}</span>
             </div>
             </div>
 
 
-            <div v-if="apt.status === 'pendente'" class="apmt-card__actions">
+            <div v-if="apt.status === 'pendente' || apt.can_issue_guide" class="apmt-card__actions">
               <q-btn
               <q-btn
+                v-if="apt.status === 'pendente'"
                 dense
                 dense
                 flat
                 flat
                 icon="mdi-pencil-outline"
                 icon="mdi-pencil-outline"
@@ -120,6 +139,17 @@
                 :label="$t('common.actions.edit')"
                 :label="$t('common.actions.edit')"
                 @click="onEditAppointment(apt)"
                 @click="onEditAppointment(apt)"
               />
               />
+              <q-btn
+                v-if="apt.can_issue_guide"
+                dense
+                flat
+                icon="mdi-file-document-outline"
+                color="violet-normal"
+                size="sm"
+                :label="$t('agendamento.guia')"
+                :loading="guideId === apt.id"
+                @click="onDownloadGuide(apt)"
+              />
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
@@ -144,11 +174,17 @@
 import { ref, reactive, computed, watch, useTemplateRef } from "vue";
 import { ref, reactive, computed, watch, useTemplateRef } from "vue";
 import { useQuasar } from "quasar";
 import { useQuasar } from "quasar";
 import { useI18n } from "vue-i18n";
 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 { useInputRules } from "src/composables/useInputRules";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import DefaultInput from "src/components/defaults/DefaultInput.vue";
 import DefaultInput from "src/components/defaults/DefaultInput.vue";
 import PartnerAgreementSelect from "src/components/selects/PartnerAgreementSelect.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 PartnerAgreementServiceSelect from "src/components/selects/PartnerAgreementServiceSelect.vue";
 import { userStore } from "src/stores/user";
 import { userStore } from "src/stores/user";
 
 
@@ -172,8 +208,15 @@ const tabs = computed(() => [
   { name: "meus", label: t("associado.my_appointments") },
   { name: "meus", label: t("associado.my_appointments") },
 ]);
 ]);
 
 
+const forDependent     = ref(false);
+const selectedDependent = ref(null);
 const selectedPartner  = ref(null);
 const selectedPartner  = ref(null);
 const selectedService  = ref(null);
 const selectedService  = ref(null);
+const guideId          = ref(null);
+
+watch(forDependent, (value) => {
+  if (!value) selectedDependent.value = null;
+});
 const appointmentForm  = reactive({ observations: "" });
 const appointmentForm  = reactive({ observations: "" });
 const submitting       = ref(false);
 const submitting       = ref(false);
 const editingId        = ref(null);
 const editingId        = ref(null);
@@ -224,6 +267,8 @@ watch(activeTab, (val) => {
 });
 });
 
 
 const resetForm = () => {
 const resetForm = () => {
+  forDependent.value           = false;
+  selectedDependent.value      = null;
   selectedPartner.value        = null;
   selectedPartner.value        = null;
   selectedService.value        = null;
   selectedService.value        = null;
   appointmentForm.observations = "";
   appointmentForm.observations = "";
@@ -237,6 +282,7 @@ const submitAppointment = async () => {
   const payload = {
   const payload = {
     partner_agreement_id:         selectedPartner.value.value,
     partner_agreement_id:         selectedPartner.value.value,
     partner_agreement_service_id: selectedService.value.value,
     partner_agreement_service_id: selectedService.value.value,
+    user_dependent_id:            selectedDependent.value?.value ?? null,
     observations: appointmentForm.observations,
     observations: appointmentForm.observations,
   };
   };
   try {
   try {
@@ -256,8 +302,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) => {
 const onEditAppointment = (apt) => {
   editingId.value              = apt.id;
   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 || "" };
   selectedPartner.value        = { value: apt.partner_agreement_id, label: apt.partner_agreement?.trade_name || apt.partner_agreement?.company_name || "" };
   selectedService.value        = { value: apt.partner_agreement_service_id, label: apt.partner_agreement_service?.name || "" };
   selectedService.value        = { value: apt.partner_agreement_service_id, label: apt.partner_agreement_service?.name || "" };
   appointmentForm.observations = apt.observations ?? "";
   appointmentForm.observations = apt.observations ?? "";