Parcourir la source

pagina de configuracoes da empresa para alterar dados da landingpage

Gustavo Zanatta il y a 1 mois
Parent
commit
2ccdc9d9a3

+ 58 - 0
src/api/companySettings.js

@@ -0,0 +1,58 @@
+import api from "src/api";
+import { createCachedApi } from "./cacheService";
+
+const cachedApi = createCachedApi("company-settings", 300);
+const cachedBenefits = createCachedApi("company-benefits", 300);
+
+export const getCompanySettings = async () => {
+  const { data } = await cachedApi.get("/company-settings");
+  return data.payload;
+};
+
+export const updateCompanySettings = async (payload) => {
+  const { data } = await cachedApi.put("/company-settings", payload);
+  return data.payload;
+};
+
+export const uploadCompanyHeroImage = async (file) => {
+  const form = new FormData();
+  form.append("image", file);
+  const { data } = await api.post("/company-settings/hero-image", form, {
+    headers: { "Content-Type": "multipart/form-data" },
+  });
+  await cachedApi.invalidateAll("/company-settings");
+  return data.payload;
+};
+
+export const removeCompanyHeroImage = async () => {
+  const { data } = await api.delete("/company-settings/hero-image");
+  await cachedApi.invalidateAll("/company-settings");
+  return data.payload;
+};
+
+export const getCompanyBenefits = async () => {
+  const { data } = await cachedBenefits.get("/company-benefits");
+  return data.payload;
+};
+
+export const createCompanyBenefit = async (payload) => {
+  const { data } = await api.post("/company-benefits", payload);
+  await cachedBenefits.invalidateAll("/company-benefits");
+  return data.payload;
+};
+
+export const updateCompanyBenefit = async (id, payload) => {
+  const { data } = await api.put(`/company-benefits/${id}`, payload);
+  await cachedBenefits.invalidateAll("/company-benefits");
+  return data.payload;
+};
+
+export const reorderCompanyBenefits = async (items) => {
+  await api.put("/company-benefits/reorder", items);
+  await cachedBenefits.invalidateAll("/company-benefits");
+};
+
+export const deleteCompanyBenefit = async (id) => {
+  await api.delete(`/company-benefits/${id}`);
+  await cachedBenefits.invalidateAll("/company-benefits");
+};

+ 131 - 0
src/components/defaults/MdiIconPicker.vue

@@ -0,0 +1,131 @@
+<template>
+  <q-field
+    :model-value="selected"
+    :rules="rules"
+    :label="label"
+    outlined
+    stack-label
+    no-error-icon
+    class="mdi-picker-field"
+    readonly
+  >
+    <template #control>
+      <div class="self-center full-width flex flex-center">
+        <q-icon
+          :name="selected || 'mdi-image-search-outline'"
+          :color="selected ? 'primary' : 'grey-5'"
+          size="2.25rem"
+          class="cursor-pointer"
+        >
+          <q-menu @hide="filterText = ''">
+            <div style="width: 280px">
+              <q-input
+                v-model="filterText"
+                dense
+                outlined
+                autofocus
+                placeholder="Buscar ícone..."
+                class="q-pa-sm"
+                @keydown.stop
+              >
+                <template #prepend>
+                  <q-icon name="mdi-magnify" size="sm" />
+                </template>
+              </q-input>
+              <q-separator />
+              <q-scroll-area style="height: 240px">
+                <q-list dense>
+                  <q-item
+                    v-for="icon in filteredIcons"
+                    :key="icon"
+                    v-close-popup
+                    clickable
+                    :active="icon === selected"
+                    active-class="bg-purple-1 text-primary"
+                    @click="selected = icon"
+                  >
+                    <q-item-section avatar style="min-width: 32px">
+                      <q-icon :name="icon" size="sm" color="primary" />
+                    </q-item-section>
+                    <q-item-section class="text-caption">{{ icon }}</q-item-section>
+                  </q-item>
+                  <q-item v-if="!filteredIcons.length" disable>
+                    <q-item-section class="text-caption text-grey text-center">
+                      Nenhum ícone encontrado
+                    </q-item-section>
+                  </q-item>
+                </q-list>
+              </q-scroll-area>
+            </div>
+          </q-menu>
+        </q-icon>
+      </div>
+    </template>
+
+    <template #append>
+      <q-icon
+        v-if="selected"
+        name="mdi-close-circle"
+        color="grey-5"
+        size="xs"
+        class="cursor-pointer"
+        @click.stop="selected = null"
+      />
+    </template>
+  </q-field>
+</template>
+
+<script setup>
+import { ref, computed } from "vue";
+import { MDI_ICONS } from "src/options/iconsOptions";
+
+const props = defineProps({
+  modelValue: {
+    type: String,
+    default: null,
+  },
+  label: {
+    type: String,
+    default: "Ícone",
+  },
+  rules: {
+    type: Array,
+    default: () => [],
+  },
+});
+
+const emit = defineEmits(["update:modelValue"]);
+
+const selected = computed({
+  get: () => props.modelValue,
+  set: (val) => emit("update:modelValue", val),
+});
+
+
+const filterText = ref("");
+
+const filteredIcons = computed(() => {
+  if (!filterText.value) return MDI_ICONS;
+  return MDI_ICONS.filter((icon) =>
+    icon.includes(filterText.value.toLowerCase()),
+  );
+});
+
+</script>
+
+<style scoped>
+.mdi-picker-field {
+  width: 100px;
+}
+
+:deep(.mdi-picker-field .q-field__control) {
+  height: 76px !important;
+  min-height: 76px !important;
+  cursor: pointer;
+}
+
+:deep(.mdi-picker-field .q-field__native) {
+  min-height: 50px;
+  padding-top: 0;
+}
+</style>

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

