Browse Source

feat: add tab de financeiro + ajustes em outras tabs

Gustavo Mantovani 1 tháng trước cách đây
mục cha
commit
55a1251013

+ 6 - 4
src/api/franchisee_contract.js

@@ -1,9 +1,11 @@
 import api from "src/api";
 
-export const getFranchiseeContractsByUnit = async (unitId) => {
-  const { data } = await api.get("/franchisee-contract", {
-    params: { unit_id: unitId },
-  });
+export const getFranchiseeContractsByUnit = async () => {
+  const { data } = await api.get("/franchisee-contract/me");
+  return data.payload;
+};
 
+export const getFranchiseeContractTaxHistory = async (id) => {
+  const { data } = await api.get(`/franchisee-contract/me/${id}/tax-history`);
   return data.payload;
 };

+ 6 - 0
src/api/inhabitant_classification.js

@@ -0,0 +1,6 @@
+import api from "src/api";
+
+export const getInhabitantClassificationsForSelect = async () => {
+  const { data } = await api.get("/inhabitant-classification/all/select");
+  return data.payload;
+};

+ 8 - 1
src/api/unit_financial.js

@@ -1,6 +1,13 @@
 import api from "src/api";
 
 export const getFinancialByUnit = async (unitId) => {
-  const { data } = await api.get("/unit-financial", { params: { unit_id: unitId } });
+  const { data } = await api.get("/unit-financial", {
+    params: { unit_id: unitId },
+  });
+  return data.payload;
+};
+
+export const getFinancialMe = async () => {
+  const { data } = await api.get("/unit-financial/me");
   return data.payload;
 };

+ 2 - 2
src/api/unit_history.js

@@ -1,6 +1,6 @@
 import api from "src/api";
 
-export const getHistoriesByUnit = async (unitId) => {
-  const { data } = await api.get("/unit-history", { params: { unit_id: unitId } });
+export const getHistoriesByUnit = async () => {
+  const { data } = await api.get("/unit-history/me");
   return data.payload;
 };

+ 4 - 8
src/api/unit_media.js

@@ -1,18 +1,14 @@
 import api from "src/api";
 
-export const getMediasByUnit = async (unitId) => {
-  const { data } = await api.get("/unit-media", { params: { unit_id: unitId } });
+export const getMediasByUnit = async () => {
+  const { data } = await api.get("/unit-media/me");
   return data.payload;
 };
 
 export const createMedia = async (formData) => {
-  const { data } = await api.post("/unit-media", formData, {
+  const { data } = await api.post("/unit-media/me", formData, {
     headers: { "Content-Type": "multipart/form-data" },
   });
-  return data.payload;
-};
 
-export const deleteMedia = async (id) => {
-  const { data } = await api.delete(`/unit-media/${id}`);
-  return data;
+  return data.payload;
 };

+ 7 - 6
src/api/unit_partner.js

@@ -1,26 +1,27 @@
 import api from "src/api";
 
-export const getPartnersByUnit = async (unitId) => {
-  const { data } = await api.get("/unit-partner", { params: { unit_id: unitId } });
+export const getPartnersByUnit = async () => {
+  const { data } = await api.get("/unit-partner/me");
   return data.payload;
 };
 
 export const createPartner = async (formData) => {
-  const { data } = await api.post("/unit-partner", formData, {
+  const { data } = await api.post("/unit-partner/me", formData, {
     headers: { "Content-Type": "multipart/form-data" },
   });
+
   return data.payload;
 };
 
 export const updatePartner = async (id, formData) => {
-  formData.append("_method", "PUT");
-  const { data } = await api.post(`/unit-partner/${id}`, formData, {
+  const { data } = await api.put(`/unit-partner/me/${id}`, formData, {
     headers: { "Content-Type": "multipart/form-data" },
   });
+
   return data.payload;
 };
 
 export const deletePartner = async (id) => {
-  const { data } = await api.delete(`/unit-partner/${id}`);
+  const { data } = await api.delete(`/unit-partner/me/${id}`);
   return data;
 };

+ 8 - 8
src/components/defaults/DefaultCepInput.vue

@@ -3,7 +3,7 @@
     v-model="model"
     v-bind="$attrs"
     debounce="500"
-    :class="disable ?? 'no-pointer-events'"
+    :class="disable ? 'no-pointer-events' : ''"
     :disable="disable"
     :label
     :mask="masks.Brasil.cep"
@@ -11,7 +11,7 @@
   />
 </template>
 <script setup>
-import { watch, ref, nextTick } from "vue";
+import { nextTick, ref, watch } from "vue";
 import { useQuasar } from "quasar";
 import masks from "src/helpers/masks.js";
 
@@ -23,15 +23,15 @@ const model = defineModel({
   type: [String, Number],
 });
 
-const { label, disable } = defineProps({
+const { disable, label } = defineProps({
+  disable: {
+    type: Boolean,
+    default: false,
+  },
   label: {
     type: String,
     default: "CEP",
   },
-  disable: {
-    type: Boolean,
-    default: undefined,
-  },
 });
 
 const emit = defineEmits([
@@ -45,7 +45,7 @@ const emit = defineEmits([
   "cepData",
 ]);
 
-const loading = ref(true);
+const loading = ref(false);
 
 watch(
   () => model.value,

+ 6 - 0
src/components/defaults/DefaultSelect.vue

@@ -112,3 +112,9 @@ defineExpose({
   selectRef,
 });
 </script>
+
+<style scoped>
+:deep(.q-field--outlined.q-field--rounded .q-field__control) {
+  border-radius: 8px;
+}
+</style>

+ 41 - 51
src/components/layout/DefaultHeaderPage.vue

@@ -1,9 +1,8 @@
 <template>
-  <div>
+  <div class="default-header-page">
     <q-breadcrumbs
-      v-if="displayBreadcrumbs != null"
-      class="q-mb-xs text-secondary flex items-center"
-      :class="$q.screen.lt.sm ? '' : 'q-pl-lg'"
+      v-if="displayBreadcrumbs.length"
+      class="q-mb-xs text-secondary"
     >
       <q-breadcrumbs-el
         v-for="crumb in displayBreadcrumbs"
@@ -12,20 +11,13 @@
         :to="crumb.name ? { name: crumb.name, params: crumb.params } : null"
       />
     </q-breadcrumbs>
-    <div
-      v-else
-      style="max-width: 180px"
-      :class="$q.screen.lt.sm ? '' : 'q-pl-lg'"
-    >
-      <q-skeleton type="text" />
-    </div>
 
     <div class="flex items-center justify-between">
-      <div
-        class="flex items-center q-pl-xs"
-        :class="$q.screen.lt.sm ? '' : 'q-pt-md'"
-      >
-        <span v-if="displayTitle" class="text-h6 text-primary text-weight-regular">
+      <div class="flex items-center" :class="$q.screen.lt.sm ? '' : 'q-pt-md'">
+        <span
+          v-if="displayTitle"
+          class="text-h6 text-primary text-weight-regular"
+        >
           {{ displayTitle }}
         </span>
         <div v-else style="width: 280px">
@@ -83,19 +75,19 @@ import { useRoute } from "vue-router";
 import { useI18n } from "vue-i18n";
 import { userStore } from "src/stores/user";
 
-const { title, breadcrumbs } = defineProps({
-  title: {
-    type: [String, Object],
-    default: null,
-  },
+const { breadcrumbs, showFilterIcon, title } = defineProps({
   breadcrumbs: {
-    type: Object,
+    type: Array,
     default: null,
   },
   showFilterIcon: {
     type: Boolean,
     default: false,
   },
+  title: {
+    type: [String, Object],
+    default: null,
+  },
 });
 
 const route = useRoute();
@@ -131,34 +123,32 @@ const displayTitle = computed(() => {
 });
 
 const displayBreadcrumbs = computed(() => {
-  if (!breadcrumbs && breadcrumbs?.length <= 0) {
-    return null;
-  } else if (breadcrumbs && breadcrumbs?.length > 0) {
-    return (breadcrumbs || []).map((b) => {
-      if (b.translate) {
-        return t(b.title);
-      } else {
-        return b.title;
-      }
-    });
-  }
-  if (!route.meta?.breadcrumbs && route.meta?.breadcrumbs?.length <= 0) {
-    return null;
-  } else if (route.meta?.breadcrumbs && route.meta?.breadcrumbs?.length > 0) {
-    return (route.meta?.breadcrumbs || []).map((b) => {
-      if (b.translate) {
-        return {
-          ...b,
-          title: t(b.title),
-        };
-      } else {
-        return {
-          ...b,
-          title: b.title,
-        };
-      }
-    });
-  }
-  return null;
+  const items = breadcrumbs?.length ? breadcrumbs : route.meta?.breadcrumbs;
+
+  return (items || []).map((b) => ({
+    ...b,
+    title: b.translate ? t(b.title) : b.title,
+  }));
 });
 </script>
+
+<style scoped>
+.default-header-page {
+  padding-left: 4px;
+}
+
+.default-header-page :deep(.q-breadcrumbs) {
+  display: inline-flex;
+  max-width: 100%;
+}
+
+.default-header-page :deep(.q-breadcrumbs__el),
+.default-header-page :deep(.q-breadcrumbs__separator) {
+  flex: 0 0 auto;
+  width: auto;
+}
+
+.default-header-page :deep(.q-breadcrumbs__el) {
+  max-width: max-content;
+}
+</style>

+ 1 - 1
src/components/layout/LeftMenuLayout.vue

@@ -31,7 +31,7 @@
         <div
           v-if="!$q.screen.lt.md"
           class="toggle-button-wrapper absolute"
-          style="top: 10px; right: -38px; z-index: 1"
+          style="top: 500px; right: 8px; z-index: 1"
         >
           <div @click="miniState = !miniState">
             <q-icon

+ 15 - 4
src/components/shared/ChangeImageDialog.vue

@@ -1,7 +1,7 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
     <q-card class="q-dialog-plugin overflow-hidden" style="min-width: 400px">
-      <DefaultDialogHeader :title="() => 'Trocar Imagem'" @close="onDialogCancel" />
+      <DefaultDialogHeader title="Trocar Imagem" @close="onDialogCancel" />
 
       <q-card-section class="q-pt-none q-pb-sm">
         <div class="text-caption text-grey-6 q-mb-xs">Personalizar</div>
@@ -27,8 +27,18 @@
       </q-card-section>
 
       <q-card-actions align="right">
-        <q-btn outline color="primary" label="Cancelar" @click="onDialogCancel" />
-        <q-btn color="primary" label="Salvar" :disable="!selectedFile" @click="onSave" />
+        <q-btn
+          outline
+          color="primary"
+          label="Cancelar"
+          @click="onDialogCancel"
+        />
+        <q-btn
+          color="primary"
+          label="Salvar"
+          :disable="!selectedFile"
+          @click="onSave"
+        />
       </q-card-actions>
     </q-card>
   </q-dialog>
@@ -41,7 +51,8 @@ import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue
 
 defineEmits([...useDialogPluginComponent.emits]);
 
-const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
 
 const selectedFile = ref(null);
 const previewUrl = ref(null);

+ 9 - 1
src/components/shared/CustomTabComponent.vue

@@ -1,5 +1,8 @@
 <template>
-  <div class="row no-wrap bg-secondary-3" style="border-radius: 8px; gap: 4px">
+  <div
+    class="custom-tab-component row no-wrap bg-secondary-3"
+    style="border-radius: 8px; gap: 4px"
+  >
     <div
       v-for="tab in tabs"
       :key="tab.name"
@@ -31,6 +34,11 @@ const emit = defineEmits(["update:activeTab"]);
 </script>
 
 <style scoped>
+.custom-tab-component {
+  position: relative;
+  z-index: 1;
+}
+
 .tab-item {
   transition:
     background-color 0.2s,

+ 31 - 20
src/pages/unit/UnitActionPage.vue

@@ -2,64 +2,75 @@
   <div>
     <DefaultHeaderPage title="Dados da Unidade" />
 
-    <CustomTabComponent v-model:active-tab="activeTab" :tabs="allTabs" />
+    <CustomTabComponent
+      v-model:active-tab="activeTab"
+      :tabs="allTabs"
+      class="q-mb-md"
+    />
 
     <UnitDataTab
       v-show="activeTab === 'unit_data'"
       v-model:form="form"
-      :unit-id="unitId"
       :get-form-as-form-data="getFormAsFormData"
       :set-update-form-as-original="setUpdateFormAsOriginal"
+      :unit-id="unitId"
     />
 
     <template v-if="unitId">
       <PartnersTab v-show="activeTab === 'partners'" :unit-id="unitId" />
+
       <ContractsTab v-show="activeTab === 'contracts'" :unit-id="unitId" />
+
       <FinancialTab v-show="activeTab === 'financial'" :unit-id="unitId" />
+
       <HistoryTab v-show="activeTab === 'history'" :unit-id="unitId" />
+
       <MediasTab v-show="activeTab === 'medias'" :unit-id="unitId" />
     </template>
   </div>
 </template>
 
 <script setup>
+import { computed, ref } from "vue";
+import { useFormUpdateTracker } from "src/composables/useFormUpdateTracker";
+import { userStore } from "src/stores/user";
+
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import CustomTabComponent from "src/components/shared/CustomTabComponent.vue";
-import UnitDataTab from "src/pages/unit/tabs/UnitDataTab.vue";
-import PartnersTab from "src/pages/unit/tabs/PartnersTab.vue";
+
 import ContractsTab from "src/pages/unit/tabs/ContractsTab.vue";
 import FinancialTab from "src/pages/unit/tabs/FinancialTab.vue";
 import HistoryTab from "src/pages/unit/tabs/HistoryTab.vue";
 import MediasTab from "src/pages/unit/tabs/MediasTab.vue";
-import { ref, computed, onMounted } from "vue";
-import { useFormUpdateTracker } from "src/composables/useFormUpdateTracker";
-import { userStore } from "src/stores/user";
+import PartnersTab from "src/pages/unit/tabs/PartnersTab.vue";
+import UnitDataTab from "src/pages/unit/tabs/UnitDataTab.vue";
 
 const store = userStore();
-const unitId = computed(() => store.user?.unit_id ?? null);
 
 const { form, getFormAsFormData, setUpdateFormAsOriginal } =
   useFormUpdateTracker({
-    fantasy_name: null,
-    social_reason: null,
-    cnpj: null,
-    state_registration: null,
-    name_responsible: null,
-    street: null,
     address_number: null,
-    postal_code: null,
-    neighborhood: null,
-    complement: null,
+    cell_number: null,
     city_id: null,
-    state_id: null,
+    cnpj: null,
+    complement: null,
     email: null,
-    secondary_email: null,
+    fantasy_name: null,
+    name_responsible: null,
+    neighborhood: null,
     phone_number: null,
-    cell_number: null,
+    postal_code: null,
+    secondary_email: null,
+    social_reason: null,
+    state_id: null,
+    state_registration: null,
+    street: null,
   });
 
 const activeTab = ref("unit_data");
 
+const unitId = computed(() => store.user?.unit_id ?? null);
+
 const allTabs = [
   { name: "unit_data", label: "Dados da Unidade" },
   { name: "partners", label: "Sócios" },

+ 41 - 18
src/pages/unit/components/AddEditHistoryDialog.vue

@@ -1,39 +1,56 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <q-card class="q-dialog-plugin overflow-hidden" style="width: 560px; max-width: 95vw">
-      <DefaultDialogHeader
-        :title="() => (history ? 'Editar Histórico' : 'Novo Histórico')"
-        @close="onDialogCancel"
-      />
+    <q-card
+      class="q-dialog-plugin overflow-hidden"
+      style="width: 560px; max-width: 95vw"
+    >
+      <DefaultDialogHeader :title="dialogTitle" @close="onDialogCancel" />
 
       <q-form ref="formRef" @submit="onOKClick">
         <q-card-section class="q-pt-none">
           <div class="column q-gutter-sm">
             <DefaultInput
               v-model="form.title"
+              v-model:error="validationErrors.title"
               label="Título"
               outlined
               :rules="[inputRules.required]"
             />
+
             <q-input
               v-model="form.content"
+              autogrow
               label="Conteúdo"
               outlined
-              type="textarea"
               rows="5"
-              autogrow
+              type="textarea"
+              :error="!!validationErrors.content"
+              :error-message="validationErrors.content"
+              @update:model-value="validationErrors.content = null"
             />
+
             <q-toggle
               v-model="form.visible_to_franchisee"
-              label="Visível ao franqueado"
               color="positive"
+              label="Visível ao franqueado"
             />
           </div>
         </q-card-section>
 
         <q-card-actions align="right" class="q-pa-md">
-          <q-btn outline color="primary" label="Cancelar" @click="onDialogCancel" />
-          <q-btn color="primary-2" :label="history ? 'Salvar' : 'Adicionar'" type="submit" :loading="loading" />
+          <q-btn
+            color="primary"
+            label="Cancelar"
+            outline
+            @click="onDialogCancel"
+          />
+
+          <q-btn
+            color="primary-2"
+            type="submit"
+            :label="history ? 'Salvar' : 'Adicionar'"
+            :loading="loading"
+          />
         </q-card-actions>
       </q-form>
     </q-card>
@@ -41,11 +58,12 @@
 </template>
 
 <script setup>
+import { createHistory, updateHistory } from "src/api/unit_history";
 import { ref } from "vue";
 import { useDialogPluginComponent } from "quasar";
 import { useInputRules } from "src/composables/useInputRules";
 import { useSubmitHandler } from "src/composables/useSubmitHandler";
-import { createHistory, updateHistory } from "src/api/unit_history";
+
 import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
 import DefaultInput from "src/components/defaults/DefaultInput.vue";
 
@@ -57,17 +75,20 @@ const { history, unitId } = defineProps({
 });
 
 const { inputRules } = useInputRules();
-const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
 
-const formRef = ref(null);
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
+
+const dialogTitle = history ? "Editar Histórico" : "Novo Histórico";
 
+const formRef = ref(null);
 const form = ref({
-  title: history?.title ?? "",
   content: history?.content ?? "",
+  title: history?.title ?? "",
   visible_to_franchisee: history?.visible_to_franchisee ?? false,
 });
 
-const { loading, execute } = useSubmitHandler({
+const { loading, validationErrors, execute } = useSubmitHandler({
   formRef,
   onSuccess: (result) => onDialogOK(result),
 });
@@ -75,13 +96,15 @@ const { loading, execute } = useSubmitHandler({
 async function onOKClick() {
   await execute(() => {
     const payload = {
-      unit_id: unitId,
-      title: form.value.title,
       content: form.value.content,
+      title: form.value.title,
+      unit_id: unitId,
       visible_to_franchisee: form.value.visible_to_franchisee,
     };
 
-    return history ? updateHistory(history.id, payload) : createHistory(payload);
+    return history
+      ? updateHistory(history.id, payload)
+      : createHistory(payload);
   });
 }
 </script>

+ 112 - 81
src/pages/unit/components/AddEditPartnerDialog.vue

@@ -4,10 +4,7 @@
       class="q-dialog-plugin overflow-hidden"
       style="width: 860px; max-width: 95vw"
     >
-      <DefaultDialogHeader
-        :title="() => (partner ? 'Editar Sócio' : 'Adicionar Sócio')"
-        @close="onDialogCancel"
-      />
+      <DefaultDialogHeader :title="dialogTitle" @close="onDialogCancel" />
 
       <q-form ref="formRef" @submit="onOKClick">
         <q-card-section class="q-pt-none">
@@ -21,90 +18,107 @@
           <div class="row q-col-gutter-sm">
             <DefaultInput
               v-model="form.name"
-              label="Nome completo"
+              v-model:error="validationErrors.name"
               class="col-8"
+              label="Nome completo"
               outlined
               :rules="[inputRules.required]"
             />
 
             <DefaultInput
               v-model="form.role"
-              label="Função"
+              v-model:error="validationErrors.role"
               class="col-4"
+              label="Função"
               outlined
             />
 
             <DefaultInput
               v-model="form.social_name"
-              label="Nome social"
+              v-model:error="validationErrors.social_name"
               class="col-6"
+              label="Nome social"
               outlined
             />
 
             <DefaultInput
               v-model="form.cpf"
-              label="CPF"
+              v-model:error="validationErrors.cpf"
               class="col-3"
+              label="CPF"
               outlined
               :mask="masks.Brasil.cpf"
               :rules="[inputRules.required]"
             />
 
-            <DefaultInput v-model="form.rg" label="RG" class="col-3" outlined />
+            <DefaultInput
+              v-model="form.rg"
+              v-model:error="validationErrors.rg"
+              class="col-3"
+              label="RG"
+              outlined
+            />
 
             <DefaultInput
               v-model="birthDateDisplay"
-              label="Data de Nascimento"
+              v-model:error="validationErrors.birth_date"
               class="col-3"
+              label="Data de Nascimento"
+              placeholder="DD/MM/AAAA"
               outlined
               :mask="masks.Brasil.date"
-              placeholder="DD/MM/AAAA"
             />
 
             <DefaultInput
               v-model="form.participation"
-              label="Participação (%)"
+              v-model:error="validationErrors.participation"
               class="col-3"
+              label="Participação (%)"
+              max="100"
+              min="0"
               outlined
               type="number"
-              min="0"
-              max="100"
             />
 
             <DefaultInput
               v-model="form.email"
-              label="E-mail"
+              v-model:error="validationErrors.email"
               class="col-6"
+              label="E-mail"
               outlined
               :rules="[inputRules.email]"
             />
 
             <DefaultInput
               v-model="form.secondary_email"
-              label="E-mail Secundário"
+              v-model:error="validationErrors.secondary_email"
               class="col-6"
+              label="E-mail Secundário"
               outlined
               :rules="[inputRules.email]"
             />
 
             <DefaultInput
               v-model="form.phone_number"
-              label="Telefone"
+              v-model:error="validationErrors.phone_number"
               class="col-6"
+              label="Telefone"
               outlined
               :mask="masks.Brasil.telefone"
             />
 
             <DefaultInput
               v-model="form.cell_number"
-              label="Celular"
+              v-model:error="validationErrors.cell_number"
               class="col-6"
+              label="Celular"
               outlined
               :mask="masks.Brasil.celular"
             />
 
             <DefaultCepInput
               v-model="form.postal_code"
+              v-model:error="validationErrors.postal_code"
               class="col-6"
               outlined
               @rua="form.street = $event"
@@ -115,46 +129,52 @@
 
             <DefaultInput
               v-model="form.street"
-              label="Endereço"
+              v-model:error="validationErrors.street"
               class="col-6"
+              label="Endereço"
               outlined
             />
 
             <DefaultInput
               v-model="form.address_number"
-              label="Número"
+              v-model:error="validationErrors.address_number"
               class="col-6"
+              label="Número"
               outlined
             />
 
             <DefaultInput
               v-model="form.neighborhood"
-              label="Bairro"
+              v-model:error="validationErrors.neighborhood"
               class="col-4"
+              label="Bairro"
               outlined
             />
 
             <StateSelect
               ref="stateSelectRef"
               v-model="selectedState"
-              label="Estado"
+              v-model:error="validationErrors.state_id"
               class="col-4"
+              label="Estado"
               outlined
             />
 
             <CitySelect
               ref="citySelectRef"
               v-model="selectedCity"
-              label="Cidade"
+              v-model:error="validationErrors.city_id"
               class="col-4"
+              label="Cidade"
               outlined
               :state="selectedState"
             />
 
             <DefaultInput
               v-model="form.complement"
-              label="Complemento"
+              v-model:error="validationErrors.complement"
               class="col-12"
+              label="Complemento"
               outlined
             />
           </div>
@@ -162,16 +182,18 @@
 
         <q-card-actions>
           <q-space />
+
           <q-btn
-            outline
             color="negative"
             label="Cancelar"
+            outline
             @click="onDialogCancel"
           />
+
           <q-btn
             color="primary-2"
-            :label="partner ? 'Salvar' : 'Adicionar'"
             type="submit"
+            :label="partner ? 'Salvar' : 'Adicionar'"
             :loading="loading"
           />
         </q-card-actions>
@@ -181,25 +203,31 @@
 </template>
 
 <script setup>
-import { ref, watch, onMounted } from "vue";
+import { createPartner, updatePartner } from "src/api/unit_partner";
+import { formatDateDMYtoYMD, formatDateYMDtoDMY } from "src/helpers/utils";
+import { onMounted, ref, watch } from "vue";
 import { useDialogPluginComponent } from "quasar";
-import { useInputRules } from "src/composables/useInputRules";
 import { useFormUpdateTracker } from "src/composables/useFormUpdateTracker";
+import { useInputRules } from "src/composables/useInputRules";
 import { useSubmitHandler } from "src/composables/useSubmitHandler";
-import { createPartner, updatePartner } from "src/api/unit_partner";
 import masks from "src/helpers/masks";
-import { formatDateDMYtoYMD, formatDateYMDtoDMY } from "src/helpers/utils";
 
 import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
 import DefaultInput from "src/components/defaults/DefaultInput.vue";
 import DefaultCepInput from "src/components/defaults/DefaultCepInput.vue";
+
 import AvatarImageComponent from "src/components/shared/AvatarImageComponent.vue";
+
 import StateSelect from "src/components/selects/StateSelect.vue";
 import CitySelect from "src/components/selects/CitySelect.vue";
 
 defineEmits([...useDialogPluginComponent.emits]);
 
-const { partner, unitId, offlineMode } = defineProps({
+const { offlineMode, partner, unitId } = defineProps({
+  offlineMode: {
+    type: Boolean,
+    default: false,
+  },
   partner: {
     type: Object,
     default: null,
@@ -208,67 +236,52 @@ const { partner, unitId, offlineMode } = defineProps({
     type: Number,
     default: null,
   },
-  offlineMode: {
-    type: Boolean,
-    default: false,
-  },
 });
 
 const { inputRules } = useInputRules();
+
 const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
   useDialogPluginComponent();
 
-const formRef = ref(null);
-const avatarRef = ref(null);
-const stateSelectRef = ref(null);
-const citySelectRef = ref(null);
-
-const selectedState = ref(null);
-const selectedCity = ref(null);
-const avatarChanged = ref(false);
-const avatarFile = ref(null);
-
-// Exibe DD/MM/YYYY para o usuário; form.birth_date armazena YYYY-MM-DD para o backend
-const birthDateDisplay = ref(
-  partner?.birth_date ? formatDateYMDtoDMY(partner.birth_date) : null,
-);
-
-watch(birthDateDisplay, (val) => {
-  try {
-    form.birth_date = val?.length === 10 ? formatDateDMYtoYMD(val) : null;
-  } catch {
-    form.birth_date = null;
-  }
-});
+const dialogTitle = partner ? "Editar Sócio" : "Adicionar Sócio";
 
 const { form, getFormAsFormData } = useFormUpdateTracker({
-  unit_id: unitId,
-  name: partner?.name ?? null,
-  social_name: partner?.social_name ?? null,
-  role: partner?.role ?? null,
-  cpf: partner?.cpf ?? null,
-  rg: partner?.rg ?? null,
+  address_number: partner?.address_number ?? null,
   birth_date: partner?.birth_date ?? null,
-  participation: partner?.participation ?? null,
+  cell_number: partner?.cell_number ?? null,
+  city_id: partner?.city_id ?? null,
+  complement: partner?.complement ?? null,
+  cpf: partner?.cpf ?? null,
   email: partner?.email ?? null,
-  secondary_email: partner?.secondary_email ?? null,
+  name: partner?.name ?? null,
+  neighborhood: partner?.neighborhood ?? null,
+  participation: partner?.participation ?? null,
   phone_number: partner?.phone_number ?? null,
-  cell_number: partner?.cell_number ?? null,
   postal_code: partner?.postal_code ?? null,
-  street: partner?.street ?? null,
-  address_number: partner?.address_number ?? null,
-  neighborhood: partner?.neighborhood ?? null,
-  complement: partner?.complement ?? null,
-  city_id: partner?.city_id ?? null,
+  rg: partner?.rg ?? null,
+  role: partner?.role ?? null,
+  secondary_email: partner?.secondary_email ?? null,
+  social_name: partner?.social_name ?? null,
   state_id: partner?.state_id ?? null,
+  street: partner?.street ?? null,
+  unit_id: unitId,
 });
 
-watch(selectedState, (state) => {
-  form.state_id = state?.value ?? null;
-});
+const avatarRef = ref(null);
+const avatarChanged = ref(false);
+const avatarFile = ref(null);
+const birthDateDisplay = ref(
+  partner?.birth_date ? formatDateYMDtoDMY(partner.birth_date) : null,
+);
+const stateSelectRef = ref(null);
+const citySelectRef = ref(null);
+const formRef = ref(null);
+const selectedState = ref(null);
+const selectedCity = ref(null);
 
-watch(selectedCity, (city) => {
-  form.city_id = city?.value ?? null;
+const { loading, validationErrors, execute } = useSubmitHandler({
+  formRef,
+  onSuccess: (result) => onDialogOK(result),
 });
 
 function onAvatarChange(file) {
@@ -276,24 +289,24 @@ function onAvatarChange(file) {
   avatarFile.value = file;
 }
 
-const { loading, execute } = useSubmitHandler({
-  formRef,
-  onSuccess: (result) => onDialogOK(result),
-});
-
 async function onOKClick() {
   if (offlineMode) {
     const partnerData = { ...form };
+
     if (avatarFile.value instanceof File) {
       if (partner?.avatar_url?.startsWith("blob:")) {
         URL.revokeObjectURL(partner.avatar_url);
       }
+
       partnerData.avatar = avatarFile.value;
+
       partnerData.avatar_url = URL.createObjectURL(avatarFile.value);
     } else if (partner?.avatar_url) {
       partnerData.avatar_url = partner.avatar_url;
     }
+
     onDialogOK(partnerData);
+
     return;
   }
 
@@ -312,15 +325,33 @@ async function onOKClick() {
   });
 }
 
+watch(birthDateDisplay, (val) => {
+  try {
+    form.birth_date = val?.length === 10 ? formatDateDMYtoYMD(val) : null;
+  } catch {
+    form.birth_date = null;
+  }
+});
+
+watch(selectedState, (state) => {
+  form.state_id = state?.value ?? null;
+});
+
+watch(selectedCity, (city) => {
+  form.city_id = city?.value ?? null;
+});
+
 onMounted(() => {
   if (!partner) return;
 
   if (partner.avatar_url) {
     avatarRef.value?.setImageUrl(partner.avatar_url);
   }
+
   if (partner.state_id) {
     stateSelectRef.value?.selectStateById(partner.state_id);
   }
+
   if (partner.city_id) {
     citySelectRef.value?.selectCityById(partner.city_id);
   }

+ 33 - 17
src/pages/unit/components/AddMediaDialog.vue

@@ -1,13 +1,17 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <q-card class="q-dialog-plugin overflow-hidden" style="width: 480px; max-width: 95vw">
-      <DefaultDialogHeader :title="() => 'Adicionar Mídia'" @close="onDialogCancel" />
+    <q-card
+      class="q-dialog-plugin overflow-hidden"
+      style="width: 480px; max-width: 95vw"
+    >
+      <DefaultDialogHeader title="Adicionar Mídia" @close="onDialogCancel" />
 
       <q-form ref="formRef" @submit="onOKClick">
         <q-card-section class="q-pt-none">
           <div class="column q-gutter-sm">
             <DefaultInput
               v-model="form.title"
+              v-model:error="validationErrors.title"
               label="Título"
               outlined
               :rules="[inputRules.required]"
@@ -15,27 +19,35 @@
 
             <q-file
               v-model="selectedFile"
+              accept="image/*,video/*,.pdf"
               label="Arquivo"
               outlined
-              accept="image/*,video/*,.pdf"
+              :error="!!validationErrors.file"
+              :error-message="validationErrors.file"
               :rules="[inputRules.required]"
+              @update:model-value="validationErrors.file = null"
             >
               <template #prepend>
                 <q-icon name="attach_file" />
               </template>
             </q-file>
-
-            <q-toggle
-              v-model="form.visible_to_franchisee"
-              label="Visível ao franqueado"
-              color="positive"
-            />
           </div>
         </q-card-section>
 
         <q-card-actions align="right" class="q-pa-md">
-          <q-btn outline color="primary" label="Cancelar" @click="onDialogCancel" />
-          <q-btn color="primary-2" label="Adicionar" type="submit" :loading="loading" />
+          <q-btn
+            color="primary"
+            label="Cancelar"
+            outline
+            @click="onDialogCancel"
+          />
+
+          <q-btn
+            color="primary-2"
+            label="Adicionar"
+            type="submit"
+            :loading="loading"
+          />
         </q-card-actions>
       </q-form>
     </q-card>
@@ -43,11 +55,12 @@
 </template>
 
 <script setup>
+import { createMedia } from "src/api/unit_media";
 import { ref } from "vue";
 import { useDialogPluginComponent } from "quasar";
 import { useInputRules } from "src/composables/useInputRules";
 import { useSubmitHandler } from "src/composables/useSubmitHandler";
-import { createMedia } from "src/api/unit_media";
+
 import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
 import DefaultInput from "src/components/defaults/DefaultInput.vue";
 
@@ -58,17 +71,18 @@ const { unitId } = defineProps({
 });
 
 const { inputRules } = useInputRules();
-const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
 
 const formRef = ref(null);
 const selectedFile = ref(null);
-
 const form = ref({
   title: "",
-  visible_to_franchisee: false,
+  visible_to_franchisee: true,
 });
 
-const { loading, execute } = useSubmitHandler({
+const { loading, validationErrors, execute } = useSubmitHandler({
   formRef,
   onSuccess: (result) => onDialogOK(result),
 });
@@ -76,10 +90,12 @@ const { loading, execute } = useSubmitHandler({
 async function onOKClick() {
   await execute(() => {
     const formData = new FormData();
+
     formData.append("unit_id", unitId);
     formData.append("title", form.value.title);
     formData.append("file", selectedFile.value);
-    formData.append("visible_to_franchisee", form.value.visible_to_franchisee ? 1 : 0);
+    formData.append("visible_to_franchisee", 1);
+
     return createMedia(formData);
   });
 }

+ 55 - 46
src/pages/unit/components/CreateContractDialog.vue

@@ -5,7 +5,7 @@
       style="width: 100%; max-width: 1100px"
     >
       <DefaultDialogHeader
-        :title="() => 'Criar novo contrato'"
+        title="Criar novo contrato"
         @close="onDialogCancel"
       />
 
@@ -15,38 +15,38 @@
         <div class="row q-col-gutter-x-sm">
           <DefaultInput
             :model-value="unitData.id"
-            label="ID"
-            color="secondary"
-            label-color="secondary"
             class="col-md-3 col-12"
+            color="secondary"
             disable
+            label="ID"
+            label-color="secondary"
           />
 
           <DefaultInput
             :model-value="unitData.franchisee_name"
-            label="Nome do Franqueado"
-            color="secondary"
-            label-color="secondary"
             class="col-md-3 col-12"
+            color="secondary"
             disable
+            label="Nome do Franqueado"
+            label-color="secondary"
           />
 
           <DefaultInput
             :model-value="unitData.franchisee_document"
-            label="CPF/CNH"
-            color="secondary"
-            label-color="secondary"
             class="col-md-3 col-12"
+            color="secondary"
             disable
+            label="CPF/CNH"
+            label-color="secondary"
           />
 
           <DefaultInput
             :model-value="unitData.franchisee_birthday"
-            label="Data de Nascimento"
-            color="secondary"
-            label-color="secondary"
             class="col-md-3 col-12"
+            color="secondary"
             disable
+            label="Data de Nascimento"
+            label-color="secondary"
           />
         </div>
       </q-card-section>
@@ -57,60 +57,60 @@
         <div class="row q-col-gutter-sm">
           <DefaultInputDatePicker
             v-model:untreated-date="contractForm.start_date"
-            label="Data de Início"
+            class="col-md-3 col-12"
             color="secondary"
+            label="Data de Início"
             label-color="secondary"
-            class="col-md-3 col-12"
           />
 
           <DefaultInputDatePicker
             v-model:untreated-date="contractForm.end_date"
-            label="Data de Fim"
+            class="col-md-3 col-12"
             color="secondary"
+            label="Data de Fim"
             label-color="secondary"
-            class="col-md-3 col-12"
           />
 
           <DefaultCurrencyInput
             v-model="contractForm.tbr_fixed_value"
-            label="TBR $"
+            class="col-md-3 col-12"
             color="secondary"
+            label="TBR $"
             label-color="secondary"
-            class="col-md-3 col-12"
           />
 
           <DefaultInput
             v-model="contractForm.invoice_due_date"
-            label="Dia de Vencimento do Boleto"
+            class="col-md-3 col-12"
             color="secondary"
+            label="Dia de Vencimento do Boleto"
             label-color="secondary"
             type="number"
-            class="col-md-3 col-12"
           >
           </DefaultInput>
 
           <DefaultSelect
             v-model="contractForm.inhabitant_classification_id"
-            label="Faixa de Habitante"
+            class="col-md-3 col-12"
             color="secondary"
-            label-color="secondary"
-            :options="inhabitantOptions"
             emit-value
-            map-options
-            use-input
             fill-input
             hide-selected
             input-debounce="0"
-            class="col-md-3 col-12"
+            label="Faixa de Habitante"
+            label-color="secondary"
+            map-options
+            use-input
+            :options="inhabitantOptions"
           />
 
           <DefaultInput
             v-model="contractForm.tax_base_royalts"
-            label="Taxa Base Royalties"
+            class="col-md-3 col-12"
             color="secondary"
+            label="Taxa Base Royalties"
             label-color="secondary"
             type="number"
-            class="col-md-3 col-12"
           >
             <template #append>
               <span class="text-secondary">%</span>
@@ -119,11 +119,11 @@
 
           <DefaultInput
             v-model="contractForm.tax_base_fnm"
-            label="Taxa Base FMN"
+            class="col-md-3 col-12"
             color="secondary"
+            label="Taxa Base FMN"
             label-color="secondary"
             type="number"
-            class="col-md-3 col-12"
           >
             <template #append>
               <span class="text-secondary">%</span>
@@ -132,11 +132,11 @@
 
           <DefaultInput
             v-model="contractForm.tax_base_maintenance"
-            label="Taxa Base Manutenção"
+            class="col-md-3 col-12"
             color="secondary"
+            label="Taxa Base Manutenção"
             label-color="secondary"
             type="number"
-            class="col-md-3 col-12"
           >
             <template #append>
               <span class="text-secondary">%</span>
@@ -147,9 +147,9 @@
 
       <q-card-actions align="right">
         <q-btn
-          outline
           color="primary"
           label="Cancelar"
+          outline
           @click="onDialogCancel"
         />
         <q-btn color="primary" label="Salvar" :loading="saving" @click="save" />
@@ -159,7 +159,11 @@
 </template>
 
 <script setup>
-import { ref, reactive, onMounted } from "vue";
+import { createFranchiseeContract } from "src/api/franchisee_contract";
+import { getInhabitantClassificationsForSelect } from "src/api/inhabitant_classification";
+import { getLatestTbr } from "src/api/tbr";
+import { getUnit } from "src/api/unit";
+import { onMounted, reactive, ref } from "vue";
 import { useDialogPluginComponent } from "quasar";
 
 import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
@@ -168,11 +172,6 @@ import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePick
 import DefaultCurrencyInput from "src/components/defaults/DefaultCurrencyInput.vue";
 import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
 
-import { getUnit } from "src/api/unit";
-import { getLatestTbr } from "src/api/tbr";
-import { getInhabitantClassificationsForSelect } from "src/api/inhabitant_classification";
-import { createFranchiseeContract } from "src/api/franchisee_contract";
-
 defineEmits([...useDialogPluginComponent.emits]);
 
 const props = defineProps({
@@ -186,24 +185,25 @@ const { dialogRef, onDialogHide, onDialogCancel, onDialogOK } =
   useDialogPluginComponent();
 
 const saving = ref(false);
+
 const inhabitantOptions = ref([]);
 
 const unitData = reactive({
   id: null,
-  franchisee_name: null,
-  franchisee_document: null,
   franchisee_birthday: null,
+  franchisee_document: null,
+  franchisee_name: null,
 });
 
 const contractForm = reactive({
-  start_date: null,
   end_date: null,
-  tbr_fixed_value: null,
-  invoice_due_date: null,
   inhabitant_classification_id: null,
-  tax_base_royalts: null,
+  invoice_due_date: null,
+  start_date: null,
   tax_base_fnm: null,
   tax_base_maintenance: null,
+  tax_base_royalts: null,
+  tbr_fixed_value: null,
 });
 
 async function loadData() {
@@ -214,9 +214,11 @@ async function loadData() {
   ]);
 
   unitData.id = unit.id;
+
   unitData.franchisee_name = unit.name_responsible;
 
   const firstPartner = unit.partners?.[0];
+
   if (firstPartner) {
     unitData.franchisee_document = firstPartner.cpf;
     unitData.franchisee_birthday = firstPartner.birth_date;
@@ -224,12 +226,15 @@ async function loadData() {
 
   if (latestTbr) {
     contractForm.tbr_fixed_value = parseFloat(latestTbr.tbr_value);
+
     contractForm.tax_base_royalts = parseFloat(
       (latestTbr.royalties_percentage * 100).toFixed(4),
     );
+
     contractForm.tax_base_fnm = parseFloat(
       (latestTbr.fnm_percentage * 100).toFixed(4),
     );
+
     contractForm.tax_base_maintenance = parseFloat(
       (latestTbr.maintenance_percentage * 100).toFixed(4),
     );
@@ -243,6 +248,7 @@ async function loadData() {
 
 async function save() {
   saving.value = true;
+
   try {
     await createFranchiseeContract({
       unit_id: props.unitId,
@@ -251,14 +257,17 @@ async function save() {
       tbr_fixed_value: contractForm.tbr_fixed_value,
       invoice_due_date: contractForm.invoice_due_date,
       inhabitant_classification_id: contractForm.inhabitant_classification_id,
+
       tbr_fixed_value_percentage:
         contractForm.tax_base_royalts != null
           ? contractForm.tax_base_royalts / 100
           : null,
+
       marketing_fund_percentage:
         contractForm.tax_base_fnm != null
           ? contractForm.tax_base_fnm / 100
           : null,
+
       maintance_tax_percentage:
         contractForm.tax_base_maintenance != null
           ? contractForm.tax_base_maintenance / 100

+ 27 - 30
src/pages/unit/components/EditContractDialog.vue

@@ -4,10 +4,7 @@
       class="q-dialog-plugin overflow-hidden"
       style="width: 100%; max-width: 1100px"
     >
-      <DefaultDialogHeader
-        :title="() => 'Editar Contrato'"
-        @close="onDialogCancel"
-      />
+      <DefaultDialogHeader title="Editar Contrato" @close="onDialogCancel" />
 
       <q-card-section>
         <div class="text-body2 q-mb-sm">Dados da Unidade</div>
@@ -15,38 +12,38 @@
         <div class="row q-col-gutter-x-sm">
           <DefaultInput
             :model-value="unitData.id"
-            label="ID"
-            color="secondary"
-            label-color="secondary"
             class="col-md-3 col-12"
+            color="secondary"
             disable
+            label="ID"
+            label-color="secondary"
           />
 
           <DefaultInput
             :model-value="unitData.franchisee_name"
-            label="Nome do Franqueado"
-            color="secondary"
-            label-color="secondary"
             class="col-md-3 col-12"
+            color="secondary"
             disable
+            label="Nome do Franqueado"
+            label-color="secondary"
           />
 
           <DefaultInput
             :model-value="unitData.franchisee_document"
-            label="CPF / CNH"
-            color="secondary"
-            label-color="secondary"
             class="col-md-3 col-12"
+            color="secondary"
             disable
+            label="CPF / CNH"
+            label-color="secondary"
           />
 
           <DefaultInput
             :model-value="unitData.franchisee_birthday"
-            label="Data de Nascimento"
-            color="secondary"
-            label-color="secondary"
             class="col-md-3 col-12"
+            color="secondary"
             disable
+            label="Data de Nascimento"
+            label-color="secondary"
           />
         </div>
       </q-card-section>
@@ -55,20 +52,20 @@
         <div class="row q-col-gutter-sm">
           <DefaultInputDatePicker
             v-model:untreated-date="contractForm.start_date"
-            label="Data de Início"
-            color="secondary"
-            label-color="secondary"
             class="col-md-3 col-12"
+            color="secondary"
             disable
+            label="Data de Início"
+            label-color="secondary"
           />
 
           <DefaultInputDatePicker
             v-model:untreated-date="contractForm.end_date"
-            label="Data de Fim"
-            color="secondary"
-            label-color="secondary"
             class="col-md-3 col-12"
+            color="secondary"
             disable
+            label="Data de Fim"
+            label-color="secondary"
           />
 
           <DefaultInput
@@ -211,23 +208,19 @@ import { getFranchiseeContractTaxHistory } from "src/api/franchisee_contract";
 defineEmits([...useDialogPluginComponent.emits]);
 
 const props = defineProps({
-  unitId: {
-    type: Number,
-    required: true,
-  },
   contract: {
     type: Object,
     required: true,
   },
+  unitId: {
+    type: Number,
+    required: true,
+  },
 });
 
 const { dialogRef, onDialogHide, onDialogCancel, onDialogOK } =
   useDialogPluginComponent();
 
-function openTaxesDialog() {
-  onDialogOK({ openTaxes: true, contract: props.contract });
-}
-
 const loadingHistory = ref(false);
 const inhabitantOptions = ref([]);
 const tbrHistory = ref([]);
@@ -302,6 +295,10 @@ const tbrColumns = [
   },
 ];
 
+function openTaxesDialog() {
+  onDialogOK({ openTaxes: true, contract: props.contract });
+}
+
 async function loadData() {
   const [unit, classifications] = await Promise.all([
     getUnit(props.unitId),

+ 11 - 9
src/pages/unit/components/EditContractTaxesDialog.vue

@@ -4,10 +4,7 @@
       class="q-dialog-plugin overflow-hidden"
       style="width: 100%; max-width: 1100px"
     >
-      <DefaultDialogHeader
-        :title="() => 'Editar Taxas'"
-        @close="onDialogCancel"
-      />
+      <DefaultDialogHeader title="Editar Taxas" @close="onDialogCancel" />
 
       <q-card-section>
         <div class="text-body2 q-mb-sm">Definir Valores</div>
@@ -71,14 +68,19 @@
           label="Cancelar"
           @click="onDialogCancel"
         />
-        <q-btn color="primary" label="Salvar" :loading="saving" @click="confirmSave" />
+        <q-btn
+          color="primary"
+          label="Salvar"
+          :loading="saving"
+          @click="confirmSave"
+        />
       </q-card-actions>
     </q-card>
   </q-dialog>
 </template>
 
 <script setup>
-import { ref, reactive, onMounted } from "vue";
+import { onMounted, reactive, ref } from "vue";
 import { useDialogPluginComponent, useQuasar } from "quasar";
 
 import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
@@ -108,9 +110,6 @@ const inhabitantOptions = ref([]);
 const form = reactive({
   inhabitant_classification_id:
     props.contract.inhabitant_classification_id ?? null,
-  tbr_fixed_value: props.contract.tbr_fixed_value
-    ? parseFloat(props.contract.tbr_fixed_value)
-    : null,
   tax_base_fnm:
     props.contract.marketing_fund_percentage != null
       ? parseFloat((props.contract.marketing_fund_percentage * 100).toFixed(4))
@@ -119,6 +118,9 @@ const form = reactive({
     props.contract.maintance_tax_percentage != null
       ? parseFloat((props.contract.maintance_tax_percentage * 100).toFixed(4))
       : null,
+  tbr_fixed_value: props.contract.tbr_fixed_value
+    ? parseFloat(props.contract.tbr_fixed_value)
+    : null,
 });
 
 async function loadData() {

+ 74 - 62
src/pages/unit/components/ViewContractDialog.vue

@@ -5,7 +5,7 @@
       style="width: 100%; max-width: 1100px"
     >
       <DefaultDialogHeader
-        :title="() => 'Visualizar Contrato'"
+        title="Visualizar Contrato"
         @close="onDialogCancel"
       />
 
@@ -15,38 +15,38 @@
         <div class="row q-col-gutter-x-sm">
           <DefaultInput
             :model-value="unitData.id"
-            label="ID"
-            color="secondary"
-            label-color="secondary"
             class="col-md-3 col-12"
+            color="secondary"
             disable
+            label="ID"
+            label-color="secondary"
           />
 
           <DefaultInput
             :model-value="unitData.franchisee_name"
-            label="Nome do Franqueado"
-            color="secondary"
-            label-color="secondary"
             class="col-md-3 col-12"
+            color="secondary"
             disable
+            label="Nome do Franqueado"
+            label-color="secondary"
           />
 
           <DefaultInput
             :model-value="unitData.franchisee_document"
-            label="CPF / CNH"
-            color="secondary"
-            label-color="secondary"
             class="col-md-3 col-12"
+            color="secondary"
             disable
+            label="CPF / CNH"
+            label-color="secondary"
           />
 
           <DefaultInput
             :model-value="unitData.franchisee_birthday"
-            label="Data de Nascimento"
-            color="secondary"
-            label-color="secondary"
             class="col-md-3 col-12"
+            color="secondary"
             disable
+            label="Data de Nascimento"
+            label-color="secondary"
           />
         </div>
       </q-card-section>
@@ -55,67 +55,67 @@
         <div class="row q-col-gutter-sm">
           <DefaultInputDatePicker
             v-model:untreated-date="contractForm.start_date"
-            label="Data de Início"
-            color="secondary"
-            label-color="secondary"
             class="col-md-3 col-12"
+            color="secondary"
             disable
+            label="Data de Início"
+            label-color="secondary"
           />
 
           <DefaultInputDatePicker
             v-model:untreated-date="contractForm.end_date"
-            label="Data de Fim"
-            color="secondary"
-            label-color="secondary"
             class="col-md-3 col-12"
+            color="secondary"
             disable
+            label="Data de Fim"
+            label-color="secondary"
           />
 
           <DefaultInput
             v-model="contractForm.invoice_due_date"
-            label="Dia de Vencimento do Boleto"
+            class="col-md-3 col-12"
             color="secondary"
+            disable
+            label="Dia de Vencimento do Boleto"
             label-color="secondary"
             type="number"
-            class="col-md-3 col-12"
-            disable
           />
 
           <DefaultSelect
             v-model="contractForm.inhabitant_classification_id"
-            label="Faixa de Habitantes"
+            class="col-md-3 col-12"
             color="secondary"
-            label-color="secondary"
-            :options="inhabitantOptions"
+            disable
             emit-value
-            map-options
-            use-input
             fill-input
             hide-selected
             input-debounce="0"
-            class="col-md-3 col-12"
-            disable
+            label="Faixa de Habitantes"
+            label-color="secondary"
+            map-options
+            use-input
+            :options="inhabitantOptions"
           />
         </div>
 
         <div class="row q-col-gutter-sm q-mt-xs">
           <DefaultCurrencyInput
             v-model="contractForm.tbr_fixed_value"
-            label="TBR $"
-            color="secondary"
-            label-color="secondary"
             class="col-md-3 col-12"
+            color="secondary"
             disable
+            label="TBR $"
+            label-color="secondary"
           />
 
           <DefaultInput
             v-model="contractForm.tax_base_royalts"
-            label="Taxa Base Royalties"
+            class="col-md-3 col-12"
             color="secondary"
+            disable
+            label="Taxa Base Royalties"
             label-color="secondary"
             type="number"
-            class="col-md-3 col-12"
-            disable
           >
             <template #append>
               <span class="text-secondary">%</span>
@@ -124,12 +124,12 @@
 
           <DefaultInput
             v-model="contractForm.tax_base_fnm"
-            label="Fundo Nacional de Marketing"
+            class="col-md-3 col-12"
             color="secondary"
+            disable
+            label="Fundo Nacional de Marketing"
             label-color="secondary"
             type="number"
-            class="col-md-3 col-12"
-            disable
           >
             <template #append>
               <span class="text-secondary">%</span>
@@ -138,12 +138,12 @@
 
           <DefaultInput
             v-model="contractForm.tax_base_maintenance"
-            label="Taxa de Manutenção"
+            class="col-md-3 col-12"
             color="secondary"
+            disable
+            label="Taxa de Manutenção"
             label-color="secondary"
             type="number"
-            class="col-md-3 col-12"
-            disable
           >
             <template #append>
               <span class="text-secondary">%</span>
@@ -153,33 +153,31 @@
       </q-card-section>
 
       <q-card-section>
-        <div class="text-body2 q-mb-sm">Histórico de Reajuste de Valores e TBR</div>
+        <div class="text-body2 q-mb-sm">
+          Histórico de Reajuste de Valores e TBR
+        </div>
 
         <q-table
-          flat
           dense
-          :rows="tbrHistory"
-          :columns="historyColumns"
+          flat
+          no-data-label="Nenhum histórico disponível"
           row-key="id"
+          :columns="historyColumns"
           :loading="loadingHistory"
           :pagination="{ rowsPerPage: 5 }"
-          no-data-label="Nenhum histórico disponível"
+          :rows="tbrHistory"
         />
       </q-card-section>
 
       <q-card-actions align="right">
-        <q-btn
-          color="primary"
-          label="Fechar"
-          @click="onDialogCancel"
-        />
+        <q-btn color="primary" label="Fechar" @click="onDialogCancel" />
       </q-card-actions>
     </q-card>
   </q-dialog>
 </template>
 
 <script setup>
-import { ref, reactive, onMounted } from "vue";
+import { onMounted, reactive, ref } from "vue";
 import { useDialogPluginComponent } from "quasar";
 
 import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
@@ -188,21 +186,21 @@ import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePick
 import DefaultCurrencyInput from "src/components/defaults/DefaultCurrencyInput.vue";
 import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
 
-import { getUnit } from "src/api/unit";
-import { getInhabitantClassificationsForSelect } from "src/api/inhabitant_classification";
 import { getFranchiseeContractTaxHistory } from "src/api/franchisee_contract";
+import { getInhabitantClassificationsForSelect } from "src/api/inhabitant_classification";
+import { getUnitMe } from "src/api/unit";
 
 defineEmits([...useDialogPluginComponent.emits]);
 
 const props = defineProps({
-  unitId: {
-    type: Number,
-    required: true,
-  },
   contract: {
     type: Object,
     required: true,
   },
+  unitId: {
+    type: Number,
+    required: true,
+  },
 });
 
 const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
@@ -213,28 +211,34 @@ const tbrHistory = ref([]);
 
 const unitData = reactive({
   id: null,
-  franchisee_name: null,
-  franchisee_document: null,
   franchisee_birthday: null,
+  franchisee_document: null,
+  franchisee_name: null,
 });
 
 const contractForm = reactive({
   start_date: props.contract.start_date ?? null,
   end_date: props.contract.end_date ?? null,
+
   tbr_fixed_value: props.contract.tbr_fixed_value
     ? parseFloat(props.contract.tbr_fixed_value)
     : null,
+
   invoice_due_date: props.contract.invoice_due_date ?? null,
+
   inhabitant_classification_id:
     props.contract.inhabitant_classification_id ?? null,
+
   tax_base_royalts:
     props.contract.tbr_fixed_value_percentage != null
       ? parseFloat((props.contract.tbr_fixed_value_percentage * 100).toFixed(4))
       : null,
+
   tax_base_fnm:
     props.contract.marketing_fund_percentage != null
       ? parseFloat((props.contract.marketing_fund_percentage * 100).toFixed(4))
       : null,
+
   tax_base_maintenance:
     props.contract.maintance_tax_percentage != null
       ? parseFloat((props.contract.maintance_tax_percentage * 100).toFixed(4))
@@ -255,7 +259,12 @@ const historyColumns = [
     field: "inhabitant_classification",
     align: "left",
   },
-  { name: "tbr_fixed_value", label: "TBR", field: "tbr_fixed_value", align: "left" },
+  {
+    name: "tbr_fixed_value",
+    label: "TBR",
+    field: "tbr_fixed_value",
+    align: "left",
+  },
   {
     name: "marketing_fund_percentage",
     label: "FNM",
@@ -278,14 +287,16 @@ const historyColumns = [
 
 async function loadData() {
   const [unit, classifications] = await Promise.all([
-    getUnit(props.unitId),
+    getUnitMe(),
     getInhabitantClassificationsForSelect(),
   ]);
 
   unitData.id = unit.id;
+
   unitData.franchisee_name = unit.name_responsible;
 
   const firstPartner = unit.partners?.[0];
+
   if (firstPartner) {
     unitData.franchisee_document = firstPartner.cpf;
     unitData.franchisee_birthday = firstPartner.birth_date;
@@ -297,6 +308,7 @@ async function loadData() {
   }));
 
   loadingHistory.value = true;
+
   try {
     tbrHistory.value = await getFranchiseeContractTaxHistory(props.contract.id);
   } finally {

+ 5 - 4
src/pages/unit/tabs/ContractsTab.vue

@@ -43,10 +43,6 @@ import DefaultTable from "src/components/defaults/DefaultTable.vue";
 import { getFranchiseeContractsByUnit } from "src/api/franchisee_contract";
 import { formatDateYMDtoDMY, formatToBRLCurrency } from "src/helpers/utils";
 
-const ViewContractDialog = defineAsyncComponent(
-  () => import("src/pages/unit/components/ViewContractDialog.vue"),
-);
-
 const props = defineProps({
   unitId: {
     type: Number,
@@ -55,8 +51,13 @@ const props = defineProps({
 });
 
 const $q = useQuasar();
+
 const tableRef = ref(null);
 
+const ViewContractDialog = defineAsyncComponent(
+  () => import("src/pages/unit/components/ViewContractDialog.vue"),
+);
+
 const loadContracts = () => getFranchiseeContractsByUnit(props.unitId);
 
 function openViewDialog(contract) {

+ 338 - 139
src/pages/unit/tabs/FinancialTab.vue

@@ -1,159 +1,258 @@
 <template>
-  <div class="q-pa-md column q-gutter-lg">
-    <!-- Dados de Contato -->
-    <div>
-      <div class="text-h6 q-mb-md">Dados de Contato</div>
+  <div class="q-pa-md">
+    <div class="row q-col-gutter-lg">
+      <div class="col-12 col-md-6 column q-gutter-lg">
+        <section>
+          <div class="text-subtitle1 text-weight-medium q-mb-sm">
+            Dados Bancários
+          </div>
 
-      <div v-if="loadingPartners" class="row justify-center q-pa-md">
-        <q-spinner color="primary" size="32px" />
-      </div>
+          <div class="row q-col-gutter-sm">
+            <DefaultSelect
+              v-model="form.tax_regime"
+              class="col-12"
+              disable
+              emit-value
+              label="Regime Tributário"
+              map-options
+              outlined
+              :options="taxRegimeOptions"
+            />
 
-      <template v-else>
-        <div class="row q-col-gutter-md">
-          <div
-            v-for="(partner, index) in partners"
-            :key="index"
-            class="col-12 col-md-4"
-          >
-            <PartnerCardComponent
-              :partner="partner"
-              :editable="false"
+            <DefaultInput
+              v-model="form.bank"
+              class="col-12"
+              disable
+              label="Banco"
+              outlined
+            />
+
+            <DefaultInput
+              v-model="form.agency"
+              class="col-12"
+              disable
+              label="Agência"
+              outlined
+            />
+
+            <DefaultInput
+              v-model="form.account"
+              class="col-12"
+              disable
+              label="Conta"
+              outlined
+            />
+
+            <DefaultSelect
+              v-model="form.account_type"
+              class="col-12"
+              disable
+              emit-value
+              label="Tipo de Conta"
+              map-options
+              outlined
+              :options="accountTypeOptions"
+            />
+
+            <DefaultInput
+              v-model="form.account_holder"
+              class="col-12"
+              disable
+              label="Titular da Conta"
+              outlined
+            />
+
+            <DefaultInput
+              v-model="form.pix_key"
+              class="col-12"
+              disable
+              label="Chave Pix"
+              outlined
             />
           </div>
-          <div
-            v-if="partners.length === 0"
-            class="col-12 text-grey-6 text-center q-pa-md"
-          >
-            Nenhum sócio cadastrado.
+        </section>
+
+        <section>
+          <div class="text-subtitle1 text-weight-medium q-mb-sm">
+            Dados para Faturamento
           </div>
-        </div>
-      </template>
-    </div>
 
-    <!-- Dados Bancários -->
-    <div>
-      <div class="text-subtitle1 text-weight-medium q-mb-sm">
-        Dados Bancários
-      </div>
+          <div class="row q-col-gutter-sm">
+            <DefaultSelect
+              v-model="form.billing_method"
+              class="col-12"
+              disable
+              emit-value
+              label="Forma de Cobrança"
+              map-options
+              outlined
+              :options="billingMethodOptions"
+            />
 
-      <div class="row q-col-gutter-sm">
-        <DefaultInput
-          v-model="form.account_holder"
-          label="Titular da Conta"
-          class="col-12 col-md-4"
-          outlined
-          disable
-        />
-        <DefaultInput
-          v-model="form.agency"
-          label="Agência"
-          class="col-12 col-md-4"
-          outlined
-          disable
-        />
-        <DefaultInput
-          v-model="form.account"
-          label="Conta"
-          class="col-12 col-md-4"
-          outlined
-          disable
-        />
-
-        <DefaultInput
-          v-model="form.bank"
-          label="Banco"
-          class="col-12 col-md-3"
-          outlined
-          disable
-        />
-        <DefaultInput
-          v-model="form.pix_key"
-          label="Chave Pix"
-          class="col-12 col-md-3"
-          outlined
-          disable
-        />
-        <DefaultSelect
-          v-model="form.tax_regime"
-          label="Regime Tributário"
-          :options="taxRegimeOptions"
-          class="col-12 col-md-3"
-          outlined
-          emit-value
-          map-options
-          disable
-        />
-        <DefaultSelect
-          v-model="form.account_type"
-          label="Tipo de Conta"
-          :options="accountTypeOptions"
-          class="col-12 col-md-3"
-          outlined
-          emit-value
-          map-options
-          disable
-        />
-      </div>
-    </div>
+            <DefaultInputDatePicker
+              v-model="form.due_date"
+              class="col-12"
+              disable
+              label="Data de Vencimento"
+            />
 
-    <!-- Dados para Faturamento -->
-    <div>
-      <div class="text-subtitle1 text-weight-medium q-mb-sm">
-        Dados para Faturamento
+            <DefaultInput
+              v-model="form.financial_email"
+              class="col-12"
+              disable
+              label="E-mail Financeiro"
+              outlined
+            />
+          </div>
+        </section>
       </div>
 
-      <div class="row q-col-gutter-sm">
-        <DefaultSelect
-          v-model="form.billing_method"
-          label="Forma de Cobrança"
-          :options="billingMethodOptions"
-          class="col-12 col-md-6"
-          outlined
-          emit-value
-          map-options
-          disable
-        />
-        <DefaultInputDatePicker
-          v-model="form.due_date"
-          label="Data de Vencimento"
-          class="col-12 col-md-6"
-          disable
-        />
-        <DefaultInput
-          v-model="form.financial_email"
-          label="E-mail Financeiro"
-          class="col-12"
-          outlined
-          disable
-        />
+      <div class="col-12 col-md-6 column q-gutter-lg">
+        <section>
+          <div class="text-subtitle1 text-weight-medium q-mb-sm">
+            Dados do Contrato
+          </div>
+
+          <div class="row q-col-gutter-sm">
+            <DefaultInput
+              :model-value="form.group"
+              class="col-12"
+              disable
+              label="Grupo"
+              outlined
+            />
+
+            <DefaultInput
+              :model-value="
+                formatPercent(currentContract?.tbr_fixed_value_percentage)
+              "
+              class="col-12"
+              disable
+              label="Royalties"
+              outlined
+            />
+
+            <DefaultInput
+              :model-value="
+                formatPercent(currentContract?.maintance_tax_percentage)
+              "
+              class="col-12"
+              disable
+              label="Taxa de Manutenção"
+              outlined
+            />
+
+            <DefaultInput
+              :model-value="
+                formatPercent(currentContract?.marketing_fund_percentage)
+              "
+              class="col-12"
+              disable
+              label="Fundo de Marketing"
+              outlined
+            />
+
+            <DefaultInput
+              :model-value="
+                formatToBRLCurrency(currentContract?.tbr_fixed_value)
+              "
+              class="col-12"
+              disable
+              label="TBR"
+              outlined
+            />
+          </div>
+        </section>
+
+        <section>
+          <div class="text-subtitle1 text-weight-medium q-mb-sm">
+            Histórico de Royalties
+          </div>
+
+          <q-table
+            dense
+            flat
+            :columns="royaltiesHistoryColumns"
+            :loading="loadingRoyaltiesHistory"
+            no-data-label="Nenhum histórico de royalties encontrado."
+            row-key="id"
+            :pagination="{ rowsPerPage: 5 }"
+            :rows="royaltiesHistory"
+          >
+            <template #body-cell-created_at="{ row }">
+              <q-td>{{ formatDate(row.created_at) }}</q-td>
+            </template>
+
+            <template #body-cell-tbr_fixed_value="{ row }">
+              <q-td>{{ formatToBRLCurrency(row.tbr_fixed_value) }}</q-td>
+            </template>
+
+            <template #body-cell-tbr_fixed_value_percentage="{ row }">
+              <q-td>{{ formatPercent(row.tbr_fixed_value_percentage) }}</q-td>
+            </template>
+
+            <template #body-cell-marketing_fund_percentage="{ row }">
+              <q-td>{{ formatPercent(row.marketing_fund_percentage) }}</q-td>
+            </template>
+
+            <template #body-cell-maintance_tax_percentage="{ row }">
+              <q-td>{{ formatPercent(row.maintance_tax_percentage) }}</q-td>
+            </template>
+          </q-table>
+        </section>
+
+        <section>
+          <div class="text-subtitle1 text-weight-medium q-mb-sm">
+            Dados de Contato
+          </div>
+
+          <div v-if="loadingPartners" class="row justify-center q-pa-md">
+            <q-spinner color="primary" size="32px" />
+          </div>
+
+          <template v-else>
+            <div class="row q-col-gutter-md">
+              <div
+                v-for="partner in partners"
+                :key="partner.id"
+                class="col-12 col-sm-6"
+              >
+                <PartnerCardComponent :editable="false" :partner="partner" />
+              </div>
+
+              <div
+                v-if="partners.length === 0"
+                class="col-12 text-grey-6 text-center q-pa-md"
+              >
+                Nenhum sócio cadastrado.
+              </div>
+            </div>
+          </template>
+        </section>
       </div>
     </div>
-
-    <!-- (read-only) -->
   </div>
 </template>
 
 <script setup>
-import { ref, onMounted } from "vue";
+import { computed, onMounted, ref } from "vue";
+import { formatDateYMDtoDMY, formatToBRLCurrency } from "src/helpers/utils";
+import {
+  getFranchiseeContractsByUnit,
+  getFranchiseeContractTaxHistory,
+} from "src/api/franchisee_contract";
+import { getFinancialMe } from "src/api/unit_financial";
+import { getPartnersByUnit } from "src/api/unit_partner";
+
 import DefaultInput from "src/components/defaults/DefaultInput.vue";
-import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
 import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
+import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
 import PartnerCardComponent from "src/components/shared/PartnerCardComponent.vue";
-import { getFinancialByUnit } from "src/api/unit_financial";
-import { getPartnersByUnit } from "src/api/unit_partner";
 
 const props = defineProps({
   unitId: { type: Number, default: null },
 });
 
-const taxRegimeOptions = [
-  { label: "Selecione", value: null },
-  { label: "Simples Nacional", value: "simples_nacional" },
-  { label: "Lucro Presumido", value: "lucro_presumido" },
-  { label: "Lucro Real", value: "lucro_real" },
-  { label: "MEI", value: "mei" },
-];
-
 const accountTypeOptions = [
   { label: "Selecione", value: null },
   { label: "Conta Corrente", value: "corrente" },
@@ -168,31 +267,112 @@ const billingMethodOptions = [
   { label: "Cartão de Crédito", value: "credit_card" },
 ];
 
+const taxRegimeOptions = [
+  { label: "Selecione", value: null },
+  { label: "Simples Nacional", value: "simples_nacional" },
+  { label: "Lucro Presumido", value: "lucro_presumido" },
+  { label: "Lucro Real", value: "lucro_real" },
+  { label: "MEI", value: "mei" },
+];
+
+const royaltiesHistoryColumns = [
+  {
+    name: "created_at",
+    label: "Data",
+    field: "created_at",
+    align: "left",
+  },
+  {
+    name: "tbr_fixed_value",
+    label: "TBR",
+    field: "tbr_fixed_value",
+    align: "left",
+  },
+  {
+    name: "tbr_fixed_value_percentage",
+    label: "Royalties",
+    field: "tbr_fixed_value_percentage",
+    align: "left",
+  },
+  {
+    name: "marketing_fund_percentage",
+    label: "FNM",
+    field: "marketing_fund_percentage",
+    align: "left",
+  },
+  {
+    name: "maintance_tax_percentage",
+    label: "Manutenção",
+    field: "maintance_tax_percentage",
+    align: "left",
+  },
+];
+
 const defaultForm = () => ({
-  tax_regime: null,
-  bank: null,
-  agency: null,
   account: null,
-  account_type: null,
   account_holder: null,
-  pix_key: null,
+  account_type: null,
+  agency: null,
+  bank: null,
   billing_method: null,
   due_date: null,
   financial_email: null,
+  group: null,
+  pix_key: null,
+  tax_regime: null,
 });
 
+const contracts = ref([]);
 const form = ref(defaultForm());
-const partners = ref([]);
 const loadingPartners = ref(false);
+const loadingRoyaltiesHistory = ref(false);
+const partners = ref([]);
+const royaltiesHistory = ref([]);
+
+const currentContract = computed(() => {
+  const today = new Date().toISOString().slice(0, 10);
+
+  return (
+    contracts.value.find((contract) => {
+      if (!contract.start_date || !contract.end_date) return false;
+      return contract.start_date <= today && contract.end_date >= today;
+    }) ??
+    contracts.value[0] ??
+    null
+  );
+});
 
 async function fetchData() {
   if (!props.unitId) return;
   await Promise.allSettled([loadFinancial(), loadPartners()]);
+  await loadContracts();
+  await loadRoyaltiesHistory();
+}
+
+function formatDate(value) {
+  return value ? formatDateYMDtoDMY(value) : "";
+}
+
+function formatPercent(value) {
+  if (value == null || value === "") return "";
+  return `${(Number(value) * 100).toLocaleString("pt-BR", {
+    maximumFractionDigits: 2,
+    minimumFractionDigits: 0,
+  })}%`;
+}
+
+async function loadContracts() {
+  try {
+    contracts.value = await getFranchiseeContractsByUnit();
+  } catch (e) {
+    console.error(e);
+    contracts.value = [];
+  }
 }
 
 async function loadFinancial() {
   try {
-    const data = await getFinancialByUnit(props.unitId);
+    const data = await getFinancialMe();
     if (data) Object.assign(form.value, data);
   } catch (e) {
     console.error(e);
@@ -201,8 +381,9 @@ async function loadFinancial() {
 
 async function loadPartners() {
   loadingPartners.value = true;
+
   try {
-    partners.value = await getPartnersByUnit(props.unitId);
+    partners.value = await getPartnersByUnit();
   } catch (e) {
     console.error(e);
   } finally {
@@ -210,5 +391,23 @@ async function loadPartners() {
   }
 }
 
+async function loadRoyaltiesHistory() {
+  royaltiesHistory.value = [];
+
+  if (!currentContract.value?.id) return;
+
+  loadingRoyaltiesHistory.value = true;
+
+  try {
+    royaltiesHistory.value = await getFranchiseeContractTaxHistory(
+      currentContract.value.id,
+    );
+  } catch (e) {
+    console.error(e);
+  } finally {
+    loadingRoyaltiesHistory.value = false;
+  }
+}
+
 onMounted(fetchData);
 </script>

+ 10 - 4
src/pages/unit/tabs/HistoryTab.vue

@@ -12,7 +12,10 @@
         </div>
 
         <template v-else>
-          <div v-if="histories.length === 0" class="text-center text-grey-6 q-pa-xl">
+          <div
+            v-if="histories.length === 0"
+            class="text-center text-grey-6 q-pa-xl"
+          >
             <q-icon name="mdi-history" size="48px" color="grey-4" />
             <div class="q-mt-sm">Nenhum histórico registrado.</div>
           </div>
@@ -27,7 +30,11 @@
               @click="selectedIndex = index"
             >
               <q-item-section avatar>
-                <q-icon name="mdi-text-box-outline" color="primary-2" size="md" />
+                <q-icon
+                  name="mdi-text-box-outline"
+                  color="primary-2"
+                  size="md"
+                />
               </q-item-section>
 
               <q-item-section>
@@ -39,8 +46,7 @@
                 </q-item-label>
               </q-item-section>
 
-              <q-item-section side>
-              </q-item-section>
+              <q-item-section side> </q-item-section>
             </q-item>
           </q-list>
         </template>

+ 27 - 31
src/pages/unit/tabs/MediasTab.vue

@@ -19,8 +19,15 @@
         </div>
 
         <template v-else>
-          <div v-if="medias.length === 0" class="text-center text-grey-6 q-pa-xl">
-            <q-icon name="mdi-image-multiple-outline" size="48px" color="grey-4" />
+          <div
+            v-if="medias.length === 0"
+            class="text-center text-grey-6 q-pa-xl"
+          >
+            <q-icon
+              name="mdi-image-multiple-outline"
+              size="48px"
+              color="grey-4"
+            />
             <div class="q-mt-sm">Nenhuma mídia adicionada.</div>
           </div>
 
@@ -34,7 +41,11 @@
               @click="selectedIndex = index"
             >
               <q-item-section avatar>
-                <q-icon :name="getFileIcon(item.mime_type)" :color="getFileColor(item.mime_type)" size="md" />
+                <q-icon
+                  :name="getFileIcon(item.mime_type)"
+                  :color="getFileColor(item.mime_type)"
+                  size="md"
+                />
               </q-item-section>
 
               <q-item-section>
@@ -46,13 +57,7 @@
                 </q-item-label>
               </q-item-section>
 
-              <q-item-section side>
-                <q-btn
-                  flat round dense icon="delete"
-                  color="negative" size="sm"
-                  @click.stop="onRemove(item, index)"
-                />
-              </q-item-section>
+              <q-item-section side />
             </q-item>
           </q-list>
         </template>
@@ -67,7 +72,11 @@
             style="min-height: 500px"
           >
             <div class="column items-center q-gutter-sm">
-              <q-icon name="mdi-image-multiple-outline" size="64px" color="grey-3" />
+              <q-icon
+                name="mdi-image-multiple-outline"
+                size="64px"
+                color="grey-3"
+              />
               <span>Selecione uma mídia para visualizar</span>
             </div>
           </div>
@@ -87,7 +96,12 @@
             <iframe
               v-else
               :src="medias[selectedIndex].file_url"
-              style="width: 100%; min-height: 500px; border: none; border-radius: 8px"
+              style="
+                width: 100%;
+                min-height: 500px;
+                border: none;
+                border-radius: 8px;
+              "
             />
           </template>
         </div>
@@ -99,7 +113,7 @@
 <script setup>
 import { ref, onMounted } from "vue";
 import { useQuasar } from "quasar";
-import { getMediasByUnit, deleteMedia } from "src/api/unit_media";
+import { getMediasByUnit } from "src/api/unit_media";
 import AddMediaDialog from "src/pages/unit/components/AddMediaDialog.vue";
 
 const props = defineProps({
@@ -133,24 +147,6 @@ function openAddDialog() {
   });
 }
 
-function onRemove(item, index) {
-  $q.dialog({
-    title: "Remover mídia",
-    message: `Deseja remover a mídia "${item.title}"?`,
-    ok: { color: "negative", label: "Remover" },
-    cancel: { color: "primary", outline: true, label: "Cancelar" },
-  }).onOk(async () => {
-    try {
-      await deleteMedia(item.id);
-      medias.value.splice(index, 1);
-      if (selectedIndex.value === index) selectedIndex.value = null;
-      else if (selectedIndex.value > index) selectedIndex.value--;
-    } catch (e) {
-      console.error(e);
-    }
-  });
-}
-
 function isImage(mimeType) {
   return mimeType?.startsWith("image/");
 }

+ 72 - 64
src/pages/unit/tabs/UnitDataTab.vue

@@ -7,130 +7,130 @@
         <div class="row full-width q-mt-md q-col-gutter-sm">
           <DefaultInput
             :model-value="form.social_reason"
-            label="Razão Social"
             class="col-12"
-            outlined
             disable
+            label="Razão Social"
+            outlined
           />
 
           <DefaultInput
             :model-value="form.fantasy_name"
-            label="Nome Fantasia"
             class="col-12"
-            outlined
             disable
+            label="Nome Fantasia"
+            outlined
           />
 
           <DefaultInput
             :model-value="form.cnpj"
-            label="CNPJ"
             class="col-4"
-            outlined
             disable
+            label="CNPJ"
+            outlined
           />
 
           <DefaultInput
             :model-value="form.state_registration"
-            label="Inscrição Estadual"
             class="col-4"
-            outlined
             disable
+            label="Inscrição Estadual"
+            outlined
           />
 
           <DefaultInput
             :model-value="form.name_responsible"
-            label="Responsável"
             class="col-4"
-            outlined
             disable
+            label="Responsável"
+            outlined
           />
 
           <DefaultInput
             :model-value="form.postal_code"
-            label="CEP"
             class="col-3"
-            outlined
             disable
+            label="CEP"
+            outlined
           />
 
           <DefaultInput
             :model-value="form.street"
-            label="Endereço"
             class="col-6"
-            outlined
             disable
+            label="Endereço"
+            outlined
           />
 
           <DefaultInput
             :model-value="form.address_number"
-            label="Número"
             class="col-3"
-            outlined
             disable
+            label="Número"
+            outlined
           />
 
           <DefaultInput
             :model-value="form.neighborhood"
-            label="Bairro"
             class="col-4"
-            outlined
             disable
+            label="Bairro"
+            outlined
           />
 
           <DefaultInput
             :model-value="stateName"
-            label="Estado"
             class="col-4"
-            outlined
             disable
+            label="Estado"
+            outlined
           />
 
           <DefaultInput
             :model-value="cityName"
-            label="Cidade"
             class="col-4"
-            outlined
             disable
+            label="Cidade"
+            outlined
           />
 
           <DefaultInput
             :model-value="form.complement"
-            label="Complemento"
             class="col-12"
-            outlined
             disable
+            label="Complemento"
+            outlined
           />
 
           <DefaultInput
             :model-value="form.email"
-            label="E-mail Principal"
             class="col-6"
-            outlined
             disable
+            label="E-mail Principal"
+            outlined
           />
 
           <DefaultInput
             :model-value="form.secondary_email"
-            label="E-mail Secundário"
             class="col-6"
-            outlined
             disable
+            label="E-mail Secundário"
+            outlined
           />
 
           <DefaultInput
             :model-value="form.phone_number"
-            label="Telefone"
             class="col-6"
-            outlined
             disable
+            label="Telefone"
+            outlined
           />
 
           <DefaultInput
             :model-value="form.cell_number"
-            label="Celular"
             class="col-6"
-            outlined
             disable
+            label="Celular"
+            outlined
           />
 
           <div class="col-12 q-mt-sm">
@@ -139,16 +139,18 @@
 
           <DefaultPasswordInput
             v-model="password"
-            label="Nova Senha"
+            v-model:error="validationErrors.password"
             class="col-6"
+            label="Nova Senha"
             outlined
             :rules="password ? [inputRules.password] : []"
           />
 
           <DefaultPasswordInput
             v-model="passwordConfirmation"
-            label="Confirmar Nova Senha"
+            v-model:error="validationErrors.password_confirmation"
             class="col-6"
+            label="Confirmar Nova Senha"
             outlined
             :rules="password ? [inputRules.samePassword(password)] : []"
           />
@@ -156,10 +158,10 @@
 
         <div class="row justify-end q-mt-md items-end full-width q-px-xs">
           <q-btn
-            label="Salvar"
             color="primary-2"
-            :loading="loading"
+            label="Salvar"
             :disable="!hasChanges"
+            :loading="loading"
             @click="onSave"
           />
         </div>
@@ -169,40 +171,64 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted } from "vue";
+import { computed, onMounted, ref } from "vue";
+import { getUnitMe, updateUnitMe } from "src/api/unit";
+import { updateUserMe } from "src/api/user.js";
+import { useInputRules } from "src/composables/useInputRules";
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
+
 import DefaultInput from "src/components/defaults/DefaultInput.vue";
 import DefaultPasswordInput from "src/components/defaults/DefaultPasswordInput.vue";
 import AvatarImageComponent from "src/components/shared/AvatarImageComponent.vue";
-import { useSubmitHandler } from "src/composables/useSubmitHandler";
-import { useInputRules } from "src/composables/useInputRules";
-import { getUnitMe, updateUnitMe } from "src/api/unit";
-import { updateUserMe } from "src/api/user.js";
-import { userStore } from "src/stores/user";
 
 const props = defineProps({
-  unitId: { type: Number, default: null },
   getFormAsFormData: { type: Function, required: true },
   setUpdateFormAsOriginal: { type: Function, required: true },
+  unitId: { type: Number, default: null },
 });
 
 const form = defineModel("form", { type: Object, required: true });
-const store = userStore();
+
 const { inputRules } = useInputRules();
 
-const formRef = ref(null);
 const avatarRef = ref(null);
+const cityName = ref("");
+const formRef = ref(null);
 const newAvatarFile = ref(null);
 const password = ref(null);
 const passwordConfirmation = ref(null);
 const stateName = ref("");
-const cityName = ref("");
 
 const hasChanges = computed(() => !!newAvatarFile.value || !!password.value);
 
+const { loading, validationErrors, execute } = useSubmitHandler({ formRef });
+
 function onAvatarChange(file) {
   newAvatarFile.value = file;
 }
 
+async function onSave() {
+  await execute(async () => {
+    if (newAvatarFile.value) {
+      const formData = new FormData();
+
+      formData.append("avatar", newAvatarFile.value);
+
+      await updateUnitMe(formData);
+
+      newAvatarFile.value = null;
+    }
+
+    if (password.value) {
+      await updateUserMe({ password: password.value });
+
+      password.value = null;
+
+      passwordConfirmation.value = null;
+    }
+  });
+}
+
 onMounted(async () => {
   try {
     const unit = await getUnitMe();
@@ -230,22 +256,4 @@ onMounted(async () => {
     console.error(e);
   }
 });
-
-const { loading, execute } = useSubmitHandler({ formRef });
-
-async function onSave() {
-  await execute(async () => {
-    if (newAvatarFile.value) {
-      const formData = new FormData();
-      formData.append("avatar", newAvatarFile.value);
-      await updateUnitMe(formData);
-      newAvatarFile.value = null;
-    }
-    if (password.value) {
-      await updateUserMe({ password: password.value });
-      password.value = null;
-      passwordConfirmation.value = null;
-    }
-  });
-}
 </script>

+ 6 - 0
src/router/routes/unit.route.js

@@ -6,6 +6,12 @@ export default [
     meta: {
       title: { value: "Dados da Unidade", translate: false },
       requireAuth: true,
+      breadcrumbs: [
+        {
+          name: "UnitDataPage",
+          title: "Dados da Unidade",
+        },
+      ],
     },
   },
   {