Quellcode durchsuchen

feat: :sparkles: feat(validaçã-carteirinha) Foi criado e ajustado o front end referente a base de pesquisa da carteirinha

Foi desenvolvido o frontend da tela de validação da carteirinha, contendo toda a estrutura necessária para realizar a busca e validação de associados através do nome, CPF ou matrícula, além da integração com o backend para consulta dinâmica das informações do associado.

fase:dev | origin:escopo
kayo henrique vor 3 Wochen
Ursprung
Commit
27535f6b8c

+ 7 - 0
src/api/associateValidation.js

@@ -0,0 +1,7 @@
+import api from "src/api";
+
+export const validateAssociate = async (payload) => {
+  const { data } = await api.post("/associate-validation", payload);
+
+  return data.payload;
+};

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

@@ -685,5 +685,17 @@
       "servico": "Service",
       "solicitacao": "Request Date"
     }
+  },
+  "associate_validation": {
+    "title": "Validate Membership Card",
+    "description": "Enter associate information to verify status.",
+    "search_placeholder": "EX: Name, CPF, Registration",
+    "search": "Search",
+    "associate_found": "Associate Found",
+    "name": "Name",
+    "cpf": "CPF",
+    "registration": "Registration",
+    "qr_code": "Read QR CODE",
+    "not_found": "Associate not found"
   }
 }

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

@@ -684,5 +684,17 @@
       "servico": "Servicio",
       "solicitacao": "Solicitud"
     }
+  },
+  "associate_validation": {
+    "title": "Validar Credencial",
+    "description": "Ingrese la información del asociado para verificar el estado.",
+    "search_placeholder": "EJ: Nombre, CPF, Matrícula",
+    "search": "Buscar",
+    "associate_found": "Asociado Encontrado",
+    "name": "Nombre",
+    "cpf": "CPF",
+    "registration": "Matrícula",
+    "qr_code": "Leer QR CODE",
+    "not_found": "Asociado no encontrado"
   }
 }

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

@@ -683,5 +683,17 @@
       "servico": "Serviço",
       "solicitacao": "Solicitação"
     }
+  },
+  "associate_validation": {
+    "title": "Validar Carteirinha",
+    "description": "Digite as informações do associado para verificar o status.",
+    "search_placeholder": "EX: Nome, CPF, Matrícula",
+    "search": "Buscar",
+    "associate_found": "Associado Encontrado",
+    "name": "Nome",
+    "cpf": "CPF",
+    "registration": "Matrícula",
+    "qr_code": "Ler QR CODE",
+    "not_found": "Associado não encontrado"
   }
-}
+}

+ 194 - 218
src/pages/parceiros-convenios/ValidarCarteirinhaPage.vue

@@ -2,250 +2,226 @@
   <div>
     <DefaultHeaderPage />
 
-    <div class="q-pa-md flex row q-gutter-sm">
-      <q-card
-        flat
-        class="associado-card"
-        :class="isVertical ? 'card-vertical' : 'card-horizontal'"
-      >
-        <div class="card-logo-row text-center">
-          <img :src="LogoSerPratiParceiro" class="card-logo" alt="SerPrati" />
+    <div class="validate-container">
+
+      <div class="validate-header">
+
+        <div class="validate-title">
+          {{ $t("associate_validation.title") }}
         </div>
 
-        <div class="card-body" :class="isVertical ? 'card-body--vertical' : 'card-body--horizontal'">
-          <q-avatar
-            class="card-avatar"
-            :size="isVertical ? '100px' : '76px'"
-          >
-            <img v-if="user?.avatar" :src="user.avatar" />
-            <q-icon v-else name="mdi-account" color="white" :size="isVertical ? '64px' : '40px'" />
-          </q-avatar>
-
-          <template v-if="!isVertical">
-            <div class="card-info">
-              <div class="card-info__row">
-                <span class="card-info__label">{{ $t('common.terms.name') }}</span>
-                <span class="card-info__value">{{ user?.name ?? '—' }}</span>
-              </div>
-              <div class="card-info__row q-pt-sm">
-                <span class="card-info__label">{{ $t('common.terms.cpf') }}</span>
-                <span class="card-info__value">{{ user?.cpf ?? '—' }}</span>
-              </div>
-              <div class="card-info__row q-pt-sm">
-                <span class="card-info__label">{{ $t('associado.registration') }}</span>
-                <span class="card-info__value">{{ user?.registration ?? '—' }}</span>
-              </div>
+        <q-btn
+          unelevated
+          no-caps
+          size="sm"
+          color="violet-normal"
+          class="qr-btn"
+          disable
+          icon="mdi-qrcode-scan"
+          :label="$t('associate_validation.qr_code')"
+        />
+
+      </div>
+
+      <q-card flat class="search-card">
+
+        <div class="search-label">
+         {{ $t("associate_validation.description") }}
+        </div>
+
+        <div class="row q-col-gutter-sm">
+
+          <div class="col">
+            <q-input
+              v-model="search"
+              outlined
+              dense
+              bg-color="white"
+              :placeholder="$t('associate_validation.search_placeholder')"
+              @keyup.enter="onValidate"
+            />
+          </div>
+
+          <div class="col-auto">
+            <q-btn
+              color="violet-normal"
+              unelevated
+              no-caps
+              :loading="loading"
+              :label="$t('associate_validation.search')"
+              @click="onValidate"
+            />
+          </div>
+
+        </div>
+
+      </q-card>
+
+      <div
+        v-if="associate"
+        class="q-mt-lg"
+      >
+
+        <q-card flat class="associate-result-card">
+
+          <div class="result-title">
+           {{ $t("associate_validation.associate_found") }}
+          </div>
+
+          <div class="q-mt-md">
+
+            <div class="result-row">
+              <span class="result-label">
+              {{ $t("associate_validation.name") }}
+              </span>
+
+              <span class="result-value">
+                {{ associate.name }}
+              </span>
             </div>
