Procházet zdrojové kódy

Merge branch 'feature/SERPRATI-GUS-plataforma-v1' of gogs.softpar.inf.br:Softpar/sfp_front_vue_serprati_digital into feature/SERPRATI-GUS-plataforma-v1

kayo henrique před 3 týdny
rodič
revize
ee2b2b2545

+ 13 - 4
src/api/appointment.js

@@ -1,22 +1,31 @@
 import api from "src/api";
 
+// ─── Rotas do Associado ───────────────────────────────────────────────────────
+
 export const getMyAppointments = async () => {
-  const { data } = await api.get("/appointment/my");
+  const { data } = await api.get("/associado/appointment/my");
   return data.payload;
 };
 
 export const createAppointment = async (appointment) => {
-  const { data } = await api.post("/appointment", appointment);
+  const { data } = await api.post("/associado/appointment", appointment);
   return data.payload;
 };
 
 export const cancelAppointment = async (id) => {
-  const { data } = await api.put(`/appointment/${id}`, { status: "cancelado" });
+  const { data } = await api.put(`/associado/appointment/${id}`, { status: "cancelado" });
   return data.payload;
 };
 
 export const updateAppointment = async (id, payload) => {
-  const { data } = await api.put(`/appointment/${id}`, payload);
+  const { data } = await api.put(`/associado/appointment/${id}`, payload);
+  return data.payload;
+};
+
+// ─── Rotas do Parceiro ────────────────────────────────────────────────────────
+
+export const getPartnerAppointments = async () => {
+  const { data } = await api.get("/parceiro/appointment");
   return data.payload;
 };
 

+ 36 - 0
src/api/notification.js

@@ -1,5 +1,41 @@
 import api from "src/api";
 
