Explorar el Código

feat: add opcoes em codigo de banco na parte de dados bancarios

Gustavo Mantovani hace 2 semanas
padre
commit
f7325e32ec

+ 6 - 0
src/api/banks.js

@@ -0,0 +1,6 @@
+import api from "src/api";
+
+export const getBanks = async () => {
+  const { data: response } = await api.get("/banks");
+  return response.payload ?? response.data ?? response;
+};

+ 320 - 54
src/components/profile/ProfileBankDataDialog.vue

@@ -1,23 +1,38 @@
 <template>
-  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right">
+  <q-dialog ref="dialogRef" maximized persistent transition-hide="slide-right" transition-show="slide-left">
     <div class="bg-page full-height no-shadow">
       <div class="row items-center q-px-md q-pb-sm bg-white shadow-profile bg-surface dialog-safe-header">
-        <q-btn v-close-popup icon="mdi-chevron-left" flat round dense color="primary" />
+        <q-btn v-close-popup color dense flat icon="mdi-chevron-left" round="primary" />
+
         <q-space />
-        <span class="gradient-diarista font16 fontbold">{{ $t('profile.bank_data.title') }}</span>
+
+        <span class="gradient-diarista font16 fontbold">
+          {{ $t('profile.bank_data.title') }}
+        </span>
+
         <q-space />
+
         <div style="width: 32px"></div>
       </div>
 
       <q-card-section class="col">
         <div class="text-text">
-          <div class="gradient-diarista font14 fontbold">{{ $t('profile.bank_data.my_bank_data_title') }}</div>
-          <div class="text-grey-7 q-mb-md font12 fontregular">{{ $t('profile.bank_data.subtitle') }}</div>
+          <div class="gradient-diarista font14 fontbold">
+            {{ $t('profile.bank_data.my_bank_data_title') }}
+          </div>
+
+          <div class="text-grey-7 q-mb-md font12 fontregular">
+            {{ $t('profile.bank_data.subtitle') }}
+          </div>
 
           <div class="pix-warning q-pa-md q-mb-md row items-start q-col-gutter-sm">
-            <q-icon name="mdi-alert-outline" color="warning" size="20px" class="q-mt-xs" />
+            <q-icon class="q-mt-xs" color="warning" name="mdi-alert-outline" size="20px" />
+
             <div class="col font12">
-              <span class="fontbold">{{ $t('profile.bank_data.warning_title') }}</span>
+              <span class="fontbold">
+                {{ $t('profile.bank_data.warning_title') }}
+              </span>
+
               {{ $t('profile.bank_data.warning_message') }}
             </div>
           </div>
@@ -26,27 +41,29 @@
             <div class="text-center text-text q-mb-sm font14 fontbold">
               <div>
                 {{ $t('profile.bank_data.pix_title') }}
-                <q-badge color="grey-5" class="q-ml-xs" :label="$t('profile.bank_data.in_development')" />
+
+                <q-badge class="q-ml-xs" color="grey-5" :label="$t('profile.bank_data.in_development')" />
               </div>
             </div>
+
             <q-input
-              outlined
+              class="q-mb-sm"
               dense
               disable
               input-class="text-text"
+              outlined
               :placeholder="$t('profile.bank_data.pix_key_placeholder')"
-              class="q-mb-sm"
             />
 
             <q-btn
-              unelevated
-              rounded
-              no-caps
-              color="grey-4"
-              text-color="grey-7"
               class="full-width"
-              padding="4px 12px"
+              color="grey-4"
               disable
+              no-caps
+              padding="4px 12px"
+              rounded
+              text-color="grey-7"
+              unelevated
               :label="$t('profile.bank_data.in_development')"
             />
           </q-card>
@@ -56,93 +73,153 @@
               {{ $t('profile.bank_data.bank_account_title') }}
             </div>
 
-            <q-input
+            <q-select
+              ref="bankSelectRef"
               v-model="bankForm.bank"
-              outlined
+              behavior="menu"
+              class="q-mb-sm col-12"
               dense
-              inputmode="numeric"
-              maxlength="3"
+              emit-value
+              fill-input
+              hide-dropdown-icon
+              hide-selected
               input-class="text-text"
+              input-debounce="0"
+              map-options
+              maxlength="3"
+              menu-anchor="bottom left"
+              menu-self="top left"
+              options-dense
+              use-input
+              outlined
+              popup-content-class="bank-select-menu"
+              stack-label
               :label="$t('profile.bank_data.bank_code')"
+              :loading="loadingBanks"
+              :menu-offset="[0, 4]"
+              :options="bankOptions"
+              :options-dark="false"
               :placeholder="$t('profile.bank_data.bank_code_placeholder')"
