Quellcode durchsuchen

ajuste: dependentes na gestao associado

Gustavo Zanatta vor 2 Wochen
Ursprung
Commit
0f810056d5

+ 25 - 0
src/composables/useDependentStatus.js

@@ -0,0 +1,25 @@
+import { useI18n } from "vue-i18n";
+
+export const useDependentStatus = () => {
+  const { t } = useI18n();
+
+  const dependentStatusColor = (status) => {
+    switch (status) {
+      case "approved": return "positive";
+      case "refused":  return "negative";
+      case "pending":  return "warning-light";
+      default:         return "grey";
+    }
+  };
+
+  const dependentStatusLabel = (status) => {
+    const map = {
+      approved: t("associado.dependent_statuses.approved"),
+      refused:  t("associado.dependent_statuses.refused"),
+      pending:  t("associado.dependent_statuses.pending"),
+    };
+    return map[status] ?? status ?? "—";
+  };
+
+  return { dependentStatusColor, dependentStatusLabel };
+};

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

@@ -556,6 +556,7 @@
     "dependent_document_pdf": "PDF document",
     "dependent_document_open_new_tab": "Click to open in a new tab",
     "dependent_document_none": "No document submitted",
+    "dependent_delete_confirm": "Are you sure you want to delete the dependent {name}?",
     "import_afastamentos": "Absences",
     "set_on_leave_title": "Set member on leave",
     "set_on_leave_message": "Confirm putting {name} on leave?",

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

@@ -557,6 +557,7 @@
     "dependent_document_pdf": "Documento en PDF",
     "dependent_document_open_new_tab": "Haga clic para abrir en una pestaña nueva",
     "dependent_document_none": "Ningún documento enviado",
+    "dependent_delete_confirm": "¿Estás seguro de que deseas eliminar al dependiente {name}?",
     "import_afastamentos": "Ausencias",
     "set_on_leave_title": "Dar de baja al asociado",
     "set_on_leave_message": "¿Confirma la baja de {name}?",

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

@@ -557,6 +557,7 @@
     "dependent_document_pdf": "Documento em PDF",
     "dependent_document_open_new_tab": "Clique para abrir em nova aba",
     "dependent_document_none": "Nenhum documento enviado",
+    "dependent_delete_confirm": "Tem certeza que deseja excluir o dependente {name}?",
     "import_afastamentos": "Afastamentos",
     "set_on_leave_title": "Afastar associado",
     "set_on_leave_message": "Confirma o afastamento de {name}?",

+ 2 - 18
src/pages/associado/profile/ProfilePage.vue

@@ -132,6 +132,7 @@ import { userStore } from "src/stores/user";
 import { getDependentsByUser } from "src/api/profile";
 import { uploadMyAvatar, deleteMyAvatar } from "src/api/user";
 import { useMediaFile } from "src/composables/useMediaFile";
+import { useDependentStatus } from "src/composables/useDependentStatus";
 
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 
@@ -143,6 +144,7 @@ const $q = useQuasar();
 const { t } = useI18n();
 const store = userStore();
 const { prepareFile } = useMediaFile();
+const { dependentStatusColor, dependentStatusLabel } = useDependentStatus();
 
 const user = ref(null);
 const dependents = ref([]);
@@ -178,24 +180,6 @@ const initials = (name) => {
   return name.split(" ").slice(0, 2).map((w) => w[0]).join("").toUpperCase();
 };
 