@@ -52,6 +52,9 @@
     },
     "convenios_medicos": {
       "description": "View, edit and add medical agreements to the system"
+    },
+    "company_settings": {
+      "description": "Configure the information displayed on the home page."
     }
   },
   "common": {
@@ -424,7 +427,40 @@
       "dependentes": "Dependents",
       "loja": "Store",
       "meus_interesses": "My Interests",
-      "meus_agendamentos": "Appointments"
+      "meus_agendamentos": "Appointments",
+      "company_settings": "Company Settings"
+    }
+  },
+  "company_settings": {
+    "section": {
+      "hero": "Home Page",
+      "stats": "Statistics",
+      "contacts": "Contacts",
+      "benefits": "Benefits"
+    },
+    "hero": {
+      "background_image": "Background Image",
+      "upload_image": "Upload Image",
+      "remove_image": "Remove",
+      "title": "Main Title",
+      "subtitle": "Subtitle"
+    },
+    "stat": {
+      "value": "Value (Stat {n})",
+      "label": "Label (Stat {n})"
+    },
+    "contact": {
+      "email": "Contact Email",
+      "phone": "Contact Phone",
+      "location": "Location",
+      "location_placeholder": "Ex: Toledo - PR"
+    },
+    "benefit": {
+      "title": "Title",
+      "title_singular": "Benefit",
+      "icon": "Icon",
+      "description": "Description",
+      "dialog_title": "Benefit"
     }
   },
   "associado": {

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

@@ -52,6 +52,9 @@
     },
     "convenios_medicos": {
       "description": "Consulte, edite y agregue los convenios médicos del sistema"
+    },
+    "company_settings": {
+      "description": "Configure la información mostrada en la página de inicio."
     }
   },
   "common": {
@@ -425,7 +428,40 @@
       "dependentes": "Dependientes",
       "loja": "Tienda",
       "meus_interesses": "Mis Intereses",
-      "meus_agendamentos": "Citas"
+      "meus_agendamentos": "Citas",
+      "company_settings": "Configuración de la Empresa"
+    }
+  },
+  "company_settings": {
+    "section": {
+      "hero": "Página de Inicio",
+      "stats": "Estadísticas",
+      "contacts": "Contactos",
+      "benefits": "Beneficios"
+    },
+    "hero": {
+      "background_image": "Imagen de Fondo",
+      "upload_image": "Subir Imagen",
+      "remove_image": "Eliminar",
+      "title": "Título Principal",
+      "subtitle": "Subtítulo"
+    },
+    "stat": {
+      "value": "Valor (Estadística {n})",
+      "label": "Etiqueta (Estadística {n})"
+    },
+    "contact": {
+      "email": "Correo de Contacto",
+      "phone": "Teléfono de Contacto",
+      "location": "Ubicación",
+      "location_placeholder": "Ej: Toledo - PR"
+    },
+    "benefit": {
+      "title": "Título",
+      "title_singular": "Beneficio",
+      "icon": "Ícono",
+      "description": "Descripción",
+      "dialog_title": "Beneficio"
     }
   },
   "associado": {

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

@@ -52,6 +52,9 @@
     },
     "convenios_medicos": {
       "description": "Consulte, edite e adicione os convênios médicos do sistema"
+    },
+    "company_settings": {
+      "description": "Configure as informações exibidas na página inicial."
     }
   },
   "common": {
@@ -425,7 +428,40 @@
       "dependentes": "Dependentes",
       "loja": "Loja",
       "meus_interesses": "Meus Interesses",
-      "meus_agendamentos": "Agendamentos"
+      "meus_agendamentos": "Agendamentos",
+      "company_settings": "Configurações da Empresa"
+    }
+  },
+  "company_settings": {
+    "section": {
+      "hero": "Página Inicial",
+      "stats": "Estatísticas",
+      "contacts": "Contatos",
+      "benefits": "Benefícios"
+    },
+    "hero": {
+      "background_image": "Imagem de Fundo",
+      "upload_image": "Enviar Imagem",
+      "remove_image": "Remover",
+      "title": "Título Principal",
+      "subtitle": "Subtítulo"
+    },
+    "stat": {
+      "value": "Valor (Estatística {n})",
+      "label": "Rótulo (Estatística {n})"
+    },
+    "contact": {
+      "email": "E-mail de Contato",
+      "phone": "Telefone de Contato",
+      "location": "Localização",
+      "location_placeholder": "Ex: Toledo - PR"
+    },
+    "benefit": {
+      "title": "Título",
+      "title_singular": "Benefício",
+      "icon": "Ícone",
+      "description": "Descrição",
+      "dialog_title": "Benefício"
     }
   },
   "associado": {

+ 224 - 0
src/options/iconsOptions.js

@@ -0,0 +1,224 @@
+export const MDI_ICONS = [
+  "mdi-tag",
+  "mdi-tag-outline",
+  "mdi-tag-multiple",
+  "mdi-tag-heart",
+  "mdi-star",
+  "mdi-star-outline",
+  "mdi-heart",
+  "mdi-heart-outline",
+  "mdi-home",
+  "mdi-home-outline",
+  "mdi-account",
+  "mdi-account-outline",
+  "mdi-account-group",
+  "mdi-account-group-outline",
+  "mdi-account-multiple",
+  "mdi-account-multiple-outline",
+  "mdi-account-check",
+  "mdi-account-check-outline",
+  "mdi-store",
+  "mdi-store-outline",
+  "mdi-cart",
+  "mdi-cart-outline",
+  "mdi-shopping",
+  "mdi-shopping-outline",
+  "mdi-gift",
+  "mdi-gift-outline",
+  "mdi-percent",
+  "mdi-percent-outline",
+  "mdi-cash",
+  "mdi-cash-multiple",
+  "mdi-currency-usd",
+  "mdi-wallet",
+  "mdi-wallet-outline",
+  "mdi-credit-card",
+  "mdi-credit-card-outline",
+  "mdi-bank",
+  "mdi-bank-outline",
+  "mdi-hospital",
+  "mdi-hospital-box",
+  "mdi-hospital-box-outline",
+  "mdi-medical-bag",
+  "mdi-stethoscope",
+  "mdi-pill",
+  "mdi-tooth",
+  "mdi-tooth-outline",
+  "mdi-eye",
+  "mdi-eye-outline",
+  "mdi-heart-pulse",
+  "mdi-run",
+  "mdi-dumbbell",
+  "mdi-yoga",
+  "mdi-bike",
+  "mdi-walk",
+  "mdi-swim",
+  "mdi-spa",
+  "mdi-leaf",
+  "mdi-food",
+  "mdi-food-apple",
+  "mdi-food-apple-outline",
+  "mdi-car",
+  "mdi-car-outline",
+  "mdi-motorbike",
+  "mdi-truck",
+  "mdi-airplane",
+  "mdi-map-marker",
+  "mdi-map-marker-outline",
+  "mdi-map",
+  "mdi-navigation",
+  "mdi-phone",
+  "mdi-phone-outline",
+  "mdi-cellphone",
+  "mdi-email",
+  "mdi-email-outline",
+  "mdi-message",
+  "mdi-message-outline",
+  "mdi-chat",
+  "mdi-chat-outline",
+  "mdi-whatsapp",
+  "mdi-bell",
+  "mdi-bell-outline",
+  "mdi-calendar",
+  "mdi-calendar-outline",
+  "mdi-calendar-check",
+  "mdi-calendar-check-outline",
+  "mdi-clock",
+  "mdi-clock-outline",
+  "mdi-check",
+  "mdi-check-circle",
+  "mdi-check-circle-outline",
+  "mdi-shield",
+  "mdi-shield-outline",
+  "mdi-shield-check",
+  "mdi-shield-check-outline",
+  "mdi-lock",
+  "mdi-lock-outline",
+  "mdi-security",
+  "mdi-family-tree",
+  "mdi-human-male-female-child",
+  "mdi-baby-carriage",
+  "mdi-human-child",
+  "mdi-school",
+  "mdi-school-outline",
+  "mdi-book",
+  "mdi-book-outline",
+  "mdi-briefcase",
+  "mdi-briefcase-outline",
+  "mdi-domain",
+  "mdi-office-building",
+  "mdi-office-building-outline",
+  "mdi-handshake",
+  "mdi-handshake-outline",
+  "mdi-thumb-up",
+  "mdi-thumb-up-outline",
+  "mdi-trophy",
+  "mdi-trophy-outline",
+  "mdi-medal",
+  "mdi-medal-outline",
+  "mdi-certificate",
+  "mdi-certificate-outline",
+  "mdi-badge-account",
+  "mdi-badge-account-outline",
+  "mdi-card-account-details",
+  "mdi-card-account-details-outline",
+  "mdi-ticket",
+  "mdi-ticket-outline",
+  "mdi-coupon",
+  "mdi-qrcode",
+  "mdi-barcode",
+  "mdi-image",
+  "mdi-image-outline",
+  "mdi-camera",
+  "mdi-camera-outline",
+  "mdi-video",
+  "mdi-video-outline",
+  "mdi-music",
+  "mdi-music-note",
+  "mdi-headphones",
+  "mdi-television",
+  "mdi-monitor",
+  "mdi-laptop",
+  "mdi-tablet",
+  "mdi-cellphone-check",
+  "mdi-wifi",
+  "mdi-bluetooth",
+  "mdi-lightning-bolt",
+  "mdi-solar-power",
+  "mdi-recycle",
+  "mdi-earth",
+  "mdi-tree",
+  "mdi-flower",
+  "mdi-flower-outline",
+  "mdi-paw",
+  "mdi-paw-outline",
+  "mdi-pizza",
+  "mdi-coffee",
+  "mdi-coffee-outline",
+  "mdi-beer",
+  "mdi-glass-cocktail",
+  "mdi-silverware-fork-knife",
+  "mdi-pool",
+  "mdi-hot-tub",
+  "mdi-sauna",
+  "mdi-basketball",
+  "mdi-soccer",
+  "mdi-tennis",
+  "mdi-golf",
+  "mdi-fishing",
+  "mdi-compass",
+  "mdi-compass-outline",
+  "mdi-binoculars",
+  "mdi-theater",
+  "mdi-palette",
+  "mdi-palette-outline",
+  "mdi-brush",
+  "mdi-pen",
+  "mdi-pencil",
+  "mdi-pencil-outline",
+  "mdi-wrench",
+  "mdi-wrench-outline",
+  "mdi-hammer",
+  "mdi-hammer-wrench",
+  "mdi-cog",
+  "mdi-cog-outline",
+  "mdi-tune",
+  "mdi-filter",
+  "mdi-filter-outline",
+  "mdi-magnify",
+  "mdi-information",
+  "mdi-information-outline",
+  "mdi-help-circle",
+  "mdi-help-circle-outline",
+  "mdi-alert-circle",
+  "mdi-alert-circle-outline",
+  "mdi-plus-circle",
+  "mdi-plus-circle-outline",
+  "mdi-minus-circle",
+  "mdi-minus-circle-outline",
+  "mdi-close-circle",
+  "mdi-close-circle-outline",
+  "mdi-numeric-1-box",
+  "mdi-numeric-2-box",
+  "mdi-numeric-3-box",
+  "mdi-crown",
+  "mdi-crown-outline",
+  "mdi-diamond",
+  "mdi-diamond-outline",
+  "mdi-fire",
+  "mdi-flash",
+  "mdi-flash-outline",
+  "mdi-rocket",
+  "mdi-rocket-outline",
+  "mdi-emoticon-happy",
+  "mdi-emoticon-happy-outline",
+  "mdi-party-popper",
+  "mdi-cake",
+  "mdi-cake-variant",
+  "mdi-balloon",
+  "mdi-hand-heart",
+  "mdi-hand-heart-outline",
+  "mdi-human-greeting",
+  "mdi-human-greeting-proximity",
+  "mdi-charity",
+];

+ 409 - 0
src/pages/company-settings/CompanySettingsPage.vue

@@ -0,0 +1,409 @@
+<template>
+  <div>
+    <DefaultHeaderPage />
+
+    <div class="q-pa-md column q-gutter-lg">
+
+      <q-card flat bordered>
+        <q-card-section class="q-pb-none">
+          <div class="text-h6 text-weight-medium">
+            {{ $t("company_settings.section.hero") }}
+          </div>
+        </q-card-section>
+        <q-card-section>
+          <q-form ref="heroFormRef" @submit="saveSettings">
+            <div class="row q-col-gutter-md">
+
+              <div class="col-12">
+                <div class="text-subtitle2 q-mb-sm">{{ $t("company_settings.hero.background_image") }}</div>
+                <div class="row items-center q-col-gutter-sm">
+                  <div class="col-auto">
+                    <q-btn
+                      outline
+                      color="primary"
+                      icon="mdi-upload"
+                      :label="$t('company_settings.hero.upload_image')"
+                      :loading="uploadingImage"
+                      @click="triggerImageUpload"
+                    />
+                    <input
+                      ref="imageInputRef"
+                      type="file"
+                      accept="image/*"
+                      class="hidden"
+                      @change="onImageSelected"
+                    />
+                  </div>
+                  <div v-if="settings.hero_background_url" class="col-auto">
+                    <q-btn
+                      flat
+                      color="negative"
+                      icon="mdi-delete"
+                      :label="$t('company_settings.hero.remove_image')"
+                      :loading="removingImage"
+                      @click="onRemoveImage"
+                    />
+                  </div>
+                  <div v-if="settings.hero_background_url" class="col-12">
+                    <img
+                      :src="settings.hero_background_url"
+                      style="max-height: 160px; border-radius: 8px; object-fit: cover"
+                      class="q-mt-xs"
+                    />
+                  </div>
+                </div>
+              </div>
+
+              <DefaultInput
+                v-model="form.hero_title"
+                v-model:error="validationErrors.hero_title"
+                :label="$t('company_settings.hero.title')"
+                class="col-12"
+              />
+
+              <DefaultInput
+                v-model="form.hero_subtitle"
+                v-model:error="validationErrors.hero_subtitle"
+                :label="$t('company_settings.hero.subtitle')"
+                type="textarea"
+                :rows="3"
+                class="col-12"
+              />
+
+              <div class="col-12">
+                <div class="text-subtitle2 q-mb-sm">{{ $t("company_settings.section.stats") }}</div>
+              </div>
+
+              <template v-for="n in 3" :key="n">
+                <DefaultInput
+                  v-model="form[`stat${n}_value`]"
+                  v-model:error="validationErrors[`stat${n}_value`]"
+                  :label="$t('company_settings.stat.value', { n })"
+                  class="col-md-3 col-6"
+                />
+                <DefaultInput
+                  v-model="form[`stat${n}_label`]"
+                  v-model:error="validationErrors[`stat${n}_label`]"
+                  :label="$t('company_settings.stat.label', { n })"
+                  class="col-md-3 col-6"
+                />
+              </template>
+
+              <div class="col-12">
+                <div class="text-subtitle2 q-mb-sm">{{ $t("company_settings.section.contacts") }}</div>
+              </div>
+
+              <DefaultInput
+                v-model="form.contact_email"
+                v-model:error="validationErrors.contact_email"
+                :label="$t('company_settings.contact.email')"
+                type="email"
+                class="col-md-4 col-12"
+              />
+              <DefaultInput
+                v-model="form.contact_phone"
+                v-model:error="validationErrors.contact_phone"
+                :label="$t('company_settings.contact.phone')"
+                class="col-md-4 col-12"
+              />
+              <DefaultInput
+                v-model="form.contact_location"
+                v-model:error="validationErrors.contact_location"
+                :label="$t('company_settings.contact.location')"
+                :placeholder="$t('company_settings.contact.location_placeholder')"
+                class="col-md-4 col-12"
+              />
+
+              <div class="col-12 flex justify-end">
+                <q-btn
+                  color="primary"
+                  icon="mdi-content-save"
+                  :label="$t('common.actions.save')"
+                  type="submit"
+                  :loading="savingSettings"
+                  :disable="!hasUpdatedFields"
+                />
+              </div>
+            </div>
+          </q-form>
+        </q-card-section>
+      </q-card>
+
+      <q-card flat bordered>
+        <q-card-section class="q-pb-none">
+          <div class="row items-center">
+            <div class="text-h6 text-weight-medium col">
+              {{ $t("company_settings.section.benefits") }}
+            </div>
+            <q-btn
+              color="primary"
+              icon="mdi-plus"
+              :label="$t('common.actions.add')"
+              padding="8px 8px"
+              @click="onAddBenefit"
+            />
+          </div>
+        </q-card-section>
+        <q-card-section>
+          <div v-if="benefitsLoading" class="flex flex-center q-py-lg">
+            <q-spinner color="primary" size="md" />
+          </div>
+
+          <template v-else>
+            <div class="row items-center q-px-sm q-py-xs text-caption text-grey-7 text-weight-medium q-mb-xs">
+              <div style="width: 28px" />
+              <div style="width: 52px">{{ $t("company_settings.benefit.icon") }}</div>
+              <div class="col">{{ $t("company_settings.benefit.title") }}</div>
+              <div class="col-5">{{ $t("company_settings.benefit.description") }}</div>
+              <div style="width: 84px">{{ $t("common.terms.actions") }}</div>
+            </div>
+
+            <draggable
+              v-model="benefits"
+              item-key="id"
+              handle=".drag-handle"
+              :animation="180"
+              ghost-class="benefit-row--ghost"
+              @end="onDragEnd"
+            >
+              <template #item="{ element }">
+                <div class="benefit-row row items-center q-px-sm q-mb-xs rounded-borders">
+                  <div class="drag-handle q-mr-xs">
+                    <q-icon name="mdi-drag-vertical" size="sm" color="grey-4"/>
+                  </div>
+                  <div style="width: 52px">
+                    <q-icon :name="element.icon" size="sm" color="primary" />
+                  </div>
+                  <div class="col text-body2">{{ element.title }}</div>
+                  <div class="col-5 text-caption text-grey-7">{{ element.description }}</div>
+                  <div style="width: 84px" class="row no-wrap q-gutter-xs">
+                    <q-btn
+                      outline
+                      style="width: 36px"
+                      @click="onEditBenefit(element)"
+                    >
+                      <q-icon name="mdi-file-edit-outline" />
+                    </q-btn>
+                    <q-btn
+                      outline
+                      style="width: 36px"
+                      @click="onDeleteBenefit(element.id)"
+                    >
+                      <q-icon name="mdi-trash-can-outline" />
+                    </q-btn>
+                  </div>
+                </div>
+              </template>
+            </draggable>
+
+            <div v-if="!benefits.length" class="text-center text-grey q-py-md text-body2">
+              {{ $t("http.errors.no_records_found") }}
+            </div>
+          </template>
+        </q-card-section>
+      </q-card>
+
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, useTemplateRef, onMounted, defineAsyncComponent } from "vue";
+import { useQuasar } from "quasar";
+import { useI18n } from "vue-i18n";
+import draggable from "vuedraggable";
+import {
+  getCompanySettings,
+  updateCompanySettings,
+  uploadCompanyHeroImage,
+  removeCompanyHeroImage,
+  getCompanyBenefits,
+  deleteCompanyBenefit,
+  reorderCompanyBenefits,
+} from "src/api/companySettings";
+import { useFormUpdateTracker } from "src/composables/useFormUpdateTracker";
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
+
+import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
+import DefaultInput from "src/components/defaults/DefaultInput.vue";
+
+const AddEditBenefitDialog = defineAsyncComponent(
+  () => import("./components/AddEditBenefitDialog.vue"),
+);
+
+const $q = useQuasar();
+const { t } = useI18n();
+const heroFormRef = useTemplateRef("heroFormRef");
+const imageInputRef = useTemplateRef("imageInputRef");
+
+const settings = ref({});
+const uploadingImage = ref(false);
+const removingImage = ref(false);
+const benefits = ref([]);
+const benefitsLoading = ref(true);
+
+const { form, getUpdatedFields, hasUpdatedFields } = useFormUpdateTracker({
+  hero_title: "",
+  hero_subtitle: "",
+  stat1_value: "",
+  stat1_label: "",
+  stat2_value: "",
+  stat2_label: "",
+  stat3_value: "",
+  stat3_label: "",
+  contact_email: "",
+  contact_phone: "",
+  contact_location: "",
+});
+
+const { loading: savingSettings, validationErrors, execute: submitForm } = useSubmitHandler({
+  formRef: heroFormRef,
+});
+
+
+const loadSettings = async () => {
+  try {
+    const data = await getCompanySettings();
+    if (data) {
+      settings.value = data;
+      Object.keys(form).forEach((key) => {
+        if (key in data) form[key] = data[key] ?? "";
+      });
+    }
+  } catch {
+    $q.notify({ type: "negative", message: t("common.ui.messages.error") });
+  }
+};
+
+const saveSettings = async () => {
+  await submitForm(async () => {
+    const updated = await updateCompanySettings(getUpdatedFields.value);
+    settings.value = { ...settings.value, ...updated };
+    $q.notify({ type: "positive", message: t("common.ui.messages.saved") });
+  });
+};
+
+const triggerImageUpload = () => {
+  imageInputRef.value.click();
+};
+
+const onImageSelected = async (event) => {
+  const file = event.target.files?.[0];
+  if (!file) return;
+  uploadingImage.value = true;
+  try {
+    const updated = await uploadCompanyHeroImage(file);
+    settings.value = { ...settings.value, ...updated };
+    $q.notify({ type: "positive", message: t("common.ui.messages.saved") });
+  } catch {
+    $q.notify({ type: "negative", message: t("common.ui.messages.error") });
+  } finally {
+    uploadingImage.value = false;
+    event.target.value = "";
+  }
+};
+
+const onRemoveImage = async () => {
+  removingImage.value = true;
+  try {
+    const updated = await removeCompanyHeroImage();
+    settings.value = { ...settings.value, ...updated };
+    $q.notify({ type: "positive", message: t("common.ui.messages.saved") });
+  } catch {
+    $q.notify({ type: "negative", message: t("common.ui.messages.error") });
+  } finally {
+    removingImage.value = false;
+  }
+};
+
+const loadBenefits = async () => {
+  benefitsLoading.value = true;
+  try {
+    benefits.value = await getCompanyBenefits() ?? [];
+  } catch {
+    $q.notify({ type: "negative", message: t("common.ui.messages.error") });
+  } finally {
+    benefitsLoading.value = false;
+  }
+};
+
+const onDragEnd = async () => {
+  const reordered = benefits.value.map((b, index) => ({ id: b.id, order: index + 1 }));
+  try {
+    await reorderCompanyBenefits(reordered);
+  } catch {
+    $q.notify({ type: "negative", message: t("common.ui.messages.error") });
+    await loadBenefits();
+  }
+};
+
+const onAddBenefit = () => {
+  $q.dialog({
+    component: AddEditBenefitDialog,
+    componentProps: {
+      title: () => `${t("common.actions.add")} ${t("company_settings.benefit.title_singular")}`,
+    },
+  }).onOk(() => loadBenefits());
+};
+
+const onEditBenefit = (row) => {
+  $q.dialog({
+    component: AddEditBenefitDialog,
+    componentProps: {
+      benefit: row,
+      title: () => `${t("common.actions.edit")} ${t("company_settings.benefit.title_singular")}`,
+    },
+  }).onOk(() => loadBenefits());
+};
+
+const onDeleteBenefit = (id) => {
+  $q.dialog({
+    title: t("common.ui.messages.confirm_action"),
+    message: t("common.ui.messages.are_you_sure_delete"),
+    ok: { color: "negative", label: t("common.actions.delete") },
+    cancel: { color: "primary", outline: true, label: t("common.actions.cancel") },
+  }).onOk(async () => {
+    try {
+      await deleteCompanyBenefit(id);
+      await loadBenefits();
+    } catch {
+      $q.notify({ type: "negative", message: t("common.ui.messages.error") });
+    }
+  });
+};
+
+onMounted(() => {
+  loadSettings();
+  loadBenefits();
+});
+</script>
+
+<style scoped>
+.benefit-row {
+  border: 1px solid rgba(0, 0, 0, 0.12);
+  min-height: 52px;
+  transition: background-color 0.15s;
+}
+
+.benefit-row:hover {
+  background-color: rgba(0, 0, 0, 0.03);
+}
+
+.benefit-row--ghost {
+  opacity: 0.4;
+  background: var(--q-primary);
+}
+
+.drag-handle {
+  touch-action: none;
+  cursor: grab;
+}
+
+.drag-handle:active {
+  cursor: grabbing;
+}
+
+.drag-handle:hover .q-icon {
+  color: var(--q-primary) !important;
+}
+</style>