-              class="q-mb-sm col-12"
-            />
+              :popup-content-style="bankPopupStyle"
+              @blur="clearInvalidBankInput"
+              @filter="filterBanks"
+              @input-value="onBankInputValue"
+              @popup-show="updateBankSelectWidth"
+            >
+              <template #append>
+                <q-icon color="grey-7" name="mdi-magnify" />
+              </template>
+
+              <template #option="scope">
+                <q-item v-bind="scope.itemProps">
+                  <q-item-section class="bank-select-menu__code" side>
+                    {{ scope.opt.code }}
+                  </q-item-section>
+
+                  <q-item-section class="bank-select-menu__name">
+                    <q-item-label class="ellipsis">
+                      {{ scope.opt.name }}
+                    </q-item-label>
+                  </q-item-section>
+                </q-item>
+              </template>
+
+              <template #no-option>
+                <q-item>
+                  <q-item-section class="text-grey">
+                    {{ $t("http.errors.no_records_found") }}
+                  </q-item-section>
+                </q-item>
+              </template>
+            </q-select>
 
             <div class="row q-mb-sm q-my-auto col-12 row items-center">
               <q-radio
                 v-model="bankForm.type"
-                val="checking"
-                :label="$t('profile.bank_data.checking')"
+                class="q-mr-lg text-text"
                 color="primary"
                 keep-color
-                class="q-mr-lg text-text"
+                val="checking"
+                :label="$t('profile.bank_data.checking')"
               />
+
               <q-radio
                 v-model="bankForm.type"
-                val="savings"
-                :label="$t('profile.bank_data.savings')"
+                class="text-text"
                 color="primary"
                 keep-color
-                class="text-text"
+                val="savings"
+                :label="$t('profile.bank_data.savings')"
               />
             </div>
 
             <div class="row col-12 q-col-gutter-sm q-mb-sm">
               <q-input
                 v-model="bankForm.branch_number"
-                outlined
+                class="col-8"
                 dense
-                inputmode="numeric"
                 input-class="text-text"
+                inputmode="numeric"
+                outlined
+                stack-label
                 :label="$t('profile.bank_data.agency')"
-                class="col-8"
+                :placeholder="$t('profile.bank_data.agency_placeholder')"
               />
+
               <q-input
                 v-model="bankForm.branch_check_digit"
-                outlined
+                class="col-4"
                 dense
-                maxlength="2"
                 input-class="text-text"
+                maxlength="2"
+                outlined
+                stack-label
                 :label="$t('profile.bank_data.agency_digit')"
-                class="col-4"
+                :placeholder="$t('profile.bank_data.digit_placeholder')"
               />
             </div>
 
             <div class="row col-12 q-col-gutter-sm q-mb-sm">
               <q-input
                 v-model="bankForm.account_number"
-                outlined
+                class="col-8"
                 dense
-                inputmode="numeric"
                 input-class="text-text"
+                inputmode="numeric"
+                outlined
+                stack-label
                 :label="$t('profile.bank_data.account')"
-                class="col-8"
+                :placeholder="$t('profile.bank_data.account_placeholder')"
               />
+
               <q-input
                 v-model="bankForm.account_check_digit"
-                outlined
+                class="col-4"
                 dense
-                maxlength="2"
                 input-class="text-text"
+                maxlength="2"
+                outlined
+                stack-label
                 :label="$t('profile.bank_data.account_digit')"
-                class="col-4"
+                :placeholder="$t('profile.bank_data.digit_placeholder')"
               />
             </div>
 
             <q-btn
-              unelevated
-              rounded
-              no-caps
+              class="full-width"
               color="primary"
+              no-caps
               padding="8px 16px"
-              class="full-width"
+              rounded
+              unelevated
+              :disable="!canSaveBank"
               :label="bankAlreadyRegistered ? $t('profile.bank_data.att_bank') : $t('profile.bank_data.save_account')"
               :loading="savingBank"
-              :disable="!canSaveBank"
               @click="saveBank"
             />
           </q-card>
         </div>
+
         <div class="q-pb-xl"></div>
       </q-card-section>
     </div>
@@ -150,11 +227,12 @@
 </template>
 
 <script setup>
-import { ref, reactive, computed, onMounted } from 'vue';
-import { useDialogPluginComponent, useQuasar } from 'quasar';
+import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
+import { getBanks } from 'src/api/banks';
 import { updateProviderBankAccount } from 'src/api/providerPaymentMethod';
