Просмотр исходного кода

feat: trocar selects de filtro por dropdown customizado compacto em Avaliações

leonardo 1 месяц назад
Родитель
Сommit
b3dcca9d8b
2 измененных файлов с 138 добавлено и 32 удалено
  1. 108 0
      src/components/base/BaseDropdown.vue
  2. 30 32
      src/views/atendimentos/AvaliacoesView.vue

+ 108 - 0
src/components/base/BaseDropdown.vue

@@ -0,0 +1,108 @@
+<template>
+  <div ref="raiz" class="relative shrink-0">
+    <button
+      type="button"
+      class="flex items-center gap-1 rounded-full border border-gray-200 px-3 py-1 text-xs font-medium text-gray-600 transition-colors hover:border-gray-300 dark:border-gray-700 dark:text-gray-300 dark:hover:border-gray-600"
+      :class="modelValue ? 'border-primary/40 bg-primary/10 text-primary dark:border-primary/40 dark:bg-primary/10 dark:text-primary' : ''"
+      :aria-expanded="aberto"
+      aria-haspopup="listbox"
+      @click="alternar"
+    >
+      <span class="max-w-[120px] truncate">{{ rotuloAtual }}</span>
+      <svg class="h-3 w-3 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+        <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
+      </svg>
+    </button>
+
+    <transition
+      enter-active-class="transition duration-150 ease-out"
+      enter-from-class="opacity-0 -translate-y-1 scale-95"
+      enter-to-class="opacity-100 translate-y-0 scale-100"
+      leave-active-class="transition duration-100 ease-in"
+      leave-from-class="opacity-100 translate-y-0 scale-100"
+      leave-to-class="opacity-0 -translate-y-1 scale-95"
+    >
+      <ul
+        v-if="aberto"
+        role="listbox"
+        class="absolute left-0 top-full z-20 mt-2 max-h-72 w-56 origin-top-left overflow-auto rounded-xl border border-gray-200 bg-background py-1 text-sm shadow-lg dark:border-gray-700"
+      >
+        <li v-for="opt in opcoesComPadrao" :key="opt.value">
+          <button
+            type="button"
+            role="option"
+            :aria-selected="opt.value === modelValue"
+            class="flex w-full items-center justify-between gap-2 px-3 py-1.5 text-left hover:bg-black/[0.04] dark:hover:bg-white/[0.06]"
+            :class="opt.value === modelValue ? 'font-medium text-primary' : 'text-gray-700 dark:text-gray-200'"
+            @click="selecionar(opt.value)"
+          >
+            <span class="truncate">{{ opt.label }}</span>
+            <svg
+              v-if="opt.value === modelValue"
+              class="h-3.5 w-3.5 shrink-0"
+              viewBox="0 0 24 24"
+              fill="none"
+              stroke="currentColor"
+              stroke-width="2"
+            >
+              <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
+            </svg>
+          </button>
+        </li>
+      </ul>
+    </transition>
+  </div>
+</template>
+
+<script setup>
+import { computed, onBeforeUnmount, onMounted, ref } from "vue";
+
+const props = defineProps({
+  modelValue: { type: String, default: "" },
+  rotulo: { type: String, required: true },
+  rotuloPadrao: { type: String, required: true },
+  opcoes: { type: Array, default: () => [] },
+});
+const emit = defineEmits(["update:modelValue", "change"]);
+
+const aberto = ref(false);
+const raiz = ref(null);
+
+const opcoesComPadrao = computed(() => [
+  { value: "", label: `${props.rotulo}: ${props.rotuloPadrao}` },
+  ...props.opcoes,
+]);
+
+const rotuloAtual = computed(() => {
+  const selecionada = opcoesComPadrao.value.find((o) => o.value === props.modelValue);
+  return selecionada ? selecionada.label : `${props.rotulo}: ${props.rotuloPadrao}`;
+});
+
+function alternar() {
+  aberto.value = !aberto.value;
+}
+function fechar() {
+  aberto.value = false;
+}
+function selecionar(valor) {
+  emit("update:modelValue", valor);
+  emit("change");
+  fechar();
+}
+function aoClicarFora(e) {
+  if (!aberto.value) return;
+  if (!raiz.value?.contains(e.target)) fechar();
+}
+function aoTeclar(e) {
+  if (e.key === "Escape" && aberto.value) fechar();
+}
+
+onMounted(() => {
+  document.addEventListener("click", aoClicarFora);
+  window.addEventListener("keydown", aoTeclar);
+});
+onBeforeUnmount(() => {
+  document.removeEventListener("click", aoClicarFora);
+  window.removeEventListener("keydown", aoTeclar);
+});
+</script>