+ 99 - 0
src/pages/company-settings/components/AddEditBenefitDialog.vue

@@ -0,0 +1,99 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="q-dialog-plugin overflow-hidden" style="width: 700px; max-width: 95vw">
+      <DefaultDialogHeader :title="title" @close="onDialogCancel" />
+      <q-form ref="formRef" @submit="onOKClick">
+        <q-card-section class="row q-col-gutter-sm q-pt-none">
+          <DefaultInput
+            v-model="form.title"
+            v-model:error="validationErrors.title"
+            :label="$t('company_settings.benefit.title')"
+            :rules="[inputRules.required]"
+            class="col-12"
+          />
+          <div class="col-12">
+            <MdiIconPicker
+              v-model="form.icon"
+              :label="$t('company_settings.benefit.icon')"
+              :rules="[inputRules.required]"
+            />
+          </div>
+          <DefaultInput
+            v-model="form.description"
+            v-model:error="validationErrors.description"
+            :label="$t('company_settings.benefit.description')"
+            type="textarea"
+            :rows="3"
+            class="col-12"
+          />
+        </q-card-section>
+        <q-card-actions>
+          <q-space />
+          <q-btn
+            outline
+            color="negative"
+            :label="$t('common.actions.cancel')"
+            @click="onDialogCancel"
+          />
+          <q-btn
+            color="primary"
+            :label="benefit ? $t('common.actions.save') : $t('common.actions.add')"
+            type="submit"
+            :loading="loading"
+            :disable="!hasUpdatedFields"
+          />
+        </q-card-actions>
+      </q-form>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { useTemplateRef } from "vue";
+import { useDialogPluginComponent } 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 { createCompanyBenefit, updateCompanyBenefit } from "src/api/companySettings";
+
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
+import DefaultInput from "src/components/defaults/DefaultInput.vue";
+import MdiIconPicker from "src/components/defaults/MdiIconPicker.vue";
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { benefit, title } = defineProps({
+  benefit: {
+    type: Object,
+    default: null,
+  },
+  title: {
+    type: Function,
+    default: () => useI18n().t("company_settings.benefit.dialog_title"),
+  },
+});
+
+const { inputRules } = useInputRules();
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+const formRef = useTemplateRef("formRef");
+
+const { form, getUpdatedFields, hasUpdatedFields } = useFormUpdateTracker({
+  title: benefit?.title ?? "",
+  icon: benefit?.icon ?? null,
+  description: benefit?.description ?? "",
+});
+
+const { loading, validationErrors, execute: submitForm } = useSubmitHandler({
+  onSuccess: () => onDialogOK(true),
+  formRef,
+});
+
+const onOKClick = async () => {
+  if (benefit) {
+    await submitForm(() => updateCompanyBenefit(benefit.id, getUpdatedFields.value));
+  } else {
+    await submitForm(() => createCompanyBenefit({ ...form }));
+  }
+};
+</script>

