Pārlūkot izejas kodu

feat: :sparkles: crud de enderecos dentro de client e provider

crud de enderecos dentro de client e provider
Gustavo Zanatta 1 mēnesi atpakaļ
vecāks
revīzija
47227423b4

+ 58 - 0
src/api/address.js

@@ -0,0 +1,58 @@
+import api from "src/api";
+
+export const getAddresses = async (source, sourceId) => {
+  const { data } = await api.get(`/addresses?source=${source}&source_id=${sourceId}`);
+  return data.payload;
+};
+
+export const getAddress = async (id) => {
+  const { data } = await api.get(`/addresses/${id}`);
+  return data.payload;
+};
+
+export const createAddress = async (address) => {
+  const { data } = await api.post(`/addresses`, { ...address });
+  return data.payload;
+};
+
+export const updateAddress = async (address, id) => {
+  const { data } = await api.put(`/addresses/${id}`, { ...address });
+  return data.payload;
+};
+
+export const deleteAddress = async (id) => {
+  const { data } = await api.delete(`/addresses/${id}`);
+  return data.payload;
+};
+
+export const searchAddressByCEP = async (cep) => {
+  try {
+    const responseCep = await fetch(`https://viacep.com.br/ws/${cep}/json/`);
+    const data = await responseCep.json();
+
+    if (data.erro) {
+      return null;
+    }
+
+    const response = await api.get('busca-estado-cidade-por-descricao', {
+      params: {
+        stateUf: data.uf,
+        cityName: data.localidade
+      }
+    });
+    
+    return {
+      zip_code: data.cep,
+      address: data.logradouro + ', ' + data.bairro,
+      has_complement: true,
+      complement: data.complemento,
+      nickname: "",
+      instructions: "",
+      address_type: "home",
+      state: response.data.payload.state,
+      city: response.data.payload.city
+    };
+  } catch {
+    return null;
+  }
+};

+ 7 - 12
src/components/regions/CitySelect.vue

@@ -80,14 +80,12 @@ const baseOptions = ref([]);
 const cityOptions = ref([]);
 
 const filterFn = async (val, update) => {
-  ensureOnlyPossibleOptions(country?.value, state?.value);
-  const needle = normalizeString(val);
-  cityOptions.value = cityOptions.value.filter((v) => {
-    return (
-      normalizeString(v.label).includes(needle) ||
-      normalizeString(v.code).includes(needle)
-    );
-  });
+  const needle = normalizeString(val || '');
+    cityOptions.value = cityOptions.value.filter((v) => {
+      return (
+        normalizeString(v.label).includes(needle) 
+      );
+    });
   update();
 };
 