-const dependentStatusColor = (status) => {
-  switch (status) {
-    case "approved": return "positive";
-    case "refused":  return "negative";
-    case "pending":  return "warning-light";
-    default:         return "grey";
-  }
-};
-
-const dependentStatusLabel = (status) => {
-  const map = {
-    approved: t("associado.dependent_statuses.approved"),
-    refused:  t("associado.dependent_statuses.refused"),
-    pending:  t("associado.dependent_statuses.pending"),
-  };
-  return map[status] ?? status ?? "—";
-};
-
 const loadUser = async () => {
   await store.fetchUser();
   user.value = store.user;

+ 20 - 2
src/pages/associado/profile/components/AddEditDependentDialog.vue

@@ -21,6 +21,17 @@
             map-options
             class="col-12"
           />
+          <DefaultSelect
+            v-if="showStatus"
+            v-model="form.status"
+            v-model:error="validationErrors.status"
+            :rules="[inputRules.required]"
+            :label="$t('common.terms.status')"
+            :options="statusOptions"
+            emit-value
+            map-options
+            class="col-12"
+          />
           <DefaultFilePicker
             v-if="!dependent"
             v-model="documentFile"
@@ -72,10 +83,11 @@ import DefaultFilePicker from "src/components/defaults/DefaultFilePicker.vue";
 
 defineEmits([...useDialogPluginComponent.emits]);
 
-const { dependent, userId, title } = defineProps({
+const { dependent, userId, title, showStatus } = defineProps({
   dependent: { type: Object, default: null },
   userId: { type: Number, required: true },
   title: { type: Function, default: () => "" },
+  showStatus: { type: Boolean, default: false },
 });
 
 const { t } = useI18n();
@@ -94,12 +106,18 @@ const kinshipOptions = computed(() => [
   { label: t("associado.kinship_options.outro"), value: "outro" },
 ]);
 
-// O documento fica fora do tracker: o File não sobrevive ao deepClone usado no diff.
+const statusOptions = computed(() => [
+  { label: t("associado.dependent_statuses.approved"), value: "approved" },
+  { label: t("associado.dependent_statuses.pending"), value: "pending" },
+  { label: t("associado.dependent_statuses.refused"), value: "refused" },
+]);
+
 const documentFile = ref(null);
 
 const { form, getUpdatedFields } = useFormUpdateTracker({
   name: dependent?.name ?? "",
   kinship: dependent?.kinship ?? "",
+  status: dependent?.status ?? "pending",
   responsible_user_id: userId,
 });
 

+ 135 - 2
src/pages/gestao-associados/components/AddEditAssociadoDialog.vue

@@ -90,6 +90,59 @@
             {{ $t("associado.first_access_password_hint") }}
           </div>
         </q-card-section>
+
+        <q-card-section v-if="associado" class="q-pt-none">
+          <div class="text-h6 q-mb-sm">{{ $t("associado.dependents") }}</div>
+
+          <q-list v-if="dependents.length > 0" class="column q-mb-md">
+            <div
+              v-for="dep in dependents"
+              :key="dep.id"
+              class="dependent-row row items-center no-wrap q-my-sm"
+            >
+              <q-avatar size="40px" class="dependent-avatar q-mr-md" text-color="white">
+                {{ initials(dep.name) }}
+              </q-avatar>
+
+              <div class="col column">
+                <span class="dependent-name">{{ dep.name }}</span>
+                <span class="dependent-kinship">
+                  {{ $t(`associado.kinship_options.${dep.kinship}`) }}
+                </span>
+              </div>
+
+              <q-badge
+                :color="dependentStatusColor(dep.status)"
+                class="q-mr-sm text-capitalize"
+                style="font-size:11px; padding: 3px 8px; border-radius: 20px;"
+              >
+                {{ dependentStatusLabel(dep.status) }}
+              </q-badge>
+
+              <q-btn
+                flat
+                round
+                dense
+                icon="mdi-pencil"
+                color="violet-normal"
+                @click="onEditDependent(dep)"
+              />
+              <q-btn
+                flat
+                round
+                dense
+                icon="mdi-trash-can-outline"
+                color="negative"
+                @click="onDeleteDependent(dep)"
+              />
+            </div>
+          </q-list>
+
+          <div v-else class="text-center q-py-md text-grey">
+            {{ $t("associado.no_dependents") }}
+          </div>
+        </q-card-section>
+
         <q-card-actions>
           <q-space />
           <q-btn
@@ -112,13 +165,15 @@
 </template>
 
 <script setup>
-import { ref, computed, useTemplateRef, watch } from "vue";
+import { ref, computed, useTemplateRef, watch, onMounted, defineAsyncComponent } from "vue";
 import { useInputRules } from "src/composables/useInputRules";
-import { useDialogPluginComponent } from "quasar";
+import { useDialogPluginComponent, useQuasar } from "quasar";
 import { useI18n } from "vue-i18n";
 import { createUser, updateUser } from "src/api/user";
+import { getDependentsByUser, deleteDependent } from "src/api/profile";
 import { useFormUpdateTracker } from "src/composables/useFormUpdateTracker";
 import { useSubmitHandler } from "src/composables/useSubmitHandler";
+import { useDependentStatus } from "src/composables/useDependentStatus";
 
 import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
 import DefaultInput from "src/components/defaults/DefaultInput.vue";
@@ -128,6 +183,10 @@ import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
 import PositionSelect from "src/components/selects/PositionSelect.vue";
 import SectorSelect from "src/components/selects/SectorSelect.vue";
 
+const AddEditDependentDialog = defineAsyncComponent(
+  () => import("src/pages/associado/profile/components/AddEditDependentDialog.vue"),
+);
+
 defineEmits([...useDialogPluginComponent.emits]);
 
 const { associado, title } = defineProps({
@@ -142,10 +201,54 @@ const { associado, title } = defineProps({
 });
 
 const { t } = useI18n();
+const $q = useQuasar();
 const { inputRules } = useInputRules();
 const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+const { dependentStatusColor, dependentStatusLabel } = useDependentStatus();
 const formRef = useTemplateRef("formRef");
 const confirmPassword = ref("");
+const dependents = ref([]);
+
+const initials = (name) => {
+  if (!name) return "?";
+  return name.split(" ").slice(0, 2).map((w) => w[0]).join("").toUpperCase();
+};
+
+const loadDependents = async () => {
+  if (!associado?.id) return;
+  try {
+    dependents.value = await getDependentsByUser(associado.id);
+  } catch {
+    dependents.value = [];
+  }
+};
+
+onMounted(loadDependents);
+
+const onEditDependent = (dep) => {
+  $q.dialog({
+    component: AddEditDependentDialog,
+    componentProps: {
+      dependent: dep,
+      userId: associado.id,
+      showStatus: true,
+      title: () => t("common.actions.edit") + " " + t("associado.dependent"),
+    },
+  }).onOk(() => loadDependents());
+};
+
+const onDeleteDependent = (dep) => {
+  $q.dialog({
+    title: t("common.actions.delete"),
+    message: t("associado.dependent_delete_confirm", { name: dep.name }),
+    cancel: { flat: true, label: t("common.actions.cancel"), color: "grey-7" },
+    ok: { unelevated: true, label: t("common.actions.delete"), color: "negative" },
+    persistent: true,
+  }).onOk(async () => {
+    await deleteDependent(dep.id);
+    await loadDependents();
+  });
+};
 
 const selectedPosition = ref(
   associado?.position ? { label: associado.position.name, value: associado.position.id } : null,
@@ -197,3 +300,33 @@ const onOKClick = async () => {
   }
 };
 </script>
+
+<style lang="scss" scoped>
+@use "src/css/quasar.variables.scss" as *;
+
+.dependent-row {
+  background: $violet-light;
+  border-radius: 8px;
+  padding: 10px 12px;
+}
+
+.dependent-avatar {
+  background: $violet-normal !important;
+  font-size: 14px;
+  font-weight: 700;
+  flex-shrink: 0;
+}
+
+.dependent-name {
+  font-size: 14px;
+  font-weight: 600;
+  color: $color-text;
+  line-height: 1.3;
+}
+
+.dependent-kinship {
+  font-size: 12px;
+  color: $color-text-2;
+  line-height: 1.2;
+}
+</style>