+ 26 - 0
src/router/routes/company-settings.route.js

@@ -0,0 +1,26 @@
+export default [
+  {
+    path: "/company-settings",
+    name: "CompanySettingsPage",
+    component: () => import("pages/company-settings/CompanySettingsPage.vue"),
+    meta: {
+      title: {
+        value: "ui.navigation.company_settings",
+        translate: true,
+      },
+      description: {
+        value: "page.company_settings.description",
+        translate: true,
+      },
+      requireAuth: true,
+      requiredPermission: "configuracoes-empresa",
+      breadcrumbs: [
+        {
+          name: "CompanySettingsPage",
+          title: "ui.navigation.company_settings",
+          translate: true,
+        },
+      ],
+    },
+  },
+];

+ 9 - 0
src/stores/navigation.js

@@ -89,6 +89,15 @@ export const navigationStore = defineStore("navigation", () => {
       permissionScope: "relatorio",
       allowedTypes: ["administrador"],
     },
+    {
+      type: "single",
+      title: "ui.navigation.company_settings",
+      name: "CompanySettingsPage",
+      icon: "mdi-cog-outline",
+      permission: false,
+      permissionScope: "configuracoes-empresa",
+      allowedTypes: ["administrador"],
+    },
 
     //ASSOCIADO
     {