+// ─── Rotas do Associado ───────────────────────────────────────────────────────
+
+export const getMyNotificationsAssociado = async () => {
+  const { data } = await api.get("/associado/notification/my");
+  return data.payload;
+};
+
+export const getMyUnreadNotificationsAssociado = async () => {
+  const { data } = await api.get("/associado/notification/my/unread");
+  return data.payload;
+};
+
+export const markNotificationAsReadAssociado = async (sendId) => {
+  const { data } = await api.patch(`/associado/notification/${sendId}/read`);
+  return data.payload;
+};
+
+// ─── Rotas do Parceiro ────────────────────────────────────────────────────────
+
+export const getMyNotificationsParceiro = async () => {
+  const { data } = await api.get("/parceiro/notification/my");
+  return data.payload;
+};
+
+export const getMyUnreadNotificationsParceiro = async () => {
+  const { data } = await api.get("/parceiro/notification/my/unread");
+  return data.payload;
+};
+
+export const markNotificationAsReadParceiro = async (sendId) => {
+  const { data } = await api.patch(`/parceiro/notification/${sendId}/read`);
+  return data.payload;
+};
+
+// ─── Rotas Admin ──────────────────────────────────────────────────────────────
+
 export const getMyUnreadNotifications = async () => {
   const { data } = await api.get("/notification/my/unread");
   return data.payload;

+ 51 - 0
src/api/partnerAgreement.js

@@ -74,3 +74,54 @@ export const uploadPartnerMedia = async (id, file) => {
 export const deletePartnerMedia = async (id, mediaId) => {
   await api.delete(`/partner-agreement/${id}/media/${mediaId}`);
 };
+
+// ─── Rotas do Associado ───────────────────────────────────────────────────────
+
+export const getConvenios = async () => {
+  const { data } = await api.get("/associado/partner-agreement");
+  return data.payload;
+};
+
+export const getConvenio = async (id) => {
+  const { data } = await api.get(`/associado/partner-agreement/${id}`);
+  return data.payload;
+};
+
+export const getConvenioDados = async (id) => {
+  const { data } = await api.get(`/associado/partner-agreement/${id}/dados`);
+  return data.payload;
+};
+
+// ─── Rotas do Parceiro (my) ───────────────────────────────────────────────────
+
+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}`);
+};

+ 10 - 0
src/api/partnerAgreementService.js

@@ -1,5 +1,15 @@
 import api from "src/api";
 
+export const getServicesByConvenio = async (partnerAgreementId) => {
+  const { data } = await api.get(`/associado/partner-agreement-service/partner/${partnerAgreementId}`);
+  return data.payload;
+};
+
+export const getConvenioService = async (id) => {
+  const { data } = await api.get(`/associado/partner-agreement-service/${id}`);
+  return data.payload;
+};
+
 export const getServicesByPartner = async (partnerAgreementId) => {
   const { data } = await api.get(`/partner-agreement-service/partner/${partnerAgreementId}`);
   return data.payload;

+ 24 - 0
src/api/storeItem.js

@@ -1,5 +1,29 @@
 import api from "src/api";
 
+// ─── Rotas do Associado ───────────────────────────────────────────────────────
+
+export const getStoreItemsAssociado = async () => {
+  const { data } = await api.get("/associado/store-item");
+  return data.payload;
+};
+
+export const getStoreItemAssociado = async (id) => {
+  const { data } = await api.get(`/associado/store-item/${id}`);
+  return data.payload;
+};
+
+export const getMyInterestsAssociado = async () => {
+  const { data } = await api.get("/associado/store-item/my/interests");
+  return data.payload;
+};
+
+export const toggleInterestAssociado = async (id) => {
+  const { data } = await api.post(`/associado/store-item/${id}/interest`);
+  return data.payload;
+};
+
+// ─── Rotas Admin ──────────────────────────────────────────────────────────────
+
 export const getStoreItems = async () => {
   const { data } = await api.get("/store-item");
   return data.payload;

+ 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>

+ 7 - 3
src/components/selects/PartnerAgreementSelect.vue

@@ -24,12 +24,12 @@
 
 <script setup>
 import { ref, onMounted } from "vue";
-import { getPartnerAgreements } from "src/api/partnerAgreement";
+import { getPartnerAgreements, getConvenios } from "src/api/partnerAgreement";
 import { normalizeString } from "src/helpers/utils";
 import { useI18n } from "vue-i18n";
 import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
 
-const { label, placeholder } = defineProps({
+const { label, placeholder, forAssociado } = defineProps({
   label: {
     type: String,
     default: () => useI18n().t("ui.navigation.convenios"),
@@ -38,6 +38,10 @@ const { label, placeholder } = defineProps({
     type: String,
     default: () => useI18n().t("common.actions.search"),
   },
+  forAssociado: {
+    type: Boolean,
+    default: false,
+  },
 });
 
 const selectedPartner = defineModel({ type: Object });
@@ -56,7 +60,7 @@ const filterFn = (val, update) => {
 
 onMounted(async () => {
   try {
-    const partners = await getPartnerAgreements();
+    const partners = await (forAssociado ? getConvenios() : getPartnerAgreements());
     baseOptions.value = partners.map((p) => ({
       label: p.trade_name || p.company_name,
       value: p.id,

+ 7 - 3
src/components/selects/PartnerAgreementServiceSelect.vue

@@ -20,11 +20,11 @@
 
 <script setup>
 import { ref, watch } from "vue";
-import { getServicesByPartner } from "src/api/partnerAgreementService";
+import { getServicesByPartner, getServicesByConvenio } from "src/api/partnerAgreementService";
 import { useI18n } from "vue-i18n";
 import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
 
-const { label, placeholder, partnerAgreementId } = defineProps({
+const { label, placeholder, partnerAgreementId, forAssociado } = defineProps({
   label: {
     type: String,
     default: () => useI18n().t("associado.service"),
@@ -37,6 +37,10 @@ const { label, placeholder, partnerAgreementId } = defineProps({
     type: Number,
     default: null,
   },
+  forAssociado: {
+    type: Boolean,
+    default: false,
+  },
 });
 
 const selectedService = defineModel({ type: Object });
@@ -53,7 +57,7 @@ watch(
     if (!id) return;
     loading.value = true;
     try {
-      const services = await getServicesByPartner(id);
+      const services = await (forAssociado ? getServicesByConvenio(id) : getServicesByPartner(id));
       serviceOptions.value = services.map((s) => ({
         label: s.name,
         value: s.id,

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

@@ -622,6 +622,7 @@
     "tab_endereco": "Address",
     "tab_horario": "Schedule & Contract",
     "tab_servicos": "My Services",
+    "usuario_parceiro": "Partner User",
     "parceiro": {
       "dashboard": {
         "title": "Partner Portal",

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

@@ -621,6 +621,7 @@
     "tab_endereco": "Dirección",
     "tab_horario": "Horario y Contrato",
     "tab_servicos": "Mis Servicios",
+    "usuario_parceiro": "Usuario Socio",
     "parceiro": {
       "dashboard": {
         " title": "Portal del Socio",

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

@@ -622,6 +622,7 @@
     "tab_endereco": "Endereço",
     "tab_horario": "Horário e Contrato",
     "tab_servicos": "Meus Serviços",
+    "usuario_parceiro": "Usuário Parceiro",
     "dashboard": {
       "title": "Portal do Parceiro",
       "authorization": "Consultas para Autorização",

+ 2 - 0
src/pages/associado/agendamentos/AgendamentosPage.vue

@@ -26,6 +26,7 @@
                   :label="$t('ui.navigation.convenios')"
                   :rules="[inputRules.required]"
                   class="col-12 input-violet"
+                  for-associado
                 />
                 <PartnerAgreementServiceSelect
                   v-model="selectedService"
@@ -33,6 +34,7 @@
                   :label="$t('associado.service')"
                   :rules="[inputRules.required]"
                   class="col-12 input-violet"
+                  for-associado
                 />
                 <DefaultInputDatePicker
                   v-model:untreated-date="appointmentForm.date"

+ 2 - 2
src/pages/associado/convenios/ConveniosPage.vue

@@ -145,7 +145,7 @@
 
 <script setup>
 import { ref, computed, onMounted } from "vue";
-import { getPartnerAgreements } from "src/api/partnerAgreement";
+import { getConvenios } from "src/api/partnerAgreement";
 import { getCategories } from "src/api/category";
 import { normalizeString } from "src/helpers/utils";
 import { date } from "quasar";
@@ -185,7 +185,7 @@ const openPartnerDetail = (partner) => {
 onMounted(async () => {
   try {
     const [partnerList, categoryList] = await Promise.all([
-      getPartnerAgreements(),
+      getConvenios(),
       getCategories("partner"),
     ]);
     partners.value = partnerList;

+ 2 - 2
src/pages/associado/interesses/InteressesPage.vue

@@ -53,7 +53,7 @@
 import { ref, onMounted } from "vue";
 import { useQuasar } from "quasar";
 import { useI18n } from "vue-i18n";
-import { getMyInterests, toggleInterest as apiToggleInterest } from "src/api/storeItem";
+import { getMyInterestsAssociado, toggleInterestAssociado as apiToggleInterest } from "src/api/storeItem";
 
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 
@@ -84,7 +84,7 @@ const removeInterest = async (item) => {
 
 onMounted(async () => {
   try {
-    items.value = await getMyInterests();
+    items.value = await getMyInterestsAssociado();
   } catch (e) {
     console.error(e);
   } finally {

+ 2 - 2
src/pages/associado/loja/AssociadoLojaPage.vue

@@ -178,7 +178,7 @@ import { ref, computed, onMounted, watch } from "vue";
 import { useQuasar } from "quasar";
 import { useI18n } from "vue-i18n";
 import { normalizeString } from "src/helpers/utils";
-import { getStoreItems, toggleInterest as apiToggleInterest } from "src/api/storeItem";
+import { getStoreItemsAssociado, toggleInterestAssociado as apiToggleInterest } from "src/api/storeItem";
 import { getCategories } from "src/api/category";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 
@@ -267,7 +267,7 @@ const toggleInterest = async (item) => {
 onMounted(async () => {
   try {
     const [itemsData, catsData] = await Promise.all([
-      getStoreItems(),
+      getStoreItemsAssociado(),
       getCategories("store"),
     ]);
     items.value = itemsData;

+ 3 - 3
src/pages/associado/notificacoes/NotificacoesAssociadoPage.vue

@@ -92,7 +92,7 @@
 <script setup>
 import { ref, computed, onMounted } from "vue";
 import { useI18n } from "vue-i18n";
-import { getMyNotifications, markNotificationAsRead } from "src/api/notification";
+import { getMyNotificationsAssociado, markNotificationAsReadAssociado } from "src/api/notification";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 
 const { t } = useI18n();
@@ -132,7 +132,7 @@ const formatDate = (dateStr) => {
 const onRead = async (item) => {
   if (item.read) return;
   try {
-    await markNotificationAsRead(item.id);
+    await markNotificationAsReadAssociado(item.id);
     item.read = true;
   } catch (e) {
     console.error(e);
@@ -141,7 +141,7 @@ const onRead = async (item) => {
 
 onMounted(async () => {
   try {
-    notifications.value = await getMyNotifications();
+    notifications.value = await getMyNotificationsAssociado();
   } catch (e) {
     console.error(e);
   } finally {

+ 187 - 0
src/pages/parceiros-convenios/AgendamentosParceiroPage.vue

@@ -0,0 +1,187 @@
+<template>
+  <div>
+    <DefaultHeaderPage />
+
+    <div class="q-pa-md">
+      <div v-if="loading" class="flex flex-center q-py-xl">
+        <q-spinner color="violet-normal" size="48px" />
+      </div>
+
+      <div v-else-if="appointments.length === 0" class="text-center text-grey q-py-xl">
+        {{ $t("http.errors.no_records_found") }}
+      </div>
+
+      <q-table
+        v-else
+        class="softpar-table q-pa-sm"
+        :rows="pagedAppointments"
+        :columns="columns"
+        row-key="id"
+        hide-pagination
+        :rows-per-page-options="[0]"
+      >
+        <template #body-cell-status="props">
+          <q-td :props="props">
+            <q-chip
+              outline
+              :color="statusColor(props.row.status)"
+              :label="$t(`agendamento.status.${props.row.status}`)"
+              size="sm"
+            />
+          </q-td>
+        </template>
+
+        <template #body-cell-acoes="props">
+          <q-td :props="props">
+            <q-btn
+              dense
+              round
+              flat
+              icon="mdi-check"
+              color="positive"
+              size="sm"
+              :disable="props.row.status !== 'pendente'"
+              :loading="actionId === props.row.id && actionType === 'approve'"
+              @click="onApprove(props.row)"
+            />
+            <q-btn
+              dense
+              round
+              flat
+              icon="mdi-close"
+              color="negative"
+              size="sm"
+              :disable="props.row.status !== 'pendente'"
+              :loading="actionId === props.row.id && actionType === 'reject'"
+              @click="onReject(props.row)"
+            />
+          </q-td>
+        </template>
+      </q-table>
+
+      <div v-if="totalPages > 1" class="flex flex-center q-mt-lg">
+        <q-pagination
+          v-model="currentPage"
+          :max="totalPages"
+          boundary-numbers
+          color="violet-normal"
+          active-color="violet-normal"
+          direction-links
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from "vue";
+import { useQuasar } from "quasar";
+import { useI18n } from "vue-i18n";
+import { getPartnerAppointments, approveAppointment, rejectAppointment } from "src/api/appointment";
+import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
+
+const $q = useQuasar();
+const { t } = useI18n();
+
+const loading = ref(true);
+const appointments = ref([]);
+const currentPage = ref(1);
+const PER_PAGE = 15;
+const actionId = ref(null);
+const actionType = ref(null);
+
+const columns = computed(() => [
+  { name: "pedido", label: t("agendamento.col.pedido"), field: "order_number", align: "left" },
+  { name: "associado", label: t("agendamento.associado"), field: (row) => row.user?.name || "—", align: "left" },
+  { name: "servico", label: t("agendamento.col.servico"), field: (row) => row.partner_agreement_service?.name || "—", align: "left" },
+  { name: "solicitacao", label: t("agendamento.col.solicitacao"), field: (row) => formatDate(row.created_at), align: "left" },
+  { name: "horario", label: t("common.terms.hour2"), field: (row) => formatDateTime(row.date, row.time), align: "left" },
+  { name: "acoes", label: t("common.terms.actions"), field: "id", align: "center" },
+  { name: "status", label: t("common.terms.status"), field: "status", align: "center" },
+]);
+
+const totalPages = computed(() => Math.max(1, Math.ceil(appointments.value.length / PER_PAGE)));
+
+const pagedAppointments = computed(() => {
+  const start = (currentPage.value - 1) * PER_PAGE;
+  return appointments.value.slice(start, start + PER_PAGE);
+});
+
+const statusColor = (status) => {
+  const map = { pendente: "warning", confirmado: "positive", cancelado: "negative", recusado: "negative", concluido: "grey" };
+  return map[status] ?? "grey";
+};
+
+const formatDate = (isoStr) => {
+  if (!isoStr) return "—";
+  const d = new Date(isoStr);
+  if (isNaN(d)) return "—";
+  return d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric" });
+};
+
+const formatDateTime = (date, time) => {
+  if (!date) return "—";
+  const d = new Date(date + "T00:00:00");
+  if (isNaN(d)) return "—";
+  const dateStr = d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric" });
+  return time ? `${dateStr} ${time}` : dateStr;
+};
+
+const onApprove = (row) => {
+  $q.dialog({
+    title: t("common.ui.messages.confirm_action"),
+    message: t("agendamento.confirm_approve"),
+    cancel: true,
+    persistent: true,
+  }).onOk(async () => {
+    actionId.value = row.id;
+    actionType.value = "approve";
+    try {
+      await approveAppointment(row.id);
+      row.status = "confirmado";
+      $q.notify({ type: "positive", message: t("http.success") });
+    } catch {
+      $q.notify({ type: "negative", message: t("http.errors.failed") });
+    } finally {
+      actionId.value = null;
+      actionType.value = null;
+    }
+  });
+};
+
+const onReject = (row) => {
+  $q.dialog({
+    title: t("common.ui.messages.confirm_action"),
+    message: t("agendamento.confirm_reject"),
+    cancel: true,
+    persistent: true,
+  }).onOk(async () => {
+    actionId.value = row.id;
+    actionType.value = "reject";
+    try {
+      await rejectAppointment(row.id);
+      row.status = "recusado";
+      $q.notify({ type: "positive", message: t("http.success") });
+    } catch {
+      $q.notify({ type: "negative", message: t("http.errors.failed") });
+    } finally {
+      actionId.value = null;
+      actionType.value = null;
+    }
+  });
+};
+
+onMounted(async () => {
+  try {
+    appointments.value = await getPartnerAppointments();
+  } catch (e) {
+    console.error(e);
+  } finally {
+    loading.value = false;
+  }
+});
+</script>
+
+<style lang="scss">
+@import "src/css/table.scss";
+</style>

+ 202 - 0
src/pages/parceiros-convenios/NotificacoesParceiroPage.vue

@@ -0,0 +1,202 @@
+<template>
+  <div>
+    <DefaultHeaderPage />
+
+    <div class="q-pt-sm">
+
+      <div v-if="loading" class="flex flex-center q-pa-xl">
+        <q-spinner color="violet-normal" size="50px" />
+      </div>
+
+      <div v-else-if="pagedItems.length === 0" class="flex flex-center q-pa-xl text-grey-6">
+        {{ $t('notification.empty') }}
+      </div>
+
+      <div v-else>
+        <div class="row q-px-md q-col-gutter-md">
+          <div
+            v-for="item in pagedItems"
+            :key="item.id"
+            class="col-xl-4 col-lg-4 col-md-4 col-sm-6 col-12"
+          >
+            <q-card
+              flat
+              bordered
+              class="notif-card"
+              :class="{ 'notif-card--unread': !item.read }"
+              @click="onRead(item)"
+            >
+              <div class="notif-card__image">
+                <img
+                  v-if="imageUrl(item)"
+                  :src="imageUrl(item)"
+                  alt=""
+                  class="notif-card__img"
+                />
+                <div v-else class="notif-card__placeholder flex flex-center">
+                  <q-icon
+                    name="mdi-bell-outline"
+                    size="40px"
+                    :color="item.read ? 'grey-4' : 'violet-normal'"
+                  />
+                </div>
+              </div>
+
+              <q-card-section class="q-pt-sm q-pb-xs">
+                <div class="row items-start justify-between no-wrap q-mb-xs">
+                  <div
+                    class="notif-card__title text-weight-bold ellipsis"
+                    :class="item.read ? 'text-grey-7' : 'text-violet-normal'"
+                  >
+                    {{ item.notification?.title }}
+                  </div>
+                  <q-badge
+                    v-if="!item.read"
+                    color="violet-normal"
+                    rounded
+                    class="q-ml-xs"
+                    style="flex-shrink: 0"
+                  />
+                </div>
+                <div class="notif-card__message text-caption text-grey-7">
+                  {{ item.notification?.message }}
+                </div>
+              </q-card-section>
+
+              <q-card-actions class="q-pt-xs q-pb-sm q-px-md">
+                <div class="text-caption text-grey-6">
+                  {{ formatDate(item.created_at) }}
+                </div>
+              </q-card-actions>
+            </q-card>
+          </div>
+        </div>
+
+        <div v-if="totalPages > 1" class="flex flex-center q-mt-lg">
+          <q-pagination
+            v-model="currentPage"
+            :max="totalPages"
+            :max-pages="6"
+            boundary-numbers
+            color="violet-normal"
+            active-color="violet-normal"
+            direction-links
+          />
+        </div>
+      </div>
+
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from "vue";
+import { useI18n } from "vue-i18n";
+import { getMyNotificationsParceiro, markNotificationAsReadParceiro } from "src/api/notification";
+import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
+
+const { t } = useI18n();
+
+const loading = ref(true);
+const notifications = ref([]);
+const currentPage = ref(1);
+const PER_PAGE = 12;
+
+const totalPages = computed(() => Math.max(1, Math.ceil(notifications.value.length / PER_PAGE)));
+
+const pagedItems = computed(() => {
+  const start = (currentPage.value - 1) * PER_PAGE;
+  return notifications.value.slice(start, start + PER_PAGE);
+});
+
+const imageUrl = (item) => {
+  const media = item.notification?.media?.[0]?.url;
+  if (media) return media;
+  const direct = item.notification?.image_url;
+  if (!direct) return null;
+  return direct.startsWith("http") ? direct : (process.env.API_URL + direct);
+};
+
+const formatDate = (dateStr) => {
+  if (!dateStr) return "";
+  const date = new Date(dateStr);
+  const today = new Date();
+  const isToday =
+    date.getDate() === today.getDate() &&
+    date.getMonth() === today.getMonth() &&
+    date.getFullYear() === today.getFullYear();
+  if (isToday) return t("common.terms.today");
+  return date.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric" });
+};
+
+const onRead = async (item) => {
+  if (item.read) return;
+  try {
+    await markNotificationAsReadParceiro(item.id);
+    item.read = true;
+  } catch (e) {
+    console.error(e);
+  }
+};
+
+onMounted(async () => {
+  try {
+    notifications.value = await getMyNotificationsParceiro();
+  } catch (e) {
+    console.error(e);
+  } finally {
+    loading.value = false;
+  }
+});
+</script>
+
+<style scoped lang="scss">
+.notif-card {
+  border-radius: 8px;
+  overflow: hidden;
+  cursor: pointer;
+  transition: box-shadow 0.2s, transform 0.15s;
+
+  &:hover {
+    box-shadow: 0 4px 16px rgba(102, 29, 117, 0.15);
+    transform: translateY(-2px);
+  }
+
+  &--unread {
+    border-top: 3px solid #7b2d97;
+  }
+
+  &__image {
+    width: 100%;
+    height: 120px;
+    overflow: hidden;
+  }
+
+  &__img {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+    display: block;
+  }
+
+  &__placeholder {
+    width: 100%;
+    height: 100%;
+    background: #f0e8f1;
+  }
+
+  &__title {
+    font-size: 14px;
+    line-height: 1.3;
+    min-width: 0;
+  }
+
+  &__message {
+    display: -webkit-box;
+    -webkit-line-clamp: 2;
+    -webkit-box-orient: vertical;
+    overflow: hidden;
+    line-height: 1.4;
+  }
+}
+</style>

+ 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(() => {

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

@@ -0,0 +1,55 @@
+export default [
+  {
+    path: "/parceiro/agendamentos",
+    name: "AgendamentosPage",
+    component: () => import("pages/parceiros-convenios/AgendamentosParceiroPage.vue"),
+    meta: {
+      title: { value: "ui.navigation.appointments", translate: true },
+      requireAuth: true,
+      requiredPermission: "parceiro.agendamento",
+      breadcrumbs: [
+        { name: "AgendamentosPage", title: "ui.navigation.appointments", translate: true },
+      ],
+    },
+  },
+  {
+    path: "/parceiro/notificacoes",
+    name: "NotificacoesPage",
+    component: () => import("pages/parceiros-convenios/NotificacoesParceiroPage.vue"),
+    meta: {
+      title: { value: "ui.navigation.notifications", translate: true },
+      requireAuth: true,
+      requiredPermission: "parceiro.notificacao",
+      breadcrumbs: [
+        { name: "NotificacoesPage", title: "ui.navigation.notifications", translate: true },
+      ],
+    },
+  },
+  {
+    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 },
+      ],
+    },
+  },
+];

+ 1 - 1
src/stores/navigation.js

@@ -142,7 +142,7 @@ export const navigationStore = defineStore("navigation", () => {
       name: "NotificacoesAssociadoPage",
       icon: "mdi-bell-outline",
       permission: false,
-      permissionScope: "notificacao",
+      permissionScope: "associado.notificacao",
       allowedTypes: ["associado"],
     },