Procházet zdrojové kódy

feat: :sparkles: feat (meusdados-parceiro) criada tela de edicao dos dados do parceiro

foi criada a tela de edicao dos dados do parceiro no layout parceiro, incluindo todas as abas do parceiro e os serviços

fase:dev | origin:escopo
Gustavo Zanatta před 3 týdny
rodič
revize
5ac6f7f519

+ 32 - 0
src/api/partnerAgreement.js

@@ -74,3 +74,35 @@ export const uploadPartnerMedia = async (id, file) => {
 export const deletePartnerMedia = async (id, mediaId) => {
   await api.delete(`/partner-agreement/${id}/media/${mediaId}`);
 };
+
+export const getMyPartnerAgreement = async () => {
+  const { data } = await api.get("/partner-agreement/my");
+  return data.payload;
+};
+
+export const updateMyPartnerAgreement = async (payload) => {
+  const { data } = await api.put("/partner-agreement/my", payload);
+  return data.payload;
+};
+
+export const uploadMyPartnerLogo = async (file) => {
+  const form = new FormData();
+  form.append("logo", file);
+  const { data } = await api.post("/partner-agreement/my/logo", form, {
+    headers: { "Content-Type": "multipart/form-data" },
+  });
+  return data.payload;
+};
+
+export const uploadMyPartnerMedia = async (file) => {
+  const form = new FormData();
+  form.append("file", file);
+  const { data } = await api.post("/partner-agreement/my/media", form, {
+    headers: { "Content-Type": "multipart/form-data" },
+  });
+  return data.payload;
+};
+
+export const deleteMyPartnerMedia = async (mediaId) => {
+  await api.delete(`/partner-agreement/my/media/${mediaId}`);
+};

+ 5 - 0
src/api/user.js

@@ -35,6 +35,11 @@ export const getAssociados = async () => {
   return users.filter((u) => u.type === "associado");
 };
 
+export const getParceiros = async () => {
+  const users = await getUsers();
+  return users.filter((u) => u.type === "parceiro");
+};
+
 export const getUsersPaginated = async ({ page = 1, perPage = 10, filter, type, status } = {}) => {
   const params = { page, per_page: perPage };
   if (type)   params.type   = type;

+ 72 - 0
src/components/selects/ParceiroSelect.vue

@@ -0,0 +1,72 @@
+<template>
+  <DefaultSelect
+    v-model="selected"
+    v-bind="$attrs"
+    use-input
+    hide-selected
+    fill-input
+    clearable
+    :options="options"
+    :label
+    :loading
+    :placeholder
+    @filter="filterFn"
+  >
+    <template #no-option>
+      <q-item>
+        <q-item-section class="text-grey">
+          {{ $t("http.errors.no_records_found") }}
+        </q-item-section>
+      </q-item>
+    </template>
+  </DefaultSelect>
+</template>
+
+<script setup>
+import { ref, onMounted } from "vue";
+import { getParceiros } from "src/api/user";
+import { normalizeString } from "src/helpers/utils";
+import { useI18n } from "vue-i18n";
+import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
+
+const { label, placeholder } = defineProps({
+  label: {
+    type: String,
+    default: () => useI18n().t("parceiro.usuario_parceiro"),
+  },
+  placeholder: {
+    type: String,
+    default: () => useI18n().t("common.actions.search"),
+  },
+});
+
+const selected = defineModel({ type: Object });
+
+const loading = ref(true);
+const baseOptions = ref([]);
+const options = ref([]);
+
+const filterFn = (val, update) => {
+  const needle = normalizeString(val);
+  options.value = baseOptions.value.filter((v) =>
+    normalizeString(v.label).includes(needle),
+  );
+  update();
+};
+
+onMounted(async () => {
+  try {
+    const parceiros = await getParceiros();
+    baseOptions.value = parceiros.map((p) => ({
+      label: p.name,
+      value: p.id,
+      data: p,
+    }));
+    options.value = baseOptions.value;
+  } catch (e) {
+    console.error(e);
+  } finally {
+    loading.value = false;
+  }
+});
+</script>

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

@@ -621,7 +621,8 @@
     "tab_contato": "Contact",
     "tab_endereco": "Address",
     "tab_horario": "Schedule & Contract",
-    "tab_servicos": "My Services"
+    "tab_servicos": "My Services",
+    "usuario_parceiro": "Partner User"
   },
   "notification": {
     "new": "New notification",

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

@@ -620,7 +620,8 @@
     "tab_contato": "Contacto",
     "tab_endereco": "Dirección",
     "tab_horario": "Horario y Contrato",
-    "tab_servicos": "Mis Servicios"
+    "tab_servicos": "Mis Servicios",
+    "usuario_parceiro": "Usuario Socio"
   },
   "notification": {
     "new": "Nueva notificación",

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

@@ -621,7 +621,8 @@
     "tab_contato": "Contato",
     "tab_endereco": "Endereço",
     "tab_horario": "Horário e Contrato",
-    "tab_servicos": "Meus Serviços"
+    "tab_servicos": "Meus Serviços",
+    "usuario_parceiro": "Usuário Parceiro"
   },
   "notification": {
     "new": "Nova notificação",

+ 17 - 0
src/pages/parceiros-convenios/ParceiroCadastroPage.vue

@@ -53,6 +53,11 @@
                 v-model:error="errorsDados.category_id"
                 class="col-12"
               />
+              <ParceiroSelect
+                v-model="selectedUser"
+                v-model:error="errorsDados.user_id"
+                class="col-12 input-violet"
+              />
               <DefaultInput
                 v-model="formDados.responsible"
                 v-model:error="errorsDados.responsible"
@@ -351,6 +356,7 @@ import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePick
 import DefaultFilePicker from "src/components/defaults/DefaultFilePicker.vue";
 import DefaultTable from "src/components/defaults/DefaultTable.vue";
 import PartnerAgreementCategorySelect from "src/components/selects/PartnerAgreementCategorySelect.vue";
+import ParceiroSelect from "src/components/selects/ParceiroSelect.vue";
 import CitySelect from "src/components/selects/CitySelect.vue";
 import StateSelect from "src/components/selects/StateSelect.vue";
 
@@ -409,6 +415,7 @@ const tabsItems = computed(() => [
 const formDadosRef = useTemplateRef("formDadosRef");
 const logoFile = ref(null);
 const selectedCategory = ref(null);
+const selectedUser = ref(null);
 
 const {
   form: formDados,
@@ -418,6 +425,7 @@ const {
   company_name:        "",
   cnpj:                "",
   category_id:         null,
+  user_id:             null,
   responsible:         "",
   discount_percentage: null,
   description:         "",
@@ -433,6 +441,10 @@ watch(selectedCategory, (val) => {
   formDados.category_id = val?.value ?? null;
 });
 
+watch(selectedUser, (val) => {
+  formDados.user_id = val?.value ?? null;
+});
+
 const saveDados = async () => {
   await execDados(async () => {
     let savedPartner;
@@ -630,6 +642,7 @@ const populateForms = (p) => {
   formDados.company_name        = p.company_name        ?? "";
   formDados.cnpj                = p.cnpj                ?? "";
   formDados.category_id         = p.category_id         ?? null;
+  formDados.user_id             = p.user_id             ?? null;
   formDados.responsible         = p.responsible         ?? "";
   formDados.discount_percentage = p.discount_percentage != null ? String(p.discount_percentage) : null;
   formDados.description         = p.description         ?? "";
@@ -638,6 +651,10 @@ const populateForms = (p) => {
     ? { label: p.category.name, value: p.category.id }
     : null;
 
+  selectedUser.value = p.user
+    ? { label: p.user.name, value: p.user.id }
+    : null;
+
   formContato.email    = p.email    ?? "";
   formContato.phone    = p.phone    ?? "";
   formContato.whatsapp = p.whatsapp ?? "";

+ 689 - 0
src/pages/parceiros-convenios/ParceiroDadosPage.vue

@@ -0,0 +1,689 @@
+<template>
+  <div class="cadastro-page q-mr-md">
+    <DefaultHeaderPage :title="{ value: $t('parceiro.dados_parceiro') }" />
+
+    <div v-if="loadingPage" class="flex flex-center q-pa-xl">
+      <q-spinner color="violet-normal" size="50px" />
+    </div>
+
+    <template v-else>
+      <div class="cadastro-page__tabs q-mb-md">
+        <div
+          v-for="tab in tabsItems"
+          :key="tab.name"
+          class="cadastro-page__tab"
+          :class="{ 'cadastro-page__tab--active': activeTab === tab.name }"
+          @click="activeTab = tab.name"
+        >
+          {{ tab.label }}
+        </div>
+      </div>
+
+      <q-tab-panels v-model="activeTab" animated keep-alive>
+
+        <q-tab-panel name="dados" class="q-pa-none">
+          <q-form ref="formDadosRef" @submit="saveDados">
+            <div class="q-pr-md q-pb-md bg-violet-light">
+              <DefaultFilePicker
+                v-model="logoFile"
+                type="image"
+                :label="$t('parceiro.logo')"
+                :initial-image="partner?.logo?.url"
+                style="height: 220px; max-width: 280px"
+              />
+            </div>
+
+            <q-card flat class="cadastro-card q-pa-md">
+              <div class="row q-col-gutter-sm">
+                <DefaultInput
+                  v-model="formDados.company_name"
+                  v-model:error="errorsDados.company_name"
+                  :rules="[inputRules.required]"
+                  :label="$t('parceiro.company_name')"
+                  class="col-12"
+                />
+                <DefaultInput
+                  v-model="formDados.cnpj"
+                  v-model:error="errorsDados.cnpj"
+                  :label="$t('common.terms.cnpj')"
+                  :placeholder="'00.000.000/0000-00'"
+                  :mask="'##.###.###/####-##'"
+                  class="col-12"
+                />
+                <PartnerAgreementCategorySelect
+                  v-model="selectedCategory"
+                  v-model:error="errorsDados.category_id"
+                  class="col-12"
+                />
+                <DefaultInput
+                  v-model="formDados.responsible"
+                  v-model:error="errorsDados.responsible"
+                  :label="$t('parceiro.responsible')"
+                  class="col-12"
+                />
+                <DefaultInput
+                  v-model="formDados.discount_percentage"
+                  v-model:error="errorsDados.discount_percentage"
+                  :label="$t('parceiro.discount_percentage')"
+                  type="number"
+                  suffix="%"
+                  class="col-12"
+                />
+                <DefaultInput
+                  v-model="formDados.description"
+                  v-model:error="errorsDados.description"
+                  :label="$t('common.terms.description')"
+                  type="textarea"
+                  autogrow
+                  class="col-12"
+                />
+              </div>
+            </q-card>
+
+            <div class="row justify-end q-py-md bg-violet-light">
+              <q-btn
+                unelevated
+                class="btn-gradient"
+                :label="$t('common.actions.save')"
+                icon="mdi-check"
+                type="submit"
+                :loading="loadingDados"
+                :disable="!hasUpdatedDados"
+              />
+            </div>
+          </q-form>
+        </q-tab-panel>
+
+        <q-tab-panel name="contato" class="q-pa-none">
+          <q-form ref="formContatoRef" @submit="saveContato">
+            <q-card flat class="cadastro-card q-pa-md">
+              <div class="row q-col-gutter-sm">
+                <DefaultInput
+                  v-model="formContato.email"
+                  v-model:error="errorsContato.email"
+                  :rules="[inputRules.email]"
+                  :label="$t('common.terms.email')"
+                  class="col-12"
+                />
+                <DefaultInput
+                  v-model="formContato.website"
+                  v-model:error="errorsContato.website"
+                  :label="$t('parceiro.website')"
+                  class="col-12"
+                />
+                <DefaultInput
+                  v-model="formContato.phone"
+                  v-model:error="errorsContato.phone"
+                  :label="$t('common.terms.phone')"
+                  :mask="'(##) #####-####'"
+                  class="col-12"
+                />
+                <DefaultInput
+                  v-model="formContato.whatsapp"
+                  v-model:error="errorsContato.whatsapp"
+                  :label="$t('common.terms.whatsapp')"
+                  :mask="'(##) #####-####'"
+                  class="col-12"
+                />
+              </div>
+            </q-card>
+
+            <div class="row justify-end bg-violet-light q-pt-md">
+              <q-btn
+                unelevated
+                class="btn-gradient"
+                :label="$t('common.actions.save')"
+                icon="mdi-check"
+                type="submit"
+                :loading="loadingContato"
+                :disable="!hasUpdatedContato"
+              />
+            </div>
+          </q-form>
+        </q-tab-panel>
+
+        <q-tab-panel name="endereco" class="q-pa-none">
+          <q-form ref="formEnderecoRef" @submit="saveEndereco">
+            <q-card flat class="cadastro-card q-pa-md">
+              <div class="row q-col-gutter-sm">
+                <DefaultInput
+                  v-model="formEndereco.zip_code"
+                  v-model:error="errorsEndereco.zip_code"
+                  :label="$t('parceiro.zip_code')"
+                  :placeholder="'00000-000'"
+                  :mask="'#####-###'"
+                  class="col-md-4 col-12"
+                  @update:model-value="onCepChange"
+                />
+                <DefaultInput
+                  v-model="formEndereco.address"
+                  v-model:error="errorsEndereco.address"
+                  :label="$t('parceiro.address')"
+                  :loading="loadingCep"
+                  class="col-md-8 col-12"
+                />
+                <DefaultInput
+                  v-model="formEndereco.neighborhood"
+                  v-model:error="errorsEndereco.neighborhood"
+                  :label="$t('parceiro.neighborhood')"
+                  :loading="loadingCep"
+                  class="col-12"
+                />
+                <StateSelect
+                  v-model="selectedState"
+                  v-model:error="errorsEndereco.state_id"
+                  class="col-md-4 col-12 input-violet"
+                />
+                <CitySelect
+                  v-model="selectedCity"
+                  v-model:error="errorsEndereco.city_id"
+                  :state="selectedState"
+                  class="col-md-8 col-12 input-violet"
+                />
+              </div>
+            </q-card>
+
+            <div class="row justify-end q-pt-md bg-violet-light">
+              <q-btn
+                unelevated
+                class="btn-gradient"
+                :label="$t('common.actions.save')"
+                icon="mdi-check"
+                type="submit"
+                :loading="loadingEndereco"
+                :disable="!hasUpdatedEndereco"
+              />
+            </div>
+          </q-form>
+        </q-tab-panel>
+
+        <q-tab-panel name="horario" class="q-pa-none">
+          <q-form ref="formHorarioRef" @submit="saveHorario">
+            <q-card flat class="cadastro-card q-pa-md">
+              <div class="row q-col-gutter-sm">
+                <DefaultInput
+                  v-model="formHorario.working_hours"
+                  v-model:error="errorsHorario.working_hours"
+                  :label="$t('parceiro.working_hours')"
+                  :placeholder="$t('parceiro.working_hours_placeholder')"
+                  class="col-12"
+                />
+                <DefaultInputDatePicker
+                  v-model:untreated-date="formHorario.contract_start"
+                  v-model:error="errorsHorario.contract_start"
+                  :label="$t('parceiro.contract_start')"
+                  class="col-md-6 col-12"
+                />
+                <DefaultInputDatePicker
+                  v-model:untreated-date="formHorario.contract_end"
+                  v-model:error="errorsHorario.contract_end"
+                  :label="$t('parceiro.contract_end')"
+                  class="col-md-6 col-12"
+                />
+              </div>
+
+              <div v-if="contractMedia.length > 0" class="q-mt-md">
+                <div class="text-subtitle2 text-violet-normal q-mb-xs">{{ $t("parceiro.contract_files") }}</div>
+                <div class="row q-col-gutter-xs">
+                  <div v-for="media in contractMedia" :key="media.id" class="col-auto">
+                    <q-chip
+                      removable
+                      clickable
+                      color="violet-light"
+                      text-color="violet-normal"
+                      icon="mdi-file-outline"
+                      :label="media.name"
+                      @click="openMedia(media)"
+                      @remove="removeMedia(media)"
+                    />
+                  </div>
+                </div>
+              </div>
+            </q-card>
+
+            <div class="row justify-end items-center q-pt-md bg-violet-light" style="gap: 8px">
+              <q-file
+                ref="mediaFileInputRef"
+                v-model="newMediaFile"
+                accept=".pdf,.doc,.docx,.png,.jpg,.jpeg"
+                style="display: none"
+                @update:model-value="onMediaFileSelected"
+              />
+              <q-btn
+                unelevated
+                class="btn-gradient"
+                :label="$t('parceiro.add_file')"
+                icon="mdi-paperclip"
+                :disable="!partnerId"
+                @click="triggerMediaUpload"
+              />
+              <q-btn
+                unelevated
+                class="btn-gradient"
+                :label="$t('common.actions.save')"
+                icon="mdi-check"
+                type="submit"
+                :loading="loadingHorario"
+                :disable="!hasUpdatedHorario"
+              />
+            </div>
+          </q-form>
+        </q-tab-panel>
+
+        <q-tab-panel name="servicos" class="q-pa-none">
+          <div class="bg-violet-light q-pb-md">
+            <div class="row justify-end q-mb-sm q-gutter-sm">
+              <q-btn
+                unelevated
+                icon="mdi-plus"
+                :label="$t('common.actions.new')"
+                padding="6px 12px"
+                :disable="!partnerId"
+                class="btn-gradient"
+                @click="onAddService"
+              />
+            </div>
+
+            <q-input
+              v-model="serviceSearch"
+              :placeholder="$t('parceiro.search_placeholder')"
+              outlined
+              dense
+              clearable
+              color="violet-normal"
+              class="q-mb-sm service-search"
+            >
+              <template #prepend>
+                <q-icon name="mdi-magnify" color="violet-normal" />
+              </template>
+            </q-input>
+          </div>
+
+          <q-card class="">
+            <div v-if="loadingServices" class="flex flex-center q-pa-xl">
+              <q-spinner color="violet-normal" size="40px" />
+            </div>
+
+            <DefaultTable
+              v-else
+              v-model:rows="filteredServices"
+              :columns="serviceColumns"
+              no-api-call
+              :show-search-field="false"
+              open-item
+              @on-row-click="({ row }) => onEditService(row)"
+            />
+          </q-card>
+        </q-tab-panel>
+
+      </q-tab-panels>
+    </template>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, watch, onMounted, useTemplateRef } from "vue";
+import { useRouter, useRoute } from "vue-router";
+import { useQuasar } from "quasar";
+import { useI18n } from "vue-i18n";
+import { useInputRules } from "src/composables/useInputRules";
+import { useFormUpdateTracker } from "src/composables/useFormUpdateTracker";
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
+import {
+  getMyPartnerAgreement,
+  updateMyPartnerAgreement,
+  uploadMyPartnerLogo,
+  uploadMyPartnerMedia,
+  deleteMyPartnerMedia,
+} from "src/api/partnerAgreement";
+import { getServicesByPartner } from "src/api/partnerAgreementService";
+import axios from "axios";
+
+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 DefaultFilePicker from "src/components/defaults/DefaultFilePicker.vue";
+import DefaultTable from "src/components/defaults/DefaultTable.vue";
+import PartnerAgreementCategorySelect from "src/components/selects/PartnerAgreementCategorySelect.vue";
+import CitySelect from "src/components/selects/CitySelect.vue";
+import StateSelect from "src/components/selects/StateSelect.vue";
+
+const router = useRouter();
+const route  = useRoute();
+const $q     = useQuasar();
+const { t }  = useI18n();
+const { inputRules } = useInputRules();
+
+const loadingPage = ref(true);
+const partner     = ref(null);
+const partnerId   = computed(() => partner.value?.id ?? null);
+const activeTab   = ref(route.query.tab ?? "dados");
+
+const tabsItems = computed(() => [
+  { name: "dados",    label: t("parceiro.tab_dados") },
+  { name: "contato",  label: t("parceiro.tab_contato") },
+  { name: "endereco", label: t("parceiro.tab_endereco") },
+  { name: "horario",  label: t("parceiro.tab_horario") },
+  { name: "servicos", label: t("parceiro.tab_servicos") },
+]);
+
+// ─── Dados ───────────────────────────────────────────────────────────────────
+const formDadosRef    = useTemplateRef("formDadosRef");
+const logoFile        = ref(null);
+const selectedCategory = ref(null);
+
+const {
+  form: formDados,
+  getUpdatedFields: updatedDados,
+  hasUpdatedFields: hasUpdatedDados,
+} = useFormUpdateTracker({
+  company_name:        "",
+  cnpj:                "",
+  category_id:         null,
+  responsible:         "",
+  discount_percentage: null,
+  description:         "",
+});
+
+const {
+  loading: loadingDados,
+  validationErrors: errorsDados,
+  execute: execDados,
+} = useSubmitHandler({ formRef: formDadosRef });
+
+watch(selectedCategory, (val) => {
+  formDados.category_id = val?.value ?? null;
+});
+
+const saveDados = async () => {
+  await execDados(async () => {
+    let saved = await updateMyPartnerAgreement(updatedDados.value);
+    if (logoFile.value instanceof File) {
+      const uploadedMedia = await uploadMyPartnerLogo(logoFile.value);
+      saved = { ...saved, logo: uploadedMedia };
+    }
+    partner.value = saved;
+    populateForms(saved);
+    logoFile.value = null;
+    $q.notify({ type: "positive", message: t("http.success") });
+  });
+};
+
+// ─── Contato ─────────────────────────────────────────────────────────────────
+const formContatoRef = useTemplateRef("formContatoRef");
+const {
+  form: formContato,
+  getUpdatedFields: updatedContato,
+  hasUpdatedFields: hasUpdatedContato,
+} = useFormUpdateTracker({ email: "", phone: "", whatsapp: "", website: "" });
+
+const {
+  loading: loadingContato,
+  validationErrors: errorsContato,
+  execute: execContato,
+} = useSubmitHandler({ formRef: formContatoRef });
+
+const saveContato = async () => {
+  await execContato(async () => {
+    const saved = await updateMyPartnerAgreement(updatedContato.value);
+    partner.value = saved;
+    $q.notify({ type: "positive", message: t("http.success") });
+  });
+};
+
+// ─── Endereço ────────────────────────────────────────────────────────────────
+const formEnderecoRef = useTemplateRef("formEnderecoRef");
+const selectedState   = ref(null);
+const selectedCity    = ref(null);
+const loadingCep      = ref(false);
+
+const {
+  form: formEndereco,
+  getUpdatedFields: updatedEndereco,
+  hasUpdatedFields: hasUpdatedEndereco,
+} = useFormUpdateTracker({
+  zip_code:     "",
+  address:      "",
+  neighborhood: "",
+  city_id:      null,
+  state_id:     null,
+});
+
+const {
+  loading: loadingEndereco,
+  validationErrors: errorsEndereco,
+  execute: execEndereco,
+} = useSubmitHandler({ formRef: formEnderecoRef });
+
+watch(selectedState, (val) => { formEndereco.state_id = val?.value ?? null; });
+watch(selectedCity,  (val) => { formEndereco.city_id  = val?.value ?? null; });
+
+const onCepChange = async (val) => {
+  const digits = (val ?? "").replace(/\D/g, "");
+  if (digits.length !== 8) return;
+  loadingCep.value = true;
+  try {
+    const { data } = await axios.get(`https://viacep.com.br/ws/${digits}/json/`);
+    if (!data.erro) {
+      formEndereco.address      = data.logradouro ?? formEndereco.address;
+      formEndereco.neighborhood = data.bairro      ?? formEndereco.neighborhood;
+    }
+  } catch {
+    // silent — user fills manually
+  } finally {
+    loadingCep.value = false;
+  }
+};
+
+const saveEndereco = async () => {
+  await execEndereco(async () => {
+    const saved = await updateMyPartnerAgreement(updatedEndereco.value);
+    partner.value = saved;
+    $q.notify({ type: "positive", message: t("http.success") });
+  });
+};
+
+// ─── Horário e Contrato ───────────────────────────────────────────────────────
+const formHorarioRef   = useTemplateRef("formHorarioRef");
+const mediaFileInputRef = useTemplateRef("mediaFileInputRef");
+const newMediaFile      = ref(null);
+const contractMedia     = ref([]);
+
+const {
+  form: formHorario,
+  getUpdatedFields: updatedHorario,
+  hasUpdatedFields: hasUpdatedHorario,
+} = useFormUpdateTracker({ working_hours: "", contract_start: null, contract_end: null });
+
+const {
+  loading: loadingHorario,
+  validationErrors: errorsHorario,
+  execute: execHorario,
+} = useSubmitHandler({ formRef: formHorarioRef });
+
+const onMediaFileSelected = async (file) => {
+  if (!file) return;
+  try {
+    const media = await uploadMyPartnerMedia(file);
+    contractMedia.value.push(media);
+  } catch {
+    $q.notify({ type: "negative", message: t("http.errors.failed") });
+  } finally {
+    newMediaFile.value = null;
+  }
+};
+
+const triggerMediaUpload = () => {
+  mediaFileInputRef.value?.pickFiles();
+};
+
+const removeMedia = async (media) => {
+  try {
+    await deleteMyPartnerMedia(media.id);
+    contractMedia.value = contractMedia.value.filter((m) => m.id !== media.id);
+  } catch {
+    $q.notify({ type: "negative", message: t("http.errors.failed") });
+  }
+};
+
+const openMedia = (media) => {
+  window.open(media.url, "_blank");
+};
+
+const saveHorario = async () => {
+  await execHorario(async () => {
+    const saved = await updateMyPartnerAgreement(updatedHorario.value);
+    partner.value = saved;
+    $q.notify({ type: "positive", message: t("http.success") });
+  });
+};
+
+// ─── Serviços ────────────────────────────────────────────────────────────────
+const services        = ref([]);
+const loadingServices = ref(false);
+const serviceSearch   = ref("");
+
+const filteredServices = computed(() => {
+  if (!serviceSearch.value) return services.value;
+  const needle = serviceSearch.value.toLowerCase();
+  return services.value.filter((s) =>
+    [s.name, s.category?.name, s.price, s.associate_price]
+      .some((f) => f != null && String(f).toLowerCase().includes(needle))
+  );
+});
+
+const serviceColumns = computed(() => [
+  { name: "name",            label: t("common.terms.name"),         field: "name",            align: "left", sortable: true },
+  { name: "category",        label: t("parceiro.service_category"), field: (r) => r.category?.name ?? "—", align: "left" },
+  { name: "price",           label: t("parceiro.price"),            field: "price",            align: "left" },
+  { name: "associate_price", label: t("parceiro.associate_price"),  field: "associate_price",  align: "left" },
+  { name: "actions",         label: t("common.terms.actions"),      align: "right", required: true },
+]);
+
+const loadServices = async () => {
+  if (!partnerId.value) return;
+  loadingServices.value = true;
+  try {
+    services.value = await getServicesByPartner(partnerId.value);
+  } finally {
+    loadingServices.value = false;
+  }
+};
+
+const onAddService = () => {
+  router.push({
+    name:   "ParceiroDadosServicoPage",
+    params: { id: partnerId.value },
+  });
+};
+
+const onEditService = (svc) => {
+  router.push({
+    name:   "ParceiroDadosServicoPage",
+    params: { id: partnerId.value, serviceId: svc.id },
+  });
+};
+
+// ─── Populate ────────────────────────────────────────────────────────────────
+const populateForms = (p) => {
+  if (!p) return;
+
+  formDados.company_name        = p.company_name        ?? "";
+  formDados.cnpj                = p.cnpj                ?? "";
+  formDados.category_id         = p.category_id         ?? null;
+  formDados.responsible         = p.responsible         ?? "";
+  formDados.discount_percentage = p.discount_percentage != null ? String(p.discount_percentage) : null;
+  formDados.description         = p.description         ?? "";
+
+  selectedCategory.value = p.category
+    ? { label: p.category.name, value: p.category.id }
+    : null;
+
+  formContato.email    = p.email    ?? "";
+  formContato.phone    = p.phone    ?? "";
+  formContato.whatsapp = p.whatsapp ?? "";
+  formContato.website  = p.website  ?? "";
+
+  formEndereco.zip_code     = p.zip_code     ?? "";
+  formEndereco.address      = p.address      ?? "";
+  formEndereco.neighborhood = p.neighborhood ?? "";
+  formEndereco.city_id      = p.city_id      ?? null;
+  formEndereco.state_id     = p.state_id     ?? null;
+
+  selectedState.value = p.state ? { label: p.state.name, value: p.state.id } : null;
+  selectedCity.value  = p.city  ? { label: p.city.name,  value: p.city.id  } : null;
+
+  formHorario.working_hours  = p.working_hours  ?? "";
+  formHorario.contract_start = p.contract_start ?? null;
+  formHorario.contract_end   = p.contract_end   ?? null;
+
+  contractMedia.value = p.media ?? [];
+};
+
+onMounted(async () => {
+  try {
+    const p = await getMyPartnerAgreement();
+    partner.value = p;
+    populateForms(p);
+  } catch {
+    $q.notify({ type: "negative", message: t("http.errors.failed") });
+  } finally {
+    loadingPage.value = false;
+  }
+});
+
+watch(activeTab, (tab) => {
+  if (tab === "servicos") loadServices();
+});
+</script>
+
+<style lang="scss" scoped>
+@use "src/css/quasar.variables.scss";
+
+.cadastro-page {
+  &__tabs {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 6px;
+  }
+
+  &__tab {
+    padding: 4px 16px;
+    border-radius: 6px;
+    font-size: 14px;
+    font-weight: 600;
+    cursor: pointer;
+    background: white;
+    color: $violet-normal;
+    border: 1.5px solid rgba($violet-normal, 0.3);
+    transition: background 0.15s, color 0.15s;
+    user-select: none;
+
+    &--active {
+      background: $violet-normal;
+      color: white;
+      border-color: $violet-normal;
+    }
+  }
+}
+
+.cadastro-card {
+  border-radius: 12px;
+  background: white;
+}
+
+.btn-gradient {
+  background: linear-gradient(90deg, #4d1658 0%, #8b30a5 100%) !important;
+  color: white !important;
+  border-radius: 8px !important;
+  padding: 8px 16px;
+}
+
+.btn-gradient :deep(.q-icon) {
+  color: white !important;
+}
+
+.service-search :deep(.q-field__control) {
+  border-radius: 24px !important;
+}
+</style>

+ 9 - 5
src/pages/parceiros-convenios/ParceiroServicoCadastroPage.vue

@@ -366,11 +366,15 @@ const onSubmit = async () => {
 };
 
 const goBack = () => {
-  router.push({
-    name:   "ParceiroCadastroPage",
-    params: { id: partnerId.value },
-    query:  { tab: "servicos" },
-  });
+  if (route.name === "ParceiroDadosServicoPage") {
+    router.push({ name: "MeusDadosPage", query: { tab: "servicos" } });
+  } else {
+    router.push({
+      name:   "ParceiroCadastroPage",
+      params: { id: partnerId.value },
+      query:  { tab: "servicos" },
+    });
+  }
 };
 
 onMounted(() => {

+ 29 - 0
src/router/routes/parceiro.route.js

@@ -0,0 +1,29 @@
+export default [
+  {
+    path: "/parceiro/meus-dados",
+    name: "MeusDadosPage",
+    component: () => import("pages/parceiros-convenios/ParceiroDadosPage.vue"),
+    meta: {
+      title: { value: "parceiro.dados_parceiro", translate: true },
+      requireAuth: true,
+      requiredPermission: "parceiro.dados",
+      breadcrumbs: [
+        { name: "MeusDadosPage", title: "parceiro.dados_parceiro", translate: true },
+      ],
+    },
+  },
+  {
+    path: "/parceiro/meus-dados/servico/:id/:serviceId?",
+    name: "ParceiroDadosServicoPage",
+    component: () => import("pages/parceiros-convenios/ParceiroServicoCadastroPage.vue"),
+    meta: {
+      title: { value: "parceiro.tab_servicos", translate: true },
+      requireAuth: true,
+      requiredPermission: "parceiro.dados",
+      breadcrumbs: [
+        { name: "MeusDadosPage", title: "parceiro.dados_parceiro", translate: true },
+        { name: "ParceiroDadosServicoPage", title: "parceiro.tab_servicos", translate: true },
+      ],
+    },
+  },
+];