+ 30 - 32
src/views/atendimentos/AvaliacoesView.vue

@@ -4,6 +4,7 @@ import { useRoute, useRouter } from "vue-router";
 import LayoutSistema from "../../layout/LayoutSistema.vue";
 import BaseBadge from "../../components/base/BaseBadge.vue";
 import BaseButton from "../../components/base/BaseButton.vue";
+import BaseDropdown from "../../components/base/BaseDropdown.vue";
 import {
   avaliarAtendimentosPendentes,
   estatisticasAvaliacoes,
@@ -20,6 +21,12 @@ import {
   sentimentoVariant
 } from "../../utils/avaliacoes.js";
 
+const opcoesResolucao = Object.entries({ ...resolvidoLabel, ...cancelamentoLabel }).map(([value, label]) => ({
+  value,
+  label
+}));
+const opcoesSentimento = Object.entries(sentimentoLabel).map(([value, label]) => ({ value, label }));
+
 const route = useRoute();
 const router = useRouter();
 
@@ -358,38 +365,29 @@ onBeforeUnmount(() => {
                 </svg>
               </button>
             </div>
-            <select
-              v-model="filtroSetor"
-              class="rounded-lg border border-gray-200 bg-background px-3 py-1.5 text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-primary/30 dark:border-gray-700 dark:text-gray-200 [&>option]:dark:bg-gray-800"
-              @change="aplicarFiltros"
-            >
-              <option value="">Setor: todos</option>
-              <option v-for="s in setores" :key="s.sigla" :value="s.sigla">{{ s.nome }} ({{ s.total }})</option>
-            </select>
-            <select
-              v-model="filtroResolvido"
-              class="rounded-lg border border-gray-200 bg-background px-3 py-1.5 text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-primary/30 dark:border-gray-700 dark:text-gray-200 [&>option]:dark:bg-gray-800"
-              @change="aplicarFiltros"
-            >
-              <option value="">Resolução: todas</option>
-              <option value="sim">Resolvido</option>
-              <option value="parcial">Parcial</option>
-              <option value="nao">Não resolvido</option>
-              <option value="indefinido">Indefinido</option>
-              <option value="cancelou">Cancelou</option>
-              <option value="ameacou">Quer cancelar</option>
-            </select>
-            <select
-              v-model="filtroSentimento"
-              class="rounded-lg border border-gray-200 bg-background px-3 py-1.5 text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-primary/30 dark:border-gray-700 dark:text-gray-200 [&>option]:dark:bg-gray-800"
-              @change="aplicarFiltros"
-            >
-              <option value="">Sentimento: todos</option>
-              <option value="positivo">Positivo</option>
-              <option value="neutro">Neutro</option>
-              <option value="negativo">Negativo</option>
-              <option value="indefinido">Indefinido</option>
-            </select>
+            <div class="flex shrink-0 items-center gap-2">
+              <BaseDropdown
+                v-model="filtroSetor"
+                rotulo="Setor"
+                rotulo-padrao="todos"
+                :opcoes="setores.map((s) => ({ value: s.sigla, label: `${s.nome} (${s.total})` }))"
+                @change="aplicarFiltros"
+              />
+              <BaseDropdown
+                v-model="filtroResolvido"
+                rotulo="Resolução"
+                rotulo-padrao="todas"
+                :opcoes="opcoesResolucao"
+                @change="aplicarFiltros"
+              />
+              <BaseDropdown
+                v-model="filtroSentimento"
+                rotulo="Sentimento"
+                rotulo-padrao="todos"
+                :opcoes="opcoesSentimento"
+                @change="aplicarFiltros"
+              />
+            </div>
             <BaseButton
               variant="secondary"
               size="sm"