@@ -108,9 +106,6 @@ const selectCityById = (id) => {
 const ensureOnlyPossibleOptions = (country_id, state_id) => {
   if (state_id) {
     cityOptions.value = baseOptions.value.filter((city) => {
-      if (country_id) {
-        return city.country_id === country_id && city.state_id === state_id;
-      }
       return city.state_id === state_id;
     });
   }
@@ -149,7 +144,7 @@ onMounted(async () => {
       label: city.name,
       value: city.id,
       state_id: city.state_id,
-    }));
+    }))
     cityOptions.value = baseOptions.value;
     if (initialId) {
       selectCityById(initialId);

+ 10 - 6
src/components/regions/StateSelect.vue

@@ -76,12 +76,16 @@ const stateOptions = ref([]);
 const filterFn = (val, update) => {
   ensureOnlyPossibleOptions(country?.value);
   const needle = normalizeString(val);
-  stateOptions.value = stateOptions.value.filter((v) => {
-    return (
-      normalizeString(v.label).includes(needle) ||
-      normalizeString(v.code).includes(needle)
-    );
-  });
+  if(val) {
+    stateOptions.value = stateOptions.value.filter((v) => {
+      return (
+        normalizeString(v.label).includes(needle) ||
+        normalizeString(v.code).includes(needle)
+      );
+    });
+  } else {
+    stateOptions.value = baseOptions.value;
+  }
   update();
 };
 

+ 4 - 6
src/helpers/utils.js

@@ -96,12 +96,10 @@ const formatToBRLCurrency = (value) => {
   return value;
 };
 
-const normalizeString = (val) =>
-  val
-    .toLowerCase()
-    .normalize("NFKD")
-    .replace(/[\u0300-\u036f~]/g, "");
-
+const normalizeString = (val) => {
+  let valNormalized = val.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f~]/g, "");
+  return valNormalized;
+}
 /**
  * @description Máscara dinâmica para CPF/CNPJ (aplica CPF até 11 dígitos, depois CNPJ)
  * @param {string} value valor do input

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

@@ -246,6 +246,32 @@
       "document": "CPF/CNPJ"
     }
   },
+  "address": {
+    "singular": "Address",
+    "plural": "Addresses",
+    "add": "Add Address",
+    "edit": "Edit Address",
+    "tab": "Addresses",
+    "fields": {
+      "zip_code": "ZIP Code",
+      "address": "Address",
+      "has_complement": "Has complement?",
+      "complement": "Complement",
+      "nickname": "Nickname",
+      "instructions": "Instructions",
+      "address_type": "Address Type"
+    },
+    "types": {
+      "home": "Home",
+      "commercial": "Commercial",
+      "other": "Other"
+    },
+    "messages": {
+      "cep_not_found": "ZIP code not found. Please fill in manually.",
+      "searching_cep": "Searching ZIP code...",
+      "delete_confirm": "Are you sure you want to delete this address?"
+    }
+  },
   "provider": {
     "singular": "Provider",
     "plural": "Providers",

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

@@ -246,6 +246,32 @@
       "document": "CPF/CNPJ"
     }
   },
+  "address": {
+    "singular": "Dirección",
+    "plural": "Direcciones",
+    "add": "Agregar Dirección",
+    "edit": "Editar Dirección",
+    "tab": "Direcciones",
+    "fields": {
+      "zip_code": "Código Postal",
+      "address": "Dirección",
+      "has_complement": "¿Tiene complemento?",
+      "complement": "Complemento",
+      "nickname": "Apodo",
+      "instructions": "Instrucciones",
+      "address_type": "Tipo de Dirección"
+    },
+    "types": {
+      "home": "Residencial",
+      "commercial": "Comercial",
+      "other": "Otro"
+    },
+    "messages": {
+      "cep_not_found": "Código postal no encontrado. Por favor, complete manualmente.",
+      "searching_cep": "Buscando código postal...",
+      "delete_confirm": "¿Está seguro de que desea eliminar esta dirección?"
+    }
+  },
   "provider": {
     "singular": "Proveedor",
     "plural": "Proveedores",

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

@@ -246,6 +246,32 @@
       "document": "CPF/CNPJ"
     }
   },
+  "address": {
+    "singular": "Endereço",
+    "plural": "Endereços",
+    "add": "Adicionar Endereço",
+    "edit": "Editar Endereço",
+    "tab": "Endereços",
+    "fields": {
+      "zip_code": "CEP",
+      "address": "Endereço",
+      "has_complement": "Tem complemento?",
+      "complement": "Complemento",
+      "nickname": "Apelido",
+      "instructions": "Instruções",
+      "address_type": "Tipo de Endereço"
+    },
+    "types": {
+      "home": "Residencial",
+      "commercial": "Comercial",
+      "other": "Outro"
+    },
+    "messages": {
+      "cep_not_found": "CEP não encontrado. Por favor, preencha manualmente.",
+      "searching_cep": "Buscando CEP...",
+      "delete_confirm": "Tem certeza que deseja excluir este endereço?"
+    }
+  },
   "provider": {
     "singular": "Prestador",
     "plural": "Prestadores",

+ 254 - 0
src/pages/address/components/AddEditAddressDialog.vue

@@ -0,0 +1,254 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="q-dialog-plugin" style="width: 1000px; max-width: 90vw">
+      <DefaultDialogHeader :title="title" @close="onDialogCancel" />
+      <q-form ref="formRef" @submit="onOKClick">
+        <q-card-section class="row q-col-gutter-sm">
+          <q-input
+            v-model="form.zip_code"
+            mask="########"
+            fill-mask
+            unmasked-value
+            :label="$t('address.fields.zip_code')"
+            :rules="[inputRules.required, validateCEP]"
+            :error="!!serverErrors?.zip_code"
+            :error-message="serverErrors?.zip_code"
+            :loading="loadingCEP"
+            class="col-12"
+            @update:model-value="onCEPChange"
+          >
+            <template #append>
+              <q-icon name="mdi-map-marker" />
+            </template>
+          </q-input>
+
+          <q-input
+            v-model="form.address"
+            :label="$t('address.fields.address')"
+            :rules="[inputRules.required]"
+            :error="!!serverErrors?.address"
+            :error-message="serverErrors?.address"
+            type="textarea"
+            rows="3"
+            class="col-12"
+          />
+
+          <div class="col-12">
+            <q-checkbox
+              v-model="form.has_complement"
+              :label="$t('address.fields.has_complement')"
+            />
+          </div>
+
+          <q-input
+            v-if="form.has_complement"
+            v-model="form.complement"
+            :label="$t('address.fields.complement')"
+            :error="!!serverErrors?.complement"
+            :error-message="serverErrors?.complement"
+            class="col-12"
+          />
+          <StateSelect
+            v-model="selectedState"
+            :label="$t('ui.navigation.state')"
+            :initial-id="form.state_id"
+            :rules="[inputRules.required]"
+            :error="!!serverErrors?.state_id"
+            :error-message="serverErrors?.state_id"
+            class="col-md-6 col-12"
+          />
+          <CitySelect
+            v-model="selectedCity"
+            :label="$t('ui.navigation.city')"
+            :state="selectedState"
+            :initial-id="form.city_id"
+            :rules="[inputRules.required]"
+            :error="!!serverErrors?.city_id"
+            :error-message="serverErrors?.city_id"
+            :disable="!selectedState"
+            class="col-md-6 col-12"
+          />
+
+          <q-input
+            v-model="form.nickname"
+            :label="$t('address.fields.nickname')"
+            :error="!!serverErrors?.nickname"
+            :error-message="serverErrors?.nickname"
+            class="col-md-6 col-12"
+          />
+
+          <q-select
+            v-model="form.address_type"
+            :options="addressTypeOptions"
+            :label="$t('address.fields.address_type')"
+            :rules="[inputRules.required]"
+            :error="!!serverErrors?.address_type"
+            :error-message="serverErrors?.address_type"
+            emit-value
+            map-options
+            class="col-md-6 col-12"
+          />
+
+          <q-input
+            v-model="form.instructions"
+            :label="$t('address.fields.instructions')"
+            :error="!!serverErrors?.instructions"
+            :error-message="serverErrors?.instructions"
+            type="textarea"
+            rows="2"
+            class="col-12"
+          />
+        </q-card-section>
+        <q-card-actions align="center">
+          <q-btn color="primary" :label="$t('common.actions.cancel')" @click="onDialogCancel" />
+          <q-space />
+          <q-btn
+            color="primary"
+            :label="$t('common.actions.save')"
+            type="submit"
+            :loading="loading"
+            :disable="!hasUpdatedFields"
+          />
+        </q-card-actions>
+      </q-form>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, useTemplateRef, computed, watch } from "vue";
+import { useInputRules } from "src/composables/useInputRules";
+import { useDialogPluginComponent, useQuasar } from "quasar";
+import { useI18n } from "vue-i18n";
+import { createAddress, updateAddress, searchAddressByCEP } from "src/api/address";
+import { useFormUpdateTracker } from "src/composables/useFormUpdateTracker";
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
+
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
+import StateSelect from "src/components/regions/StateSelect.vue";
+import CitySelect from "src/components/regions/CitySelect.vue";
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { address, source, sourceId, title } = defineProps({
+  address: {
+    type: Object,
+    default: null,
+  },
+  source: {
+    type: String,
+    required: true,
+  },
+  sourceId: {
+    type: Number,
+    required: true,
+  },
+  title: {
+    type: String,
+    default: "",
+  },
+});
+
+const { t } = useI18n();
+const $q = useQuasar();
+const { inputRules } = useInputRules();
+
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
+
+const formRef = useTemplateRef("formRef");
+
+const { form, getUpdatedFields, hasUpdatedFields } = useFormUpdateTracker({
+  zip_code: address?.zip_code || "",
+  address: address?.address || "",
+  has_complement: address?.has_complement || false,
+  complement: address?.complement || "",
+  nickname: address?.nickname || "",
+  instructions: address?.instructions || "",
+  address_type: address?.address_type || "home",
+  city_id: address?.city_id || null,
+  state_id: address?.state_id || null,
+  source: source,
+  source_id: sourceId,
+});
+
+const selectedCity = ref(null);
+const selectedState = ref(null);
+
+const loadingCEP = ref(false);
+
+const addressTypeOptions = computed(() => [
+  { label: t("address.types.home"), value: "home" },
+  { label: t("address.types.commercial"), value: "commercial" },
+  { label: t("address.types.other"), value: "other" },
+]);
+
+const {
+  loading,
+  serverErrors,
+  execute: submitForm,
+} = useSubmitHandler({
+  onSuccess: () => onDialogOK(true),
+  formRef: formRef,
+});
+
+const validateCEP = (val) => {
+  if (!val) return true;
+  return /^\d{8}$/.test(val) || t("validation.rules.cep");
+};
+
+const onCEPChange = async (cep) => {
+  if (cep && cep.length === 8) {
+    loadingCEP.value = true;
+    
+    const result = await searchAddressByCEP(cep);
+    
+    loadingCEP.value = false;
+    
+    if (result) {
+      form.address = result.address;
+      form.has_complement = result.has_complement;
+      form.complement = result.complement;
+      form.nickname = result.nickname;
+      form.instructions = result.instructions;
+      form.address_type = result.address_type;
+
+      form.state_id = result.state.value;
+      form.city_id = result.city.value;
+      selectedState.value = result.state;
+      selectedCity.value = result.city;
+      $q.notify({
+        type: "positive",
+        message: t("address.messages.searching_cep"),
+        position: "top",
+      });
+    } else {
+      $q.notify({
+        type: "warning",
+        message: t("address.messages.cep_not_found"),
+        position: "top",
+      });
+    }
+  }
+};
+
+const onOKClick = async () => {
+  const data = {
+    ...form,
+  };
+
+  if (address) {
+    await submitForm(() => updateAddress(getUpdatedFields.value, address.id));
+  } else {
+    await submitForm(() => createAddress(data));
+  }
+};
+
+watch(selectedCity, () => {
+  form.city_id = selectedCity.value?.value;
+});
+
+watch(selectedState, () => {
+  form.state_id = selectedState.value?.value;
+});
+</script>

+ 137 - 0
src/pages/address/components/AddressesPanel.vue

@@ -0,0 +1,137 @@
+<template>
+  <div class="q-pa-md">
+    <div>
+      <DefaultTable
+        ref="tableRef"
+        :columns="columns"
+        :api-call="() => getAddresses(props.source, props.sourceId)"
+        :delete-function="deleteAddress"
+        :mostrar-selecao-de-colunas="false"
+        :mostrar-botao-fullscreen="false"
+        :mostrar-toggle-inativos="false"
+        open-item
+        add-item
+        @on-row-click="onRowClick"
+        @on-add-item="onAddItem"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, defineAsyncComponent } from "vue";
+import { useQuasar } from "quasar";
+import { useI18n } from "vue-i18n";
+import { getAddresses, deleteAddress } from "src/api/address";
+import { permissionStore } from "src/stores/permission";
+import DefaultTable from "src/components/defaults/DefaultTable.vue";
+
+const AddEditAddressDialog = defineAsyncComponent(
+  () => import("src/pages/address/components/AddEditAddressDialog.vue"),
+);
+
+const props = defineProps({
+  source: {
+    type: String,
+    required: true,
+  },
+  sourceId: {
+    type: Number,
+    required: true,
+  },
+});
+
+const { t } = useI18n();
+const $q = useQuasar();
+const permission_store = permissionStore();
+
+const tableRef = ref(null);
+
+const columns = computed(() => [
+  {
+    name: "nickname",
+    label: t("address.fields.nickname"),
+    field: "nickname",
+    align: "left",
+    sortable: true,
+  },
+  {
+    name: "zip_code",
+    label: t("address.fields.zip_code"),
+    field: "zip_code",
+    align: "left",
+    sortable: true,
+  },
+  {
+    name: "address",
+    label: t("address.fields.address"),
+    field: "address",
+    align: "left",
+    sortable: true,
+  },
+  {
+    name: "address_type",
+    label: t("address.fields.address_type"),
+    field: "address_type",
+    align: "left",
+    sortable: true,
+  },
+  {
+    name: "actions",
+    label: t("common.terms.actions"),
+    align: "center",
+    required: true,
+  },
+]);
+
+const onRowClick = ({ row }) => {
+  if (permission_store.getAccess("config.address", "edit") === false) {
+    $q.loading.hide();
+    $q.notify({
+      type: "negative",
+      message: t("validation.permissions.edit"),
+    });
+    return;
+  }
+  $q.dialog({
+    component: AddEditAddressDialog,
+    componentProps: {
+      address: row,
+      source: props.source,
+      sourceId: props.sourceId,
+      title: () =>
+        useI18n().t("common.actions.edit") +
+        " " +
+        useI18n().t("common.terms.address"),
+    },
+  }).onOk(async (success) => {
+    if (success) {
+      tableRef.value.refresh();
+    }
+  });
+};
+
+const onAddItem = () => {
+  if (permission_store.getAccess("config.address", "add") === false) {
+    $q.loading.hide();
+    $q.notify({
+      type: "negative",
+      message: t("validation.permissions.add"),
+    });
+    return;
+  }
+  $q.dialog({
+    component: AddEditAddressDialog,
+    componentProps: {
+      title: () =>
+        useI18n().t("common.actions.add") + " " + useI18n().t("common.terms.address"),
+      source: props.source,
+      sourceId: props.sourceId,
+    },
+  }).onOk(async (success) => {
+    if (success) {
+      tableRef.value.refresh();
+    }
+  });
+};
+</script>

+ 65 - 39
src/pages/client/components/AddEditClientDialog.vue

@@ -1,47 +1,71 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <q-card class="q-dialog-plugin" style="width: 700px; max-width: 90vw">
+    <q-card class="q-dialog-plugin" style="width: 1000px; max-width: 90vw">
       <DefaultDialogHeader :title="title" @close="onDialogCancel" />
-      <q-form ref="formRef" @submit="onOKClick">
-        <q-card-section class="row q-col-gutter-sm">
-          <!-- User -->
-          <UserSelect
-            v-model="selectedUser"
-            :label="$t('common.terms.user')"
-            :rules="[inputRules.required]"
-            :error="!!serverErrors?.user_id"
-            :error-message="serverErrors?.user_id"
-            :initial-id="client ? client.user_id : null"
-            class="col-12"
-            @update:model-value="serverErrors.user_id = null"
-          />
+      
+      <q-tabs
+        v-model="tab"
+        dense
+        class="text-grey"
+        active-color="primary"
+        indicator-color="primary"
+        align="justify"
+      >
+        <q-tab name="data" label="Dados" />
+        <q-tab v-if="client" name="addresses" :label="$t('address.tab')" />
+      </q-tabs>
 
-          <!-- Document (CPF/CNPJ) -->
-          <q-input
-            v-model="form.document"
-            :mask="documentMask"
-            fill-mask
-            unmasked-value
-            :label="$t('client.fields.document')"
-            :rules="[inputRules.required, validateDocument]"
-            :error="!!serverErrors?.document"
-            :error-message="serverErrors?.document"
-            class="col-12"
-            @update:model-value="serverErrors.document = null"
-          />
-        </q-card-section>
-        <q-card-actions align="center">
-          <q-btn color="primary" label="Cancel" @click="onDialogCancel" />
-          <q-space />
-          <q-btn
-            color="primary"
-            label="OK"
-            :type="'submit'"
-            :loading="loading"
-            :disable="!hasUpdatedFields"
+      <q-separator v-if="client" />
+
+      <q-tab-panels v-model="tab" animated>
+        <q-tab-panel name="data">
+          <q-form ref="formRef" @submit="onOKClick">
+            <q-card-section class="row q-col-gutter-sm">
+              <UserSelect
+                v-model="selectedUser"
+                :label="$t('common.terms.user')"
+                :rules="[inputRules.required]"
+                :error="!!serverErrors?.user_id"
+                :error-message="serverErrors?.user_id"
+                :initial-id="client ? client.user_id : null"
+                class="col-12"
+                @update:model-value="serverErrors.user_id = null"
+              />
+
+              <q-input
+                v-model="form.document"
+                :mask="documentMask"
+                fill-mask
+                unmasked-value
+                :label="$t('client.fields.document')"
+                :rules="[inputRules.required, validateDocument]"
+                :error="!!serverErrors?.document"
+                :error-message="serverErrors?.document"
+                class="col-12"
+                @update:model-value="serverErrors.document = null"
+              />
+            </q-card-section>
+            <q-card-actions align="center">
+              <q-btn color="primary" label="Cancel" @click="onDialogCancel" />
+              <q-space />
+              <q-btn
+                color="primary"
+                label="OK"
+                :type="'submit'"
+                :loading="loading"
+                :disable="!hasUpdatedFields"
+              />
+            </q-card-actions>
+          </q-form>
+        </q-tab-panel>
+
+        <q-tab-panel v-if="client" name="addresses">
+          <AddressesPanel 
+            :source="'client'" 
+            :source-id="client.id"
           />
-        </q-card-actions>
-      </q-form>
+        </q-tab-panel>
+      </q-tab-panels>
     </q-card>
   </q-dialog>
 </template>
@@ -58,6 +82,7 @@ import { dynamicCpfCnpjMask, validateCpfCnpj } from 'src/helpers/utils';
 
 import DefaultDialogHeader from 'src/components/defaults/DefaultDialogHeader.vue';
 import UserSelect from 'src/components/user/UserSelect.vue';
+import AddressesPanel from 'src/pages/address/components/AddressesPanel.vue';
 
 defineEmits([
   ...useDialogPluginComponent.emits,
@@ -81,6 +106,7 @@ const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
   useDialogPluginComponent();
 
 const formRef = useTemplateRef('formRef');
+const tab = ref('data');
 
 const { form, getUpdatedFields, hasUpdatedFields } = useFormUpdateTracker({
   user_id: client ? client?.user_id : null,

+ 149 - 137
src/pages/provider/components/AddEditProviderDialog.vue

@@ -1,152 +1,162 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <q-card class="q-dialog-plugin overflow-hidden" style="width: 900px; max-width: 90vw">
+    <q-card class="q-dialog-plugin overflow-hidden" style="width: 1000px; max-width: 90vw">
       <DefaultDialogHeader :title="title" @close="onDialogCancel" />
-      <q-form ref="formRef" @submit="onOKClick">
-        <q-card-section class="row q-col-gutter-sm">
-          <UserSelect
-            v-model="selectedUser"
-            :label="$t('common.terms.user')"
-            :rules="[inputRules.required]"
-            :error="!!serverErrors?.user_id"
-            :error-message="serverErrors?.user_id"
-            :initial-id="provider ? provider.user_id : null"
-            class="col-md-6 col-12"
-          />
-
-          <q-input
-            v-model="form.document"
-            :mask="documentMask"
-            fill-mask
-            unmasked-value
-            :label="$t('provider.fields.document')"
-            :rules="[inputRules.required, validateDocument]"
-            :error="!!serverErrors?.document"
-            :error-message="serverErrors?.document"
-            class="col-md-6 col-12"
-          />
-
-          <q-input
-            v-model="form.rg"
-            mask="##.###.###-#"
-            fill-mask
-            :label="$t('provider.fields.rg')"
-            :error="!!serverErrors?.rg"
-            :error-message="serverErrors?.rg"
-            class="col-md-6 col-12"
-          />
-          <DefaultInputDatePicker
-            v-model:untreated-date="form.birth_date"
-            :label="$t('provider.fields.birth_date')"
-            :error="serverErrors?.birth_date"
-            :error-message="serverErrors?.birth_date"
-            class="col-md-6 col-12"
-          />
-
-          <DefaultCurrencyInput
-            v-model="form.daily_price_8h"
-            :label="$t('provider.fields.daily_price_8h')"
-            :error="!!serverErrors?.daily_price_8h"
-            :error-message="serverErrors?.daily_price_8h"
-            :hint="$t('provider.hints.daily_price')"
-            lazy-rules
-            class="col-md-3 col-6"
-          />
-
-          <DefaultCurrencyInput
-            v-model="form.daily_price_6h"
-            :label="$t('provider.fields.daily_price_6h')"
-            :error="!!serverErrors?.daily_price_6h"
-            :error-message="serverErrors?.daily_price_6h"
-            disable
-            class="col-md-3 col-6"
-          />
-
-          <DefaultCurrencyInput
-            v-model="form.daily_price_4h"
-            :label="$t('provider.fields.daily_price_4h')"
-            :error="!!serverErrors?.daily_price_4h"
-            :error-message="serverErrors?.daily_price_4h"
-            disable
-            class="col-md-3 col-6"
-          />
-
-          <DefaultCurrencyInput
-            v-model="form.daily_price_2h"
-            :label="$t('provider.fields.daily_price_2h')"
-            :error="!!serverErrors?.daily_price_2h"
-            :error-message="serverErrors?.daily_price_2h"
-            disable
-            class="col-md-3 col-6"
-          />
-
-          <div class="col-12">
-            <q-checkbox
-              v-model="form.is_approved"
-              :label="$t('provider.fields.is_approved')"
-            />
-          </div>
-
-          <div class="col-12 q-mt-md">
-            <div class="row q-col-gutter-md">
-              <div class="col-auto flex items-center">
-                <q-avatar size="80px" color="grey-3">
-                  <q-icon name="mdi-account" size="50px" color="grey-6" />
-                </q-avatar>
+
+      <q-tabs v-model="tab" dense class="text-grey" active-color="primary" indicator-color="primary" align="justify">
+        <q-tab name="data" label="Dados" />
+        <q-tab v-if="provider" name="addresses" :label="$t('address.tab')" />
+      </q-tabs>
+
+      <q-separator v-if="provider" />
+
+      <q-tab-panels v-model="tab" animated>
+        <q-tab-panel name="data">
+          <q-form ref="formRef" @submit="onOKClick">
+            <q-card-section class="row q-col-gutter-sm">
+              <UserSelect
+                v-model="selectedUser"
+                :label="$t('common.terms.user')"
+                :rules="[inputRules.required]"
+                :error="!!serverErrors?.user_id"
+                :error-message="serverErrors?.user_id"
+                :initial-id="provider ? provider.user_id : null"
+                class="col-md-6 col-12"
+              />
+
+              <q-input
+                v-model="form.document"
+                :mask="documentMask"
+                fill-mask
+                unmasked-value
+                :label="$t('provider.fields.document')"
+                :rules="[inputRules.required, validateDocument]"
+                :error="!!serverErrors?.document"
+                :error-message="serverErrors?.document"
+                class="col-md-6 col-12"
+              />
+
+              <q-input
+                v-model="form.rg"
+                mask="##.###.###-#"
+                fill-mask
+                :label="$t('provider.fields.rg')"
+                :error="!!serverErrors?.rg"
+                :error-message="serverErrors?.rg"
+                class="col-md-6 col-12"
+              />
+              <DefaultInputDatePicker
+                v-model:untreated-date="form.birth_date"
+                :label="$t('provider.fields.birth_date')"
+                :error="serverErrors?.birth_date"
+                :error-message="serverErrors?.birth_date"
+                class="col-md-6 col-12"
+              />
+
+              <DefaultCurrencyInput
+                v-model="form.daily_price_8h"
+                :label="$t('provider.fields.daily_price_8h')"
+                :error="!!serverErrors?.daily_price_8h"
+                :error-message="serverErrors?.daily_price_8h"
+                :hint="$t('provider.hints.daily_price')"
+                lazy-rules
+                class="col-md-3 col-6"
+              />
+
+              <DefaultCurrencyInput
+                v-model="form.daily_price_6h"
+                :label="$t('provider.fields.daily_price_6h')"
+                :error="!!serverErrors?.daily_price_6h"
+                :error-message="serverErrors?.daily_price_6h"
+                disable
+                class="col-md-3 col-6"
+              />
+
+              <DefaultCurrencyInput
+                v-model="form.daily_price_4h"
+                :label="$t('provider.fields.daily_price_4h')"
+                :error="!!serverErrors?.daily_price_4h"
+                :error-message="serverErrors?.daily_price_4h"
+                disable
+                class="col-md-3 col-6"
+              />
+
+              <DefaultCurrencyInput
+                v-model="form.daily_price_2h"
+                :label="$t('provider.fields.daily_price_2h')"
+                :error="!!serverErrors?.daily_price_2h"
+                :error-message="serverErrors?.daily_price_2h"
+                disable
+                class="col-md-3 col-6"
+              />
+
+              <div class="col-12">
+                <q-checkbox
+                  v-model="form.is_approved"
+                  :label="$t('provider.fields.is_approved')"
+                />
               </div>
 
-              <div class="col row q-col-gutter-sm">
-                <div class="col-md-6 col-12">
-                  <div class="text-subtitle2 text-grey-7">
-                    {{ $t('provider.fields.average_rating') }}
-                  </div>
-                  <div class="text-body1 text-weight-medium">
-                    {{ provider?.average_rating || '-' }}
+              <div class="col-12 q-mt-md">
+                <div class="row q-col-gutter-md">
+                  <div class="col-auto flex items-center">
+                    <q-avatar size="80px" color="grey-3">
+                      <q-icon name="mdi-account" size="50px" color="grey-6" />
+                    </q-avatar>
                   </div>
-                </div>
 
-                <div class="col-md-6 col-12">
-                  <div class="text-subtitle2 text-grey-7">
-                    {{ $t('provider.fields.total_services') }}
-                  </div>
-                  <div class="text-body1 text-weight-medium">
-                    {{ provider?.total_services || '0' }}
-                  </div>
-                </div>
+                  <div class="col row q-col-gutter-sm">
+                    <div class="col-md-6 col-12">
+                      <div class="text-subtitle2 text-grey-7">
+                        {{ $t('provider.fields.average_rating') }}
+                      </div>
+                      <div class="text-body1 text-weight-medium">
+                        {{ provider?.average_rating || '-' }}
+                      </div>
+                    </div>
 
-                <div class="col-md-6 col-12">
-                  <div class="text-subtitle2 text-grey-7">
-                    {{ $t('provider.fields.selfie_verified') }}
-                  </div>
-                  <div class="text-body1 text-weight-medium">
-                    {{ provider?.selfie_verified ? $t('common.status.yes') : $t('common.status.no') }}
-                  </div>
-                </div>
+                    <div class="col-md-6 col-12">
+                      <div class="text-subtitle2 text-grey-7">
+                        {{ $t('provider.fields.total_services') }}
+                      </div>
+                      <div class="text-body1 text-weight-medium">
+                        {{ provider?.total_services || '0' }}
+                      </div>
+                    </div>
 
-                <div class="col-md-6 col-12">
-                  <div class="text-subtitle2 text-grey-7">
-                    {{ $t('provider.fields.document_verified') }}
-                  </div>
-                  <div class="text-body1 text-weight-medium">
-                    {{ provider?.document_verified ? $t('common.status.yes') : $t('common.status.no') }}
+                    <div class="col-md-6 col-12">
+                      <div class="text-subtitle2 text-grey-7">
+                        {{ $t('provider.fields.selfie_verified') }}
+                      </div>
+                      <div class="text-body1 text-weight-medium">
+                        {{ provider?.selfie_verified ? $t('common.status.yes') : $t('common.status.no') }}
+                      </div>
+                    </div>
+
+                    <div class="col-md-6 col-12">
+                      <div class="text-subtitle2 text-grey-7">
+                        {{ $t('provider.fields.document_verified') }}
+                      </div>
+                      <div class="text-body1 text-weight-medium">
+                        {{ provider?.document_verified ? $t('common.status.yes') : $t('common.status.no') }}
+                      </div>
+                    </div>
                   </div>
                 </div>
               </div>
-            </div>
-          </div>
-        </q-card-section>
-        <q-card-actions align="center">
-          <q-btn color="primary" label="Cancel" @click="onDialogCancel" />
-          <q-space />
-          <q-btn
-            color="primary"
-            label="OK"
-            :type="'submit'"
-            :loading="loading"
-            :disable="!hasUpdatedFields"
-          />
-        </q-card-actions>
-      </q-form>
+            </q-card-section>
+            <q-card-actions align="center">
+              <q-btn color="primary" label="Cancel" @click="onDialogCancel" />
+              <q-space />
+              <q-btn color="primary" label="OK" :type="'submit'" :loading="loading" :disable="!hasUpdatedFields" />
+            </q-card-actions>
+          </q-form>
+        </q-tab-panel>
+
+        <q-tab-panel v-if="provider" name="addresses">
+          <AddressesPanel :source="'provider'" :source-id="provider.id" />
+        </q-tab-panel>
+      </q-tab-panels>
     </q-card>
   </q-dialog>
 </template>
@@ -164,6 +174,7 @@ import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue
 import UserSelect from "src/components/user/UserSelect.vue";
 import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
 import DefaultCurrencyInput from "src/components/defaults/DefaultCurrencyInput.vue";
+import AddressesPanel from "src/pages/address/components/AddressesPanel.vue";
 
 defineEmits([
   ...useDialogPluginComponent.emits,
@@ -187,6 +198,7 @@ const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
   useDialogPluginComponent();
 
 const formRef = useTemplateRef("formRef");
+const tab = ref("data");
 
 const { form, getUpdatedFields, hasUpdatedFields } = useFormUpdateTracker({
   user_id: provider ? provider?.user_id : null,