Selaa lähdekoodia

funcao de afastamento + usuario afastado n consegue criar agendamento + usuario ativo cria agendamento aprovado automaticamente + associado afastado nao retorna na validacao de carteirinha

Gustavo Zanatta 1 kuukausi sitten
vanhempi
commit
7c48edf435

+ 5 - 0
src/api/user.js

@@ -66,6 +66,11 @@ export const getUsersPaginated = async ({ page = 1, perPage = 10, filter, type,
 export const getAssociadosPaginated = async (params = {}) =>
   getUsersPaginated({ ...params, type: "associado" });
 
+export const setUserOnLeave = async (id) => {
+  const { data } = await api.put(`/user/${id}/on-leave`);
+  return data.payload;
+};
+
 export const importAssociados = async (file) => {
   const formData = new FormData();
   formData.append("file", file);

+ 5 - 2
src/helpers/utils.js

@@ -7,6 +7,7 @@ import { useI18n } from "vue-i18n";
  * @returns {string} string cortada.
  */
 const excerpt = (string, size = 30) => {
+  console.log(size)
   if (size == null) return string;
   if (string.length > size) {
     string = string.substring(0, size) + "...";
@@ -105,8 +106,9 @@ const normalizeString = (val) =>
 
 const getStatusColor = (status) => {
   const s = typeof status === "object" ? status?.value : status;
-  if (s === "active")  return "positive";
-  if (s === "pending") return "warning";
+  if (s === "active")   return "positive";
+  if (s === "pending")  return "warning";
+  if (s === "on_leave") return "orange";
   if (s === "inactive") return "inactive";
   return "grey-6";
 };
@@ -116,6 +118,7 @@ const getStatusI18nKey = (status) => {
   if (s === "active")   return "common.status.active";
   if (s === "inactive") return "common.status.inactive";
   if (s === "pending")  return "common.status.pending";
+  if (s === "on_leave") return "common.status.on_leave";
   return "common.status.inactive";
 };
 

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

@@ -140,6 +140,7 @@
       "active": "Active",
       "inactive": "Inactive",
       "pending": "Pending",
+      "on_leave": "On Leave",
       "canceled": "Canceled",
       "loading": "Please wait...",
       "yes": "Yes",
@@ -426,6 +427,9 @@
     "dependent": "Dependent",
     "dependents": "Dependents",
     "import_afastamentos": "Absences",
+    "set_on_leave_title": "Set member on leave",
+    "set_on_leave_message": "Confirm putting {name} on leave?",
+    "set_on_leave_confirm": "Confirm leave",
     "import_associados": "Associates",
     "import_sync_result": "{created} created · {updated} updated · {inactivated} inactivated",
     "import_processing": "Import is still processing. Please refresh the page in a moment.",
@@ -467,6 +471,7 @@
     "my_appointments": "My Appointments",
     "notes": "Notes",
     "schedule": "Schedule",
+    "on_leave_appointment": "You are on leave. Please contact the secretariat for more information.",
     "confirm_cancel_appointment": "Are you sure you want to cancel this appointment?",
     "appointment_status": {
       "pendente": "Pending",

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

@@ -141,6 +141,7 @@
       "active": "Activo",
       "inactive": "Inactivo",
       "pending": "Pendiente",
+      "on_leave": "De Baja",
       "canceled": "Cancelado",
       "loading": "Por favor espere...",
       "yes": "Sí",
@@ -427,6 +428,9 @@
     "dependent": "Dependiente",
     "dependents": "Dependientes",
     "import_afastamentos": "Ausencias",
+    "set_on_leave_title": "Dar de baja al asociado",
+    "set_on_leave_message": "¿Confirma la baja de {name}?",
+    "set_on_leave_confirm": "Confirmar baja",
     "import_associados": "Asociados",
     "import_sync_result": "{created} creados · {updated} actualizados · {inactivated} inactivados",
     "import_processing": "La importación está en progreso. Actualice la página en unos instantes.",
@@ -468,6 +472,7 @@
     "my_appointments": "Mis Citas",
     "notes": "Observaciones",
     "schedule": "Agendar",
+    "on_leave_appointment": "Está de baja. Comuníquese con la secretaría para más información.",
     "confirm_cancel_appointment": "¿Está seguro de que desea cancelar esta cita?",
     "appointment_status": {
       "pendente": "Pendiente",

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

@@ -141,6 +141,7 @@
       "active": "Ativo",
       "inactive": "Inativo",
       "pending": "Pendente",
+      "on_leave": "Afastado",
       "canceled": "Cancelado",
       "loading": "Por favor, aguarde...",
       "yes": "Sim",
@@ -427,6 +428,9 @@
     "dependent": "Dependente",
     "dependents": "Dependentes",
     "import_afastamentos": "Afastamentos",
+    "set_on_leave_title": "Afastar associado",
+    "set_on_leave_message": "Confirma o afastamento de {name}?",
+    "set_on_leave_confirm": "Confirmar afastamento",
     "import_associados": "Associados",
     "import_sync_result": "{created} criados · {updated} atualizados · {inactivated} inativados",
     "import_processing": "Importação em andamento. Atualize a página em alguns instantes.",
@@ -468,6 +472,7 @@
     "my_appointments": "Meus Agendamentos",
     "notes": "Observações",
     "schedule": "Agendar",
+    "on_leave_appointment": "Você está afastado. Entre em contato com a secretaria para mais informações.",
     "confirm_cancel_appointment": "Tem certeza que deseja cancelar este agendamento?",
     "appointment_status": {
       "pendente": "Pendente",

+ 13 - 0
src/pages/agendamentos/AppointmentsAdminPage.vue

@@ -118,6 +118,12 @@
         :add-item="false"
         :show-search-field="true"
       >
+        <template #body-cell-service_name="{ row }">
+          <q-td>
+            {{ excerpt(row.service_name || '—', 35) }}
+            <q-tooltip v-if="row.service_name">{{ row.service_name }}</q-tooltip>
+          </q-td>
+        </template>
         <template #body-cell-status="{ row }">
           <q-td class="text-center">
             <q-chip
@@ -167,6 +173,12 @@
         :add-item="false"
         :show-search-field="true"
       >
+        <template #body-cell-service_name="{ row }">
+          <q-td>
+            {{ excerpt(row.service_name || '—', 35) }}
+            <q-tooltip v-if="row.service_name">{{ row.service_name }}</q-tooltip>
+          </q-td>
+        </template>
         <template #body-cell-status>
           <q-td class="text-center">
             <q-chip
@@ -187,6 +199,7 @@ import { ref, computed, onMounted, useTemplateRef, nextTick } from "vue";
 import { useQuasar } from "quasar";
 import { useI18n } from "vue-i18n";
 import { useInputRules } from "src/composables/useInputRules";
+import { excerpt } from "src/helpers/utils";
 
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import DefaultTableServerSide from "src/components/defaults/DefaultTableServerSide.vue";

+ 15 - 25
src/pages/associado/agendamentos/AgendamentosPage.vue

@@ -17,7 +17,14 @@
 
       <!-- Tab: Nova Solicitação -->
       <div v-if="activeTab === 'novo'" class="q-mt-md">
-        <q-card flat bordered >
+        <q-banner v-if="isOnLeave" rounded class="bg-orange-1 text-orange-9 q-mb-md">
+          <template #avatar>
+            <q-icon name="mdi-account-off-outline" color="orange" />
+          </template>
+          {{ $t('associado.on_leave_appointment') }}
+        </q-banner>
+
+        <q-card v-if="!isOnLeave" flat bordered>
           <q-card-section>
             <q-form ref="appointmentFormRef" @submit="submitAppointment">
               <div class="row q-col-gutter-sm">
@@ -36,22 +43,6 @@
                   class="col-12 input-violet"
                   for-associado
                 />
-                <DefaultInputDatePicker
-                  v-model:untreated-date="appointmentForm.date"
-                  :label="$t('common.terms.date')"
-                  :rules="[inputRules.required]"
-                  class="col-12 col-md-6 input-violet"
-                  placeholder="dd/mm/aaaa"
-                  lazy-rules
-                />
-                <DefaultInput
-                  v-model="appointmentForm.time"
-                  :label="$t('common.terms.hour')"
-                  :rules="[inputRules.required]"
-                  mask="##:##"
-                  placeholder="HH:MM"
-                  class="col-12 col-md-6 input-violet"
-                />
                 <DefaultInput
                   v-model="appointmentForm.observations"
                   :label="$t('associado.notes')"
@@ -152,7 +143,6 @@ import { createAppointment, getMyAppointments, updateAppointment } from "src/api
 import { useInputRules } from "src/composables/useInputRules";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import DefaultInput from "src/components/defaults/DefaultInput.vue";
-import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
 import PartnerAgreementSelect from "src/components/selects/PartnerAgreementSelect.vue";
 import PartnerAgreementServiceSelect from "src/components/selects/PartnerAgreementServiceSelect.vue";
 import { userStore } from "src/stores/user";
@@ -160,6 +150,12 @@ const { t } = useI18n();
 const { inputRules } = useInputRules();
 const appointmentFormRef = useTemplateRef("appointmentFormRef");
 const user = userStore();
+
+const isOnLeave = computed(() => {
+  const s = user.user?.status;
+  return s === "on_leave" || s?.value === "on_leave";
+});
+
 const activeTab = ref("novo");
 const PER_PAGE = 12;
 const currentPage = ref(1);
@@ -180,7 +176,7 @@ const columns = computed(() => [
 
 const selectedPartner = ref(null);
 const selectedService = ref(null);
-const appointmentForm = reactive({ date: "", time: "", observations: "" });
+const appointmentForm = reactive({ observations: "" });
 const submitting = ref(false);
 const editingId = ref(null);
 
@@ -233,8 +229,6 @@ watch(activeTab, (val) => {
 const resetForm = () => {
   selectedPartner.value = null;
   selectedService.value = null;
-  appointmentForm.date = "";
-  appointmentForm.time = "";
   appointmentForm.observations = "";
   editingId.value = null;
 };
@@ -246,8 +240,6 @@ const submitAppointment = async () => {
   const payload = {
     partner_agreement_id: selectedPartner.value.value,
     partner_agreement_service_id: selectedService.value.value,
-    date: appointmentForm.date,
-    time: appointmentForm.time,
     observations: appointmentForm.observations,
   };
   try {
@@ -276,8 +268,6 @@ const onEditAppointment = (apt) => {
     value: apt.partner_agreement_service_id,
     label: apt.partner_agreement_service?.name || "",
   };
-  appointmentForm.date = apt.date ?? "";
-  appointmentForm.time = apt.time ?? "";
   appointmentForm.observations = apt.observations ?? "";
   activeTab.value = "novo";
 };

+ 34 - 6
src/pages/gestao-associados/GestaoAssociadosPage.vue

@@ -47,12 +47,18 @@
       open-item
       @on-row-click="onRowClick"
     >
-      <template #body-cell-actions>
+      <template #body-cell-actions="{ row }">
         <div class="row no-wrap items-center" style="gap: 4px">
-          <q-btn flat round dense color="violet-normal" icon="mdi-pencil" />
-          <q-btn flat round dense color="violet-normal" icon="mdi-account-minus" />
-          <q-btn flat round dense color="violet-normal" icon="mdi-calendar" />
-          <q-btn flat round dense color="violet-normal" icon="mdi-sofa" />
+          <q-btn flat round dense color="violet-normal" icon="mdi-pencil" @click.stop="onRowClick({ row })" />
+          <q-btn
+            flat round dense
+            :color="row.status === 'on_leave' || row.status?.value === 'on_leave' ? 'grey-4' : 'violet-normal'"
+            icon="mdi-account-minus"
+            :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 />
         </div>
       </template>
 
@@ -78,7 +84,7 @@ import { ref, computed, defineAsyncComponent, useTemplateRef } from "vue";
 import { useQuasar } from "quasar";
 import { useI18n } from "vue-i18n";
 import { permissionStore } from "src/stores/permission";
-import { getAssociadosPaginated, importAssociados } from "src/api/user";
+import { getAssociadosPaginated, importAssociados, setUserOnLeave } from "src/api/user";
 import { getStatusColor, getStatusI18nKey } from "src/helpers/utils";
 import { useImportPoller } from "src/composables/useImportPoller";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
@@ -180,6 +186,28 @@ const onRowClick = async ({ row }) => {
   });
 };
 
+const onSetOnLeave = (row) => {
+  if (!permission_store.getAccess("associado", "edit")) {
+    $q.notify({ type: "negative", message: t("validation.permissions.edit") });
+    return;
+  }
+
+  $q.dialog({
+    title: t("associado.set_on_leave_title"),
+    message: t("associado.set_on_leave_message", { name: row.name }),
+    cancel: { flat: true, label: t("common.actions.cancel"), color: "grey-7" },
+    ok: { unelevated: true, label: t("associado.set_on_leave_confirm"), color: "violet-normal" },
+    persistent: true,
+  }).onOk(async () => {
+    try {
+      await setUserOnLeave(row.id);
+      tableRef.value?.refresh();
+    } catch {
+      // silent
+    }
+  });
+};
+
 const onImportClick = () => {
   if (!permission_store.getAccess("associado", "add")) {
     $q.notify({ type: "negative", message: t("validation.permissions.add") });

+ 1 - 0
src/pages/gestao-associados/components/AddEditAssociadoDialog.vue

@@ -153,6 +153,7 @@ const statusOptions = computed(() => [
   { label: t("common.status.active"),   value: "active" },
   { label: t("common.status.inactive"), value: "inactive" },
   { label: t("common.status.pending"),  value: "pending" },
+  { label: t("common.status.on_leave"), value: "on_leave" },
 ]);
 
 const { form, getUpdatedFields, hasUpdatedFields } = useFormUpdateTracker({

+ 17 - 1
src/pages/parceiros-convenios/AgendamentosParceiroPage.vue

@@ -46,6 +46,15 @@
           hide-pagination
           :rows-per-page-options="[0]"
         >
+          <template #body-cell-servico="props">
+            <q-td :props="props">
+              {{ excerpt(props.row.partner_agreement_service?.name || '—', excerptSize) }}
+              <q-tooltip v-if="props.row.partner_agreement_service?.name">
+                {{ props.row.partner_agreement_service.name }}
+              </q-tooltip>
+            </q-td>
+          </template>
+
           <template #body-cell-status="props">
             <q-td :props="props">
               <q-chip
@@ -112,12 +121,19 @@ import {
   approveAppointmentParceiro,
   rejectAppointmentParceiro,
 } from "src/api/appointment";
+import { excerpt } from "src/helpers/utils";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import ApproveAppointmentDialog from "src/components/ApproveAppointmentDialog.vue";
 
 const $q = useQuasar();
 const { t } = useI18n();
 
+const excerptSize = computed(() => {
+  if ($q.screen.lt.md) return 20;
+  if ($q.screen.md)    return 30;
+  return 45;
+});
+
 const loading = ref(true);
 const appointments = ref([]);
 const currentPage = ref(1);
@@ -128,7 +144,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: "servico",    label: t("agendamento.col.servico"),     field: (row) => row.partner_agreement_service?.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" },
   { name: "acoes",      label: t("common.terms.actions"),        field: "id",                                                                        align: "center" },