-import { userStore } from 'src/stores/user';
+import { useDialogPluginComponent, useQuasar } from 'quasar';
 import { useI18n } from 'vue-i18n';
+import { userStore } from 'src/stores/user';
 
 defineEmits([...useDialogPluginComponent.emits]);
 
@@ -163,10 +241,13 @@ const $q = useQuasar();
 const { t } = useI18n();
 const user = userStore();
 
-const savingBank = ref(false);
+const banks = ref([]);
 const bankAlreadyRegistered = ref(false);
-
-const providerId = computed(() => user.user?.provider?.id ?? user.user?.provider_id);
+const bankOptions = ref([]);
+const bankSelectRef = ref(null);
+const bankSelectWidth = ref(null);
+const loadingBanks = ref(false);
+const savingBank = ref(false);
 
 const bankForm = reactive({
   bank: '',
@@ -177,22 +258,150 @@ const bankForm = reactive({
   account_check_digit: '',
 });
 
+const bankPopupStyle = computed(() => ({
+  maxHeight: '188px',
+  overflowY: 'auto',
+  width: bankSelectWidth.value ? `${bankSelectWidth.value}px` : undefined,
+  maxWidth: bankSelectWidth.value ? `${bankSelectWidth.value}px` : undefined,
+}));
+
+const providerId = computed(() => user.user?.provider?.id ?? user.user?.provider_id);
+
+const isSelectedBankValid = (code) => {
+  if (!code) {
+    return false;
+  }
+
+  return banks.value.some((bank) => getBankCode(bank) === code);
+};
+
 const canSaveBank = computed(
   () =>
     !!providerId.value &&
-    !!bankForm.bank &&
+    isSelectedBankValid(bankForm.bank) &&
     !!bankForm.branch_number &&
     !!bankForm.account_number &&
     !!bankForm.account_check_digit,
 );
 
+const getBankCode = (bank) => {
+  const code = bank?.code ?? bank?.bank_code ?? bank?.number ?? bank?.value ?? '';
+  return String(code).padStart(3, '0');
+};
+
+const getBankName = (bank) => bank?.name ?? bank?.label ?? bank?.title ?? '';
+
+const getBankList = (response) => {
+  if (Array.isArray(response)) {
+    return response;
+  }
+
+  if (Array.isArray(response?.data)) {
+    return response.data;
+  }
+
+  if (Array.isArray(response?.payload)) {
+    return response.payload;
+  }
+
+  if (response && typeof response === 'object') {
+    return Object.entries(response).map(([code, name]) => ({
+      code,
+      name,
+    }));
+  }
+
+  return [];
+};
+
+//
+
+const filterBanks = (val, update) => {
+  const sanitized = onlyBankCodeDigits(val);
+
+  if (val !== sanitized) {
+    bankSelectRef.value?.updateInputValue(sanitized, true);
+  }
+
+  update(() => {
+    const needle = normalizeText(sanitized);
+
+    if (!needle) {
+      setBankOptions(banks.value);
+      return;
+    }
+
+    const filtered = banks.value.filter((bank) => {
+      const code = getBankCode(bank);
+
+      return code.includes(needle);
+    });
+
+    setBankOptions(filtered);
+  });
+};
+
+const formatBankOption = (bank) => {
+  const code = getBankCode(bank);
+  const name = getBankName(bank);
+
+  return {
+    code,
+    name,
+    label: code,
+    value: code,
+  };
+};
+
+const normalizeText = (text) =>
+  String(text ?? '')
+    .normalize('NFD')
+    .replace(/[\u0300-\u036f]/g, '')
+    .toLowerCase();
+
+const onlyBankCodeDigits = (value) => String(value ?? '').replace(/\D/g, '').slice(0, 3);
+
+const setBankOptions = (list) => {
+  bankOptions.value = list.map(formatBankOption).filter((bank) => bank.code);
+};
+
+const updateBankSelectWidth = async () => {
+  await nextTick();
+
+  bankSelectWidth.value = bankSelectRef.value?.$el?.getBoundingClientRect()?.width ?? null;
+};
+
+//
+
+const clearInvalidBankInput = () => {
+  if (!isSelectedBankValid(bankForm.bank)) {
+    bankForm.bank = '';
+
+    bankSelectRef.value?.updateInputValue('', true);
+  }
+};
+
+const onBankInputValue = (value) => {
+  const sanitized = onlyBankCodeDigits(value);
+
+  if (value !== sanitized) {
+    bankSelectRef.value?.updateInputValue(sanitized, true);
+  }
+
+  if (sanitized !== bankForm.bank) {
+    bankForm.bank = '';
+  }
+};
+
 const saveBank = async () => {
   if (!canSaveBank.value) {
     $q.notify({ type: 'negative', message: t('profile.bank_data.bank_fields_required') });
+
     return;
   }
 
   savingBank.value = true;
+
   try {
     // Holder (nome/CPF) é derivado do prestador no backend; aqui só os campos digitados.
     const bankAccount = {
@@ -205,23 +414,42 @@ const saveBank = async () => {
     };
 
     await updateProviderBankAccount(providerId.value, bankAccount);
+
     await user.fetchUser();
 
     bankAlreadyRegistered.value = true;
+
     $q.notify({ type: 'positive', message: t('profile.bank_data.account_saved') });
   } catch (error) {
     $q.notify({ type: 'negative', message: t('profile.bank_data.save_error') });
+
     console.log(error);
   } finally {
     savingBank.value = false;
   }
 };
 
-onMounted(() => {
+const loadBanks = async () => {
+  loadingBanks.value = true;
+
+  try {
+    const response = await getBanks();
+
+    banks.value = getBankList(response);
+
+    setBankOptions(banks.value);
+  } catch (error) {
+    console.log(error);
+  } finally {
+    loadingBanks.value = false;
+  }
+};
+
+onMounted(async () => {
   const bank = user.user?.provider?.recipient_default_bank_account;
 
   if (bank && Object.keys(bank).length) {
-    bankForm.bank = bank.bank || '';
+    bankForm.bank = bank.bank ? String(bank.bank).padStart(3, '0') : '';
     bankForm.type = bank.type || 'checking';
     bankForm.branch_number = bank.branch_number || '';
     bankForm.branch_check_digit = bank.branch_check_digit || '';
@@ -229,6 +457,16 @@ onMounted(() => {
     bankForm.account_check_digit = bank.account_check_digit || '';
     bankAlreadyRegistered.value = true;
   }
+
+  await loadBanks();
+
+  await updateBankSelectWidth();
+
+  window.addEventListener('resize', updateBankSelectWidth);
+});
+
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', updateBankSelectWidth);
 });
 </script>
 
@@ -238,3 +476,31 @@ onMounted(() => {
   border-radius: 12px;
 }
 </style>
+
+<style>
+.bank-select-menu {
+  background: #FFFFFF !important;
+  color: #1F1F1F !important;
+  border: 1px solid #E7E7E7;
+  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.14);
+  max-height: 188px !important;
+  overflow-y: auto !important;
+}
+
+.bank-select-menu .q-item {
+  min-height: 32px;
+  padding: 6px 10px;
+  color: #1F1F1F;
+}
+
+.bank-select-menu__name {
+  min-width: 0;
+}
+
+.bank-select-menu__code {
+  min-width: 36px;
+  color: #8C8C8C;
+  font-family: monospace;
+  padding-right: 8px;
+}
+</style>

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

@@ -488,10 +488,13 @@
       "checking": "Checking",
       "savings": "Savings",
       "agency": "Agency",
+      "agency_placeholder": "Agency",
       "account": "Account",
+      "account_placeholder": "Account",
       "digit": "Digit",
+      "digit_placeholder": "Digit",
       "bank_code": "Bank (code)",
-      "bank_code_placeholder": "e.g. 341",
+      "bank_code_placeholder": "Bank code. e.g. 341",
       "agency_digit": "Digit",
       "account_digit": "Digit",
       "in_development": "Under development",

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

@@ -488,10 +488,13 @@
       "checking": "Corriente",
       "savings": "Ahorro",
       "agency": "Agencia",
+      "agency_placeholder": "Agencia",
       "account": "Cuenta",
+      "account_placeholder": "Cuenta",
       "digit": "Dígito",
+      "digit_placeholder": "Dígito",
       "bank_code": "Banco (código)",
-      "bank_code_placeholder": "Ej.: 341",
+      "bank_code_placeholder": "Código del banco. Ej.: 341",
       "agency_digit": "Dígito",
       "account_digit": "Dígito",
       "in_development": "En desarrollo",

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

@@ -491,10 +491,13 @@
       "checking": "Corrente",
       "savings": "Poupança",
       "agency": "Agência",
+      "agency_placeholder": "Agência",
       "account": "Conta",
+      "account_placeholder": "Conta",
       "digit": "Dígito",
+      "digit_placeholder": "Dígito",
       "bank_code": "Banco (código)",
-      "bank_code_placeholder": "Ex.: 341",
+      "bank_code_placeholder": "Código do banco. Ex.: 341",
       "agency_digit": "Dígito",
       "account_digit": "Dígito",
       "in_development": "Em desenvolvimento",