leonardo 1 maand geleden
bovenliggende
commit
2c5572447e

+ 42 - 0
src/components/base/StatCard.vue

@@ -0,0 +1,42 @@
+<template>
+  <div
+    class="rounded-2xl border border-gray-200 bg-background/70 p-4 shadow-sm transition-colors dark:border-gray-700 dark:bg-background/20 hover:bg-black/[0.05] dark:hover:bg-white/[0.06]"
+  >
+    <div class="flex items-center gap-2.5">
+      <div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg" :class="iconWrapClass">
+        <slot name="icon" />
+      </div>
+      <div class="text-xs text-gray-500 dark:text-gray-400">{{ label }}</div>
+    </div>
+    <div v-if="value !== null" class="mt-2 text-2xl font-semibold text-gray-900 dark:text-gray-100">
+      {{ value }}<span v-if="suffix" class="text-sm font-normal text-gray-400">{{ suffix }}</span>
+    </div>
+    <div v-if="$slots.default" class="mt-1 text-xs text-gray-500 dark:text-gray-400" :class="{ 'mt-2': value === null }">
+      <slot />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed } from "vue";
+
+const props = defineProps({
+  label: { type: String, required: true },
+  value: { type: [String, Number], default: null },
+  suffix: { type: String, default: "" },
+  variant: {
+    type: String,
+    default: "default",
+    validator: (v) => ["default", "success", "danger", "info", "warning", "violet"].includes(v),
+  },
+});
+
+const iconWrapClass = computed(() => ({
+  default: "bg-gray-200/70 text-gray-600 dark:bg-white/10 dark:text-gray-300",
+  success: "bg-emerald-100 text-emerald-600 dark:bg-emerald-500/20 dark:text-emerald-300",
+  danger: "bg-red-100 text-red-600 dark:bg-red-500/20 dark:text-red-300",
+  info: "bg-blue-100 text-blue-600 dark:bg-blue-500/20 dark:text-blue-300",
+  warning: "bg-amber-100 text-amber-600 dark:bg-amber-500/20 dark:text-amber-300",
+  violet: "bg-violet-100 text-violet-600 dark:bg-violet-500/20 dark:text-violet-300",
+}[props.variant]));
+</script>

+ 40 - 38
src/views/atendimentos/AvaliacoesView.vue