-            <div class="card-qr column">
-              <canvas v-if="qrReady" ref="qrCanvas" />
-              <q-spinner v-else color="white" size="48px" />
-              <div v-if="user?.expiry_date" class="card-info__row q-pt-sm text-right">
-                <span class="card-info__label text-right">{{ $t('associado.validity') }}</span>
-                <span class="card-info__value">{{ user.expiry_date }}</span>
-              </div>
+
+            <div class="result-row">
+              <span class="result-label">
+              {{ $t("associate_validation.cpf") }}
+              </span>
+
+              <span class="result-value">
+                {{ associate.cpf }}
+              </span>
             </div>
-          </template>
-
-          <div v-if="isVertical" class="card-body__vertical-content">
-            <div class="card-info card-info--vertical">
-              <div class="card-info__row">
-                <span class="card-info__label">{{ $t('common.terms.name') }}</span>
-                <span class="card-info__value">{{ user?.name ?? '—' }}</span>
-              </div>
-              <div class="card-info__row q-pt-xs">
-                <span class="card-info__label">{{ $t('common.terms.cpf') }}</span>
-                <span class="card-info__value">{{ user?.cpf ?? '—' }}</span>
-              </div>
-              <div class="card-info__row q-pt-xs">
-                <span class="card-info__label">{{ $t('associado.registration') }}</span>
-                <span class="card-info__value">{{ user?.registration ?? '—' }}</span>
-              </div>
-              <div class="card-info__row q-pt-xs">
-                <span class="card-info__label">{{ $t('associado.validity') }}</span>
-                <span class="card-info__value">{{ user?.expiry_date ?? '—' }}</span>
-              </div>
+
+            <div class="result-row">
+              <span class="result-label">
+                {{ $t("associate_validation.registration") }}
+              </span>
+
+              <span class="result-value">
+                {{ associate.registration }}
+              </span>
             </div>
-            <canvas v-if="qrReady" ref="qrCanvas" class="card-qr__canvas" />
-            <q-spinner v-else color="white" size="48px" />
+
           </div>
-        </div>
-      </q-card>
-      <div class="q-mt-auto justify-end">
-        <q-btn
-          round
-          flat
-          :icon="isVertical ? 'mdi-phone-rotate-landscape' : 'mdi-phone-rotate-portrait'"
-          color="violet-normal"
-          size="sm"
-          class="self-start"
-          :title="isVertical ? $t('associado.horizontal') : $t('associado.vertical')"
-          @click="isVertical = !isVertical"
-        />
+
+        </q-card>
+
       </div>
+
     </div>
+
   </div>
 </template>
 
 <script setup>
-import { ref, onMounted, useTemplateRef, watch, nextTick } from "vue";
-import QRCode from "qrcode";
-import { userStore } from "src/stores/user";
+import { ref } from "vue";
+import { Notify } from "quasar";
+
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
-import LogoSerPratiParceiro from "src/assets/logo_serprati_parceiro.svg";
-
-const store = userStore();
-const user = ref(null);
-const isVertical = ref(false);
-const qrCanvas = useTemplateRef("qrCanvas");
-const qrReady = ref(false);
-
-const QR_SIZE_HORIZONTAL = 90;
-const QR_SIZE_VERTICAL   = 110;
-
-const generateQR = async () => {
-  if (!user.value?.cpf && !user.value?.registration) return;
-  await nextTick();
-  const canvas = qrCanvas.value instanceof Array ? qrCanvas.value[0] : qrCanvas.value;
-  if (!canvas) return;
-  const qrData = `CPF:${user.value.cpf ?? ""}|MAT:${user.value.registration ?? ""}`;
-  const size = isVertical.value ? QR_SIZE_VERTICAL : QR_SIZE_HORIZONTAL;
-  await QRCode.toCanvas(canvas, qrData, { width: size, margin: 1, color: { dark: "#ffffff", light: "#661d75" } });
-};
 
-watch(isVertical, async () => {
-  await nextTick();
-  await generateQR();
-});
-
-onMounted(async () => {
-  await store.fetchUser();
-  user.value = store.user;
-  qrReady.value = true;
-  await generateQR();
-});
-</script>
+import {
+  validateAssociate,
+} from "src/api/associateValidation";
 
-<style lang="scss" scoped>
-@use "src/css/quasar.variables.scss" as vars;
+const search = ref("");
 
-.associado-card {
-  border-radius: 16px;
-  background: linear-gradient(180deg, #661D75 0%, #BF36DB 100%);
-  overflow: hidden;
-  box-shadow: 0 8px 32px rgba(77, 22, 88, 0.35);
-  color: vars.$neutral-light;
+const loading = ref(false);
 
-  .card-logo-row {
-    .card-logo {
-      height: 65px;
-    }
-  }
+const associate = ref(null);
 
-  .card-body {
-    padding: 8px 16px 16px;
-    display: flex;
-    gap: 12px;
-
-    &--horizontal {
-      flex-direction: row;
-      align-items: center;
-    }
-
-    &--vertical {
-      flex-direction: column;
-      align-items: center;
-      text-align: center;
-    }
-  }
+const onValidate = async () => {
 
-  .card-avatar {
-    border: 2px solid rgba(255, 255, 255, 0.4);
-    background: rgba(255, 255, 255, 0.15);
-    flex-shrink: 0;
+  if (!search.value) {
+    return;
   }
 
-  .card-info {
-    flex: 1;
-    display: flex;
-    flex-direction: column;
-    gap: 2px;
-
-    .card-info__label {
-      font-size: 11px;
-      opacity: 0.8;
-    }
-
-    .card-info__value {
-      font-size: 15px;
-      font-weight: 600;
-    }
-    &--vertical {
-      width: 100%;
-      align-items: flex-start;
-      text-align: left;
-
-    }
-
-    &__name {
-      font-size: 14px;
-      font-weight: 600;
-      color: vars.$neutral-light;
-      margin-bottom: 4px;
-    }
-
-    &__row {
-      display: flex;
-      flex-direction: column;
-      line-height: 1.2;
-    }
-
-    &__label {
-      font-size: 9px;
-      opacity: 0.75;
-      text-transform: uppercase;
-      letter-spacing: 0.4px;
-    }
-
-    &__value {
-      font-size: 11px;
-      font-weight: 500;
-      color: vars.$neutral-light;
-    }
-  }
+  loading.value = true;
 
-  .card-qr {
-    flex-shrink: 0;
-    display: flex;
-    align-items: center;
-    justify-content: center;
+  associate.value = null;
 
-    canvas {
-      border-radius: 6px;
-    }
-  }
+  try {
 
-  .card-body__vertical-content {
-    width: 100%;
-    display: flex;
-    flex-direction: row;
-    align-items: flex-end;
-    justify-content: space-between;
-    gap: 8px;
-  }
+    const response = await validateAssociate({
 
-  .card-qr__canvas {
-    border-radius: 6px;
-    flex-shrink: 0;
-  }
+      cpf: search.value,
+      registration: search.value,
+      name: search.value,
 
-  &.card-horizontal {
-    width: 360px;
-    max-width: calc(100vw - 32px);
-  }
+    });
+
+    associate.value = response;
+
+  } catch  {
+
+    Notify.create({
+      type: "negative",
+      message: "Associado não encontrado",
+    });
+
+  } finally {
+
+    loading.value = false;
 
-  &.card-vertical {
-    width: 260px;
-    max-width: calc(100vw - 32px);
   }
+};
+</script>
+
+<style scoped lang="scss">
+.validate-container {
+  padding: 20px;
+}
+
+.validate-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12px;
+}
+
+.validate-title {
+  font-size: 28px;
+  font-weight: 700;
+  color: #702082;
+}
+
+.qr-btn {
+  border-radius: 20px;
+}
+
+.search-card {
+  padding: 16px;
+  border-radius: 10px;
+  background: white;
+}
+
+.search-label {
+  font-size: 13px;
+  color: #702082;
+  margin-bottom: 10px;
+}
+
+.associate-result-card {
+  padding: 20px;
+  border-radius: 10px;
+  background: #F8F3F9;
+}
+
+.result-title {
+  font-size: 18px;
+  font-weight: 700;
+  color: #702082;
+}
+
+.result-row {
+  display: flex;
+  gap: 6px;
+  margin-bottom: 10px;
+}
+
+.result-label {
+  font-weight: 700;
+  color: #702082;
+}
+
+.result-value {
+  color: #555;
 }
-</style>
+</style>