@@ -5,6 +5,7 @@ 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 StatCard from "../../components/base/StatCard.vue";
 import {
   avaliarAtendimentosPendentes,
   estatisticasAvaliacoes,
@@ -295,44 +296,45 @@ onBeforeUnmount(() => {
         </section>
 
         <section v-if="stats" class="grid grid-cols-2 gap-3 md:grid-cols-5">
-          <div class="rounded-2xl border border-gray-200 bg-background/70 p-4 shadow-sm dark:border-gray-700 dark:bg-background/20">
-            <div class="text-xs text-gray-500 dark:text-gray-400">Score médio</div>
-            <div class="mt-1 text-2xl font-semibold text-gray-900 dark:text-gray-100">
-              {{ stats.scoreMedio ?? "—" }}<span class="text-sm font-normal text-gray-400"> / 10</span>
-            </div>
-          </div>
-          <div class="rounded-2xl border border-gray-200 bg-background/70 p-4 shadow-sm dark:border-gray-700 dark:bg-background/20">
-            <div class="text-xs text-gray-500 dark:text-gray-400">Avaliados</div>
-            <div class="mt-1 text-2xl font-semibold text-gray-900 dark:text-gray-100">{{ stats.avaliados }}</div>
-            <div class="text-xs text-gray-500 dark:text-gray-400">{{ stats.pendentes }} pendentes</div>
-          </div>
-          <div class="rounded-2xl border border-gray-200 bg-background/70 p-4 shadow-sm dark:border-gray-700 dark:bg-background/20">
-            <div class="text-xs text-gray-500 dark:text-gray-400">Problemas resolvidos</div>
-            <div class="mt-1 text-2xl font-semibold text-gray-900 dark:text-gray-100">
-              {{ pct(stats.resolvido?.sim ?? 0, stats.avaliados) }}
-            </div>
-            <div class="text-xs text-gray-500 dark:text-gray-400">
-              {{ stats.resolvido?.sim ?? 0 }} sim · {{ stats.resolvido?.parcial ?? 0 }} parcial · {{ stats.resolvido?.nao ?? 0 }} não
-            </div>
-          </div>
-          <div class="rounded-2xl border border-gray-200 bg-background/70 p-4 shadow-sm dark:border-gray-700 dark:bg-background/20">
-            <div class="text-xs text-gray-500 dark:text-gray-400">Clientes satisfeitos</div>
-            <div class="mt-1 text-2xl font-semibold text-gray-900 dark:text-gray-100">
-              {{ pct(stats.sentimento?.positivo ?? 0, stats.avaliados) }}
-            </div>
-            <div class="text-xs text-gray-500 dark:text-gray-400">
-              {{ stats.sentimento?.negativo ?? 0 }} saíram insatisfeitos
-            </div>
-          </div>
-          <div class="rounded-2xl border border-gray-200 bg-background/70 p-4 shadow-sm dark:border-gray-700 dark:bg-background/20">
-            <div class="text-xs text-gray-500 dark:text-gray-400">Cancelamentos</div>
-            <div class="mt-1 text-2xl font-semibold text-gray-900 dark:text-gray-100">
-              {{ pct(stats.cancelamento?.cancelou ?? 0, stats.avaliados) }}
-            </div>
-            <div class="text-xs text-gray-500 dark:text-gray-400">
-              {{ stats.cancelamento?.cancelou ?? 0 }} cancelaram · {{ stats.cancelamento?.ameacou ?? 0 }} ameaçaram cancelar
-            </div>
-          </div>
+          <StatCard label="Score médio" :value="stats.scoreMedio ?? '—'" suffix=" / 10" variant="info">
+            <template #icon>
+              <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.5c.16-.4.88-.4 1.04 0l1.98 4.88 5.25.4c.43.03.6.57.28.85l-4 3.46 1.22 5.13c.1.42-.36.75-.73.52L12 15.98l-4.52 2.76c-.37.23-.83-.1-.73-.52l1.22-5.13-4-3.46c-.33-.28-.15-.82.28-.85l5.25-.4 1.98-4.88Z" />
+              </svg>
+            </template>
+          </StatCard>
+          <StatCard label="Avaliados" :value="stats.avaliados" variant="default">
+            <template #icon>
+              <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
+              </svg>
+            </template>
+            {{ stats.pendentes }} pendentes
+          </StatCard>
+          <StatCard label="Problemas resolvidos" :value="pct(stats.resolvido?.sim ?? 0, stats.avaliados)" variant="success">
+            <template #icon>
+              <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
+                <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
+              </svg>
+            </template>
+            {{ stats.resolvido?.sim ?? 0 }} sim · {{ stats.resolvido?.parcial ?? 0 }} parcial · {{ stats.resolvido?.nao ?? 0 }} não
+          </StatCard>
+          <StatCard label="Clientes satisfeitos" :value="pct(stats.sentimento?.positivo ?? 0, stats.avaliados)" variant="violet">
+            <template #icon>
+              <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M9 15s1.125 1.5 3 1.5S15 15 15 15M9.75 9.75h.008v.008H9.75V9.75Zm4.5 0h.008v.008h-.008V9.75ZM21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
+              </svg>
+            </template>
+            {{ stats.sentimento?.negativo ?? 0 }} saíram insatisfeitos
+          </StatCard>
+          <StatCard label="Cancelamentos" :value="pct(stats.cancelamento?.cancelou ?? 0, stats.avaliados)" variant="danger">
+            <template #icon>
+              <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v4m0 4h.01M10.29 3.86 1.82 18a1.5 1.5 0 0 0 1.3 2.25h17.76a1.5 1.5 0 0 0 1.3-2.25L13.71 3.86a1.5 1.5 0 0 0-2.42 0Z" />
+              </svg>
+            </template>
+            {{ stats.cancelamento?.cancelou ?? 0 }} cancelaram · {{ stats.cancelamento?.ameacou ?? 0 }} ameaçaram cancelar
+          </StatCard>
         </section>
 
         <section class="rounded-2xl border border-gray-200 bg-background/70 shadow-sm dark:border-gray-700 dark:bg-background/20">

+ 24 - 19
src/views/atendimentos/NaoResolvidosView.vue

@@ -5,6 +5,7 @@ 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 StatCard from "../../components/base/StatCard.vue";
 import { baixarRelatorioNaoResolvidos, estatisticasAvaliacoes, listarAvaliacoes, listarSetoresAvaliacoes } from "../../api/atendimentos.js";
 import { useToast } from "../../composables/useToast.js";
 import {
@@ -281,31 +282,35 @@ onBeforeUnmount(() => {
         </section>
 
         <section v-if="stats" class="grid grid-cols-2 gap-3 md:grid-cols-3">
-          <div class="rounded-2xl border border-gray-200 bg-background/70 p-4 shadow-sm dark:border-gray-700 dark:bg-background/20">
-            <div class="text-xs text-gray-500 dark:text-gray-400">Não resolvidos no período</div>
-            <div class="mt-1 text-2xl font-semibold text-gray-900 dark:text-gray-100">
-              {{ stats.avaliados }}
-            </div>
-            <div class="text-xs text-gray-500 dark:text-gray-400">
-              {{ pct(stats.avaliados, statsGeral?.avaliados) }} dos {{ statsGeral?.avaliados ?? 0 }} avaliados
-            </div>
-          </div>
-          <div class="rounded-2xl border border-gray-200 bg-background/70 p-4 shadow-sm dark:border-gray-700 dark:bg-background/20">
-            <div class="text-xs text-gray-500 dark:text-gray-400">Score médio dos não resolvidos</div>
-            <div class="mt-1 text-2xl font-semibold text-gray-900 dark:text-gray-100">
-              {{ stats.scoreMedio ?? "—" }}<span class="text-sm font-normal text-gray-400"> / 10</span>
-            </div>
-          </div>
-          <div class="rounded-2xl border border-gray-200 bg-background/70 p-4 shadow-sm dark:border-gray-700 dark:bg-background/20">
-            <div class="text-xs text-gray-500 dark:text-gray-400">Por setor</div>
-            <div class="mt-1.5 space-y-1">
+          <StatCard label="Não resolvidos no período" :value="stats.avaliados" variant="danger">
+            <template #icon>
+              <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v4m0 4h.01M10.29 3.86 1.82 18a1.5 1.5 0 0 0 1.3 2.25h17.76a1.5 1.5 0 0 0 1.3-2.25L13.71 3.86a1.5 1.5 0 0 0-2.42 0Z" />
+              </svg>
+            </template>
+            {{ pct(stats.avaliados, statsGeral?.avaliados) }} dos {{ statsGeral?.avaliados ?? 0 }} avaliados
+          </StatCard>
+          <StatCard label="Score médio dos não resolvidos" :value="stats.scoreMedio ?? '—'" suffix=" / 10" variant="info">
+            <template #icon>
+              <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.5c.16-.4.88-.4 1.04 0l1.98 4.88 5.25.4c.43.03.6.57.28.85l-4 3.46 1.22 5.13c.1.42-.36.75-.73.52L12 15.98l-4.52 2.76c-.37.23-.83-.1-.73-.52l1.22-5.13-4-3.46c-.33-.28-.15-.82.28-.85l5.25-.4 1.98-4.88Z" />
+              </svg>
+            </template>
+          </StatCard>
+          <StatCard label="Por setor" variant="default">
+            <template #icon>
+              <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M3 13.5h4.5V21H3v-7.5Zm6.75-6H14.25V21H9.75V7.5Zm6.75 3H21V21h-4.5v-10.5Z" />
+              </svg>
+            </template>
+            <div class="space-y-1">
               <div v-if="setoresOrdenados.length === 0" class="text-sm text-gray-400 dark:text-gray-500">—</div>
               <div v-for="[sigla, qtd] in setoresOrdenados" :key="sigla" class="flex items-center justify-between text-xs">
                 <span class="text-gray-600 dark:text-gray-300">{{ sigla }}</span>
                 <span class="font-medium text-gray-900 dark:text-gray-100">{{ qtd }}</span>
               </div>
             </div>
-          </div>
+          </StatCard>
         </section>
 
         <section class="rounded-2xl border border-gray-200 bg-background/70 shadow-sm dark:border-gray-700 dark:bg-background/20">