Эх сурвалжийг харах

Merge branch 'feat/GINC-GAB-adiciona-interatividade-dashboard-franqueadora' of Softpar/sfp_vue_franchisor_ginastica_cerebro into development

Gabriel Alves 1 сар өмнө
parent
commit
27fd100132

+ 47 - 8
src/components/charts/DashboardStatCard.vue

@@ -1,12 +1,16 @@
 <template>
-  <q-card class="stat-card">
+  <q-card
+    class="stat-card"
+    :class="{ 'stat-card--clickable': clickable }"
+    @click="clickable && emit('click')"
+  >
     <div class="flex justify-between items-start no-wrap">
-      <span class="text-subtitle2 text-dark">{{ title }}</span>
-      <q-icon :name="icon" size="22px" color="dark" />
+      <span class="text-subtitle2 card-title">{{ title }}</span>
+      <q-icon :name="icon" size="22px" class="card-icon" />
     </div>
 
     <div class="value-area">
-      <span class="text-h5 text-primary value-text">{{ value }}</span>
+      <span class="text-h5 value-text card-value">{{ value }}</span>
 
       <q-badge
         v-if="badge"
@@ -15,9 +19,11 @@
         :style="customStyle"
         class="stat-badge"
       />
-      <span v-else-if="subtitle" class="text-caption text-foreground">{{
-        subtitle
-      }}</span>
+      <span
+        v-else-if="subtitle"
+        class="text-caption text-foreground card-subtitle"
+        >{{ subtitle }}</span
+      >
     </div>
   </q-card>
 </template>
@@ -31,7 +37,10 @@ defineProps({
   badge: { type: String, default: "" },
   badgeColor: { type: String, default: "accent-1" },
   customStyle: { type: String, default: "padding: 4px" },
+  clickable: { type: Boolean, default: false },
 });
+
+const emit = defineEmits(["click"]);
 </script>
 
 <style scoped lang="scss">
@@ -45,6 +54,34 @@ defineProps({
   display: flex;
   flex-direction: column;
   justify-content: space-between;
+  transition:
+    background-color 0.2s,
+    color 0.2s;
+
+  .card-title {
+    color: #1a1a1a;
+    transition: color 0.2s;
+  }
+  .card-icon {
+    color: #1a1a1a;
+    transition: color 0.2s;
+  }
+  .card-value {
+    color: $primary;
+    transition: color 0.2s;
+  }
+  .card-subtitle {
+    transition: color 0.2s;
+  }
+}
+
+.stat-card--clickable {
+  cursor: pointer;
+
+  &:hover {
+    background-color: $terciary !important;
+    box-shadow: 0 0 0 1px $primary !important;
+  }
 }
 
 .value-area {
@@ -60,9 +97,11 @@ defineProps({
 
 .stat-badge {
   font-size: 12px;
-
   border-radius: 8px;
   width: fit-content;
   color: $primary;
+  transition:
+    background-color 0.2s,
+    color 0.2s;
 }
 </style>

+ 2 - 0
src/css/quasar.variables.scss

@@ -57,6 +57,8 @@ $colors: (
   "accent-1": #E38B37,
 
   "foreground": #505050,
+
+  "btn-badge": #554EF4,
 );
 
 @each $name, $color in $colors {

+ 45 - 0
src/pages/dashboard/DashboardPage.vue

@@ -166,18 +166,24 @@
           icon="mdi-account-multiple-outline"
           value="4.527"
           badge="3.200 ativos"
+          clickable
+          @click="openAlunosDialog"
         />
         <DashboardStatCard
           title="Contratos Congelados"
           icon="mdi-snowflake"
           value="57"
           subtitle="É hora de incentivar nossos alunos"
+          clickable
+          @click="openContratosCongeladosDialog"
         />
         <DashboardStatCard
           title="Contratos Cancelados"
           icon="mdi-cancel"
           value="57"
           subtitle="É hora de incentivar nossos alunos"
+          clickable
+          @click="openContratosCanceladosDialog"
         />
         <DashboardStatCard
           title="Receita Geral"
@@ -223,11 +229,15 @@
           badge="Alta"
           badge-color="approved"
           custom-style="padding: 6px 24px"
+          clickable
+          @click="openFrequenciaMediaDialog"
         />
         <DashboardStatCard
           title="Estoque Geral de Produtos"
           icon="mdi-currency-usd"
           value="56"
+          clickable
+          @click="openEstoqueProdutosDialog"
         />
         <DashboardStatCard
           title="Tarefas Pendentes"
@@ -240,6 +250,8 @@
           icon="mdi-calendar-outline"
           value="2"
           subtitle="Estável"
+          clickable
+          @click="openTicketsAbertoDialog"
         />
       </div>
     </div>
@@ -247,21 +259,54 @@
     <div v-else class="flex flex-center full-width q-pa-xl">
       <q-spinner color="primary" size="50px" />
     </div>
+
   </div>
 </template>
 
 <script setup>
 import { onMounted, ref, watch } from "vue";
+import { useQuasar } from "quasar";
 import { useI18n } from "vue-i18n";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import DashboardStatCard from "src/components/charts/DashboardStatCard.vue";
 import DashboardChartCard from "src/components/charts/DashboardChartCard.vue";
 import GroupedBarChart from "src/components/charts/normal/GroupedBarChart.vue";
 import AniversariantesCard from "src/components/charts/AniversariantesCard.vue";
+import AlunosAtivosDialog from "src/pages/dashboard/components/AlunosAtivosDialog.vue";
+import ContratosCongeladosDialog from "src/pages/dashboard/components/ContratosCongeladosDialog.vue";
+import ContratosCanceladosDialog from "src/pages/dashboard/components/ContratosCanceladosDialog.vue";
+import FrequenciaMediaDialog from "src/pages/dashboard/components/FrequenciaMediaDialog.vue";
+import EstoqueProdutosDialog from "src/pages/dashboard/components/EstoqueProdutosDialog.vue";
+import TicketsAbertoDialog from "src/pages/dashboard/components/TicketsAbertoDialog.vue";
 
 const { t } = useI18n();
 
+const $q = useQuasar();
 const isLoading = ref(true);
+
+const openAlunosDialog = () => {
+  $q.dialog({ component: AlunosAtivosDialog });
+};
+
+const openContratosCongeladosDialog = () => {
+  $q.dialog({ component: ContratosCongeladosDialog });
+};
+
+const openContratosCanceladosDialog = () => {
+  $q.dialog({ component: ContratosCanceladosDialog });
+};
+
+const openFrequenciaMediaDialog = () => {
+  $q.dialog({ component: FrequenciaMediaDialog });
+};
+
+const openEstoqueProdutosDialog = () => {
+  $q.dialog({ component: EstoqueProdutosDialog });
+};
+
+const openTicketsAbertoDialog = () => {
+  $q.dialog({ component: TicketsAbertoDialog });
+};
 const selectedUnit = ref(null);
 const defaultPeriod = ref("month");
 const defaultEventId = ref(1);

+ 182 - 0
src/pages/dashboard/components/AlunosAtivosDialog.vue

@@ -0,0 +1,182 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card style="width: 800px; max-width: 95vw; border-radius: 12px">
+      <q-bar class="bg-transparent q-px-md" style="height: 55px">
+        <span class="text-h6 text-dark" style="font-weight: 600"
+          >Alunos Ativos</span
+        >
+        <q-space />
+        <q-btn dense flat icon="mdi-close" @click="onDialogCancel" />
+      </q-bar>
+
+      <q-card-section class="q-pt-none q-pb-md q-px-md">
+        <q-card flat bordered style="border-radius: 8px">
+          <q-card-section class="q-pb-xs">
+            <div class="text-subtitle2 text-dark">Lista de alunos</div>
+            <div class="text-caption text-grey-6">
+              {{ alunos.length }} Alunos Cadastrados
+            </div>
+          </q-card-section>
+
+          <q-card-section class="q-pt-xs q-pb-sm">
+            <q-input
+              v-model="search"
+              dense
+              borderless
+              placeholder="Busque por status, nome, tel/whats"
+            >
+              <template #prepend>
+                <q-icon name="mdi-magnify" color="grey-6" />
+              </template>
+            </q-input>
+          </q-card-section>
+
+          <q-separator />
+
+          <div class="list-header q-px-md q-py-xs">
+            <span class="text-caption text-grey-7">Nome</span>
+            <span class="text-caption text-grey-7">Contato/Unidade</span>
+            <span class="text-caption text-grey-7">Contrato</span>
+          </div>
+
+          <q-separator />
+
+          <div style="max-height: 320px; overflow-y: auto">
+            <template v-for="(aluno, index) in filteredAlunos" :key="aluno.id">
+              <div
+                class="list-row q-px-md q-py-sm"
+                :class="{ 'row-selected': index === selectedIndex }"
+                @click="selectedIndex = index"
+              >
+                <span class="text-body2 text-dark">{{ aluno.nome }}</span>
+                <div class="column" style="gap: 2px">
+                  <span class="text-caption text-dark">{{
+                    aluno.telefone
+                  }}</span>
+                  <span class="text-caption text-grey-6">{{
+                    aluno.unidade
+                  }}</span>
+                </div>
+                <q-badge
+                  :label="aluno.status"
+                  :color="aluno.status === 'Ativo' ? 'btn-badge' : 'grey'"
+                  style="
+                    border-radius: 8px;
+                    font-size: 11px;
+                    padding: 4px;
+                    width: max-content;
+                    margin-left: 10px;
+                  "
+                />
+              </div>
+              <q-separator v-if="index < filteredAlunos.length - 1" />
+            </template>
+          </div>
+        </q-card>
+      </q-card-section>
+
+      <q-card-actions align="right" class="q-px-md q-pb-md q-pt-none">
+        <q-btn
+          label="EXPORTAR"
+          color="primary-2"
+          unelevated
+          style="border-radius: 8px; font-weight: 600; letter-spacing: 0.5px"
+        />
+      </q-card-actions>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, computed } from "vue";
+import { useDialogPluginComponent } from "quasar";
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
+
+const search = ref("");
+const selectedIndex = ref(0);
+
+const alunos = [
+  {
+    id: 1,
+    nome: "Heloisa Faria",
+    telefone: "(46)99999-9999",
+    unidade: "Unidade franco",
+    status: "Ativo",
+  },
+  {
+    id: 2,
+    nome: "Carol",
+    telefone: "(45)99999-9999",
+    unidade: "Arapongas-PR",
+    status: "Ativo",
+  },
+  {
+    id: 3,
+    nome: "Marcelo Souza",
+    telefone: "(45)98888-8888",
+    unidade: "Curitiba-PR",
+    status: "Ativo",
+  },
+  {
+    id: 4,
+    nome: "Ana Lúcia",
+    telefone: "(45)97777-7777",
+    unidade: "Londrina-PR",
+    status: "Ativo",
+  },
+  {
+    id: 5,
+    nome: "Ricardo Silva",
+    telefone: "(45)96666-6666",
+    unidade: "Ponta Grossa-PR",
+    status: "Ativo",
+  },
+  {
+    id: 6,
+    nome: "Juliana Costa",
+    telefone: "(45)95555-5555",
+    unidade: "Maringá-PR",
+    status: "Ativo",
+  },
+];
+
+const filteredAlunos = computed(() => {
+  if (!search.value) return alunos;
+  const q = search.value.toLowerCase();
+  return alunos.filter(
+    (a) =>
+      a.nome.toLowerCase().includes(q) ||
+      a.telefone.includes(q) ||
+      a.unidade.toLowerCase().includes(q) ||
+      a.status.toLowerCase().includes(q),
+  );
+});
+</script>
+
+<style scoped>
+.list-header {
+  display: grid;
+  grid-template-columns: 1fr 1fr 100px;
+  align-items: center;
+}
+
+.list-row {
+  display: grid;
+  grid-template-columns: 1fr 1fr 100px;
+  align-items: center;
+  align-content: center;
+  cursor: pointer;
+  transition: background-color 0.15s;
+}
+
+.list-row:hover {
+  background-color: #f5f5f5;
+}
+
+.row-selected {
+  background-color: #b2dfdb !important;
+}
+</style>

+ 178 - 0
src/pages/dashboard/components/ContratosCanceladosDialog.vue

@@ -0,0 +1,178 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card style="width: 800px; max-width: 95vw; border-radius: 12px">
+      <q-bar class="bg-transparent q-px-md" style="height: 55px">
+        <span class="text-h6 text-dark" style="font-weight: 600"
+          >Contratos Cancelados</span
+        >
+        <q-space />
+        <q-btn dense flat icon="mdi-close" @click="onDialogCancel" />
+      </q-bar>
+
+      <q-card-section class="q-pt-none q-pb-md q-px-md">
+        <q-card flat bordered style="border-radius: 8px">
+          <q-card-section class="q-pb-xs">
+            <div class="text-subtitle2 text-dark">Lista de alunos</div>
+            <div class="text-caption text-grey-6">
+              {{ contratos.length }} Alunos Cadastrados
+            </div>
+          </q-card-section>
+
+          <q-card-section class="q-pt-xs q-pb-sm">
+            <q-input
+              v-model="search"
+              dense
+              borderless
+              placeholder="Busque por status, nome, tel/whats"
+            >
+              <template #prepend>
+                <q-icon name="mdi-magnify" color="grey-6" />
+              </template>
+            </q-input>
+          </q-card-section>
+
+          <q-separator />
+
+          <div class="list-header q-px-md q-py-xs">
+            <span class="text-caption text-grey-7">Nome</span>
+            <span class="text-caption text-grey-7">Contato/Unidade</span>
+            <span class="text-caption text-grey-7">Contrato</span>
+          </div>
+
+          <q-separator />
+
+          <div style="max-height: 320px; overflow-y: auto">
+            <template
+              v-for="(contrato, index) in filteredContratos"
+              :key="contrato.id"
+            >
+              <div
+                class="list-row q-px-md q-py-sm"
+                :class="{ 'row-selected': index === selectedIndex }"
+                @click="selectedIndex = index"
+              >
+                <span class="text-body2 text-dark">{{ contrato.nome }}</span>
+                <div class="column" style="gap: 2px">
+                  <span class="text-caption text-dark">{{
+                    contrato.telefone
+                  }}</span>
+                  <span class="text-caption text-grey-6">{{
+                    contrato.unidade
+                  }}</span>
+                </div>
+                <q-badge
+                  label="Cancelados"
+                  color="negative"
+                  style="
+                    border-radius: 8px;
+                    font-size: 11px;
+                    padding: 4px;
+                    width: max-content;
+                    margin-left: 10px;
+                  "
+                />
+              </div>
+              <q-separator v-if="index < filteredContratos.length - 1" />
+            </template>
+          </div>
+        </q-card>
+      </q-card-section>
+
+      <q-card-actions align="right" class="q-px-md q-pb-md q-pt-none">
+        <q-btn
+          label="EXPORTAR"
+          color="primary-2"
+          unelevated
+          style="border-radius: 8px; font-weight: 600; letter-spacing: 0.5px"
+        />
+      </q-card-actions>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, computed } from "vue";
+import { useDialogPluginComponent } from "quasar";
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
+
+const search = ref("");
+const selectedIndex = ref(0);
+
+const contratos = [
+  {
+    id: 1,
+    nome: "Heloisa Faria",
+    telefone: "(45)99999-9999",
+    unidade: "Unidade franco",
+  },
+  {
+    id: 2,
+    nome: "Carol",
+    telefone: "(45)99999-9999",
+    unidade: "Arapongas-PR",
+  },
+  {
+    id: 3,
+    nome: "Marcelo Souza",
+    telefone: "(45)98888-8888",
+    unidade: "Curitiba-PR",
+  },
+  {
+    id: 4,
+    nome: "Ana Lúcia",
+    telefone: "(45)97777-7777",
+    unidade: "Londrina-PR",
+  },
+  {
+    id: 5,
+    nome: "Ricardo Silva",
+    telefone: "(45)96666-6666",
+    unidade: "Ponta Grossa-PR",
+  },
+  {
+    id: 6,
+    nome: "Juliana Costa",
+    telefone: "(45)95555-5555",
+    unidade: "Maringá-PR",
+  },
+];
+
+const filteredContratos = computed(() => {
+  if (!search.value) return contratos;
+  const q = search.value.toLowerCase();
+  return contratos.filter(
+    (c) =>
+      c.nome.toLowerCase().includes(q) ||
+      c.telefone.includes(q) ||
+      c.unidade.toLowerCase().includes(q),
+  );
+});
+</script>
+
+<style scoped>
+.list-header {
+  display: grid;
+  grid-template-columns: 1fr 1fr 100px;
+  align-items: center;
+}
+
+.list-row {
+  display: grid;
+  grid-template-columns: 1fr 1fr 100px;
+  align-items: center;
+  align-content: center;
+  cursor: pointer;
+  transition: background-color 0.15s;
+}
+
+.list-row:hover {
+  background-color: #f5f5f5;
+}
+
+.row-selected {
+  background-color: #b2dfdb !important;
+}
+</style>

+ 178 - 0
src/pages/dashboard/components/ContratosCongeladosDialog.vue

@@ -0,0 +1,178 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card style="width: 800px; max-width: 95vw; border-radius: 12px">
+      <q-bar class="bg-transparent q-px-md" style="height: 55px">
+        <span class="text-h6 text-dark" style="font-weight: 600"
+          >Contratos Congelados</span
+        >
+        <q-space />
+        <q-btn dense flat icon="mdi-close" @click="onDialogCancel" />
+      </q-bar>
+
+      <q-card-section class="q-pt-none q-pb-md q-px-md">
+        <q-card flat bordered style="border-radius: 8px">
+          <q-card-section class="q-pb-xs">
+            <div class="text-subtitle2 text-dark">Lista de alunos</div>
+            <div class="text-caption text-grey-6">
+              {{ contratos.length }} Alunos Cadastrados
+            </div>
+          </q-card-section>
+
+          <q-card-section class="q-pt-xs q-pb-sm">
+            <q-input
+              v-model="search"
+              dense
+              borderless
+              placeholder="Busque por status, nome, tel/whats"
+            >
+              <template #prepend>
+                <q-icon name="mdi-magnify" color="grey-6" />
+              </template>
+            </q-input>
+          </q-card-section>
+
+          <q-separator />
+
+          <div class="list-header q-px-md q-py-xs">
+            <span class="text-caption text-grey-7">Nome</span>
+            <span class="text-caption text-grey-7">Contato/Unidade</span>
+            <span class="text-caption text-grey-7">Contrato</span>
+          </div>
+
+          <q-separator />
+
+          <div style="max-height: 320px; overflow-y: auto">
+            <template
+              v-for="(contrato, index) in filteredContratos"
+              :key="contrato.id"
+            >
+              <div
+                class="list-row q-px-md q-py-sm"
+                :class="{ 'row-selected': index === selectedIndex }"
+                @click="selectedIndex = index"
+              >
+                <span class="text-body2 text-dark">{{ contrato.nome }}</span>
+                <div class="column" style="gap: 2px">
+                  <span class="text-caption text-dark">{{
+                    contrato.telefone
+                  }}</span>
+                  <span class="text-caption text-grey-6">{{
+                    contrato.unidade
+                  }}</span>
+                </div>
+                <q-badge
+                  label="Congelado"
+                  color="orange"
+                  style="
+                    border-radius: 8px;
+                    font-size: 11px;
+                    padding: 4px;
+                    width: max-content;
+                    margin-left: 10px;
+                  "
+                />
+              </div>
+              <q-separator v-if="index < filteredContratos.length - 1" />
+            </template>
+          </div>
+        </q-card>
+      </q-card-section>
+
+      <q-card-actions align="right" class="q-px-md q-pb-md q-pt-none">
+        <q-btn
+          label="EXPORTAR"
+          color="primary-2"
+          unelevated
+          style="border-radius: 8px; font-weight: 600; letter-spacing: 0.5px"
+        />
+      </q-card-actions>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, computed } from "vue";
+import { useDialogPluginComponent } from "quasar";
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
+
+const search = ref("");
+const selectedIndex = ref(0);
+
+const contratos = [
+  {
+    id: 1,
+    nome: "Heloisa Faria",
+    telefone: "(45)99999-9999",
+    unidade: "Unidade franco",
+  },
+  {
+    id: 2,
+    nome: "Carol",
+    telefone: "(45)99999-9999",
+    unidade: "Arapongas-PR",
+  },
+  {
+    id: 3,
+    nome: "Marcelo Souza",
+    telefone: "(45)98888-8888",
+    unidade: "Curitiba-PR",
+  },
+  {
+    id: 4,
+    nome: "Ana Lúcia",
+    telefone: "(45)97777-7777",
+    unidade: "Londrina-PR",
+  },
+  {
+    id: 5,
+    nome: "Ricardo Silva",
+    telefone: "(45)96666-6666",
+    unidade: "Ponta Grossa-PR",
+  },
+  {
+    id: 6,
+    nome: "Juliana Costa",
+    telefone: "(45)95555-5555",
+    unidade: "Maringá-PR",
+  },
+];
+
+const filteredContratos = computed(() => {
+  if (!search.value) return contratos;
+  const q = search.value.toLowerCase();
+  return contratos.filter(
+    (c) =>
+      c.nome.toLowerCase().includes(q) ||
+      c.telefone.includes(q) ||
+      c.unidade.toLowerCase().includes(q),
+  );
+});
+</script>
+
+<style scoped>
+.list-header {
+  display: grid;
+  grid-template-columns: 1fr 1fr 100px;
+  align-items: center;
+}
+
+.list-row {
+  display: grid;
+  grid-template-columns: 1fr 1fr 100px;
+  align-items: center;
+  align-content: center;
+  cursor: pointer;
+  transition: background-color 0.15s;
+}
+
+.list-row:hover {
+  background-color: #f5f5f5;
+}
+
+.row-selected {
+  background-color: #b2dfdb !important;
+}
+</style>

+ 98 - 0
src/pages/dashboard/components/EstoqueProdutosDialog.vue

@@ -0,0 +1,98 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card style="width: 800px; max-width: 95vw; border-radius: 12px">
+      <q-bar class="bg-transparent q-px-md" style="height: 55px">
+        <span class="text-h6 text-dark" style="font-weight: 600"
+          >Estoque de Produtos</span
+        >
+        <q-space />
+        <q-btn dense flat icon="mdi-close" @click="onDialogCancel" />
+      </q-bar>
+
+      <q-card-section class="q-pt-none q-pb-lg q-px-md">
+        <q-card flat bordered style="border-radius: 8px; padding: 20px">
+          <div class="text-subtitle1 text-dark q-mb-md" style="font-weight: 600">
+            Meu Estoque
+          </div>
+
+          <q-card flat bordered style="border-radius: 8px">
+            <div class="list-header q-px-md q-py-xs">
+              <span class="text-caption text-grey-7">Produto</span>
+              <span class="text-caption text-grey-7">Estoque</span>
+              <span class="text-caption text-grey-7">Ações</span>
+            </div>
+
+            <q-separator />
+
+            <template v-for="(item, index) in produtos" :key="item.id">
+              <div class="list-row q-px-md q-py-sm">
+                <span class="text-body2 text-dark">{{ item.nome }}</span>
+                <div class="row items-center" style="gap: 12px">
+                  <span class="text-body2 text-dark">{{ item.quantidade }}</span>
+                  <q-badge
+                    :label="item.status"
+                    :outline="item.status === 'Limitado'"
+                    :color="statusColor(item.status)"
+                    style="border-radius: 8px; font-size: 11px; padding: 4px 10px"
+                  />
+                </div>
+                <div class="row" style="gap: 6px">
+                  <q-btn
+                    flat
+                    dense
+                    icon="mdi-clipboard-outline"
+                    color="dark"
+                    style="border: 1px solid #ccc; border-radius: 6px; width: 34px; height: 34px"
+                  />
+                  <q-btn
+                    flat
+                    dense
+                    icon="mdi-delete-outline"
+                    color="dark"
+                    style="border: 1px solid #ccc; border-radius: 6px; width: 34px; height: 34px"
+                  />
+                </div>
+              </div>
+              <q-separator v-if="index < produtos.length - 1" />
+            </template>
+          </q-card>
+        </q-card>
+      </q-card-section>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { useDialogPluginComponent } from "quasar";
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
+
+const produtos = [
+  { id: 1, nome: "Box Viver", quantidade: 14, status: "Baixo" },
+  { id: 2, nome: "Brinquedo 1", quantidade: 100, status: "Alto" },
+  { id: 3, nome: "Brinquedo 2", quantidade: 100, status: "Alto" },
+  { id: 4, nome: "Novo Produto", quantidade: 30, status: "Limitado" },
+];
+
+const statusColor = (status) => {
+  if (status === "Alto") return "primary";
+  if (status === "Baixo") return "orange";
+  return "primary";
+};
+</script>
+
+<style scoped>
+.list-header {
+  display: grid;
+  grid-template-columns: 1fr 1fr 90px;
+  align-items: center;
+}
+
+.list-row {
+  display: grid;
+  grid-template-columns: 1fr 1fr 90px;
+  align-items: center;
+}
+</style>

+ 120 - 0
src/pages/dashboard/components/FrequenciaMediaDialog.vue

@@ -0,0 +1,120 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card style="width: 800px; max-width: 95vw; border-radius: 12px">
+      <q-bar class="bg-transparent q-px-md" style="height: 55px">
+        <span class="text-h6 text-dark" style="font-weight: 600"
+          >Frequência Média</span
+        >
+        <q-space />
+        <q-btn dense flat icon="mdi-close" @click="onDialogCancel" />
+      </q-bar>
+
+      <q-card-section class="q-pt-none q-pb-md q-px-md">
+        <q-card flat bordered style="border-radius: 8px">
+          <q-card-section class="q-pb-sm">
+            <q-input
+              v-model="search"
+              dense
+              borderless
+              placeholder="Busque por unidade, status"
+            >
+              <template #prepend>
+                <q-icon name="mdi-magnify" color="grey-6" />
+              </template>
+            </q-input>
+          </q-card-section>
+
+          <q-separator />
+
+          <div class="list-header q-px-md q-py-xs">
+            <span class="text-caption text-grey-7">Unidade</span>
+            <span class="text-caption text-grey-7">Frequência média</span>
+            <span class="text-caption text-grey-7">Status</span>
+          </div>
+
+          <q-separator />
+
+          <div>
+            <template
+              v-for="(item, index) in filteredUnidades"
+              :key="item.id"
+            >
+              <div
+                class="list-row q-px-md q-py-sm"
+                :class="{ 'row-selected': index === selectedIndex }"
+                @click="selectedIndex = index"
+              >
+                <span class="text-body2 text-dark">{{ item.unidade }}</span>
+                <span class="text-body2 text-dark">{{ item.frequencia }}</span>
+                <q-badge
+                  :label="item.status"
+                  :color="item.status === 'Alto' ? 'secondary' : 'orange'"
+                  style="
+                    border-radius: 8px;
+                    font-size: 11px;
+                    padding: 4px 8px;
+                    width: max-content;
+                  "
+                />
+              </div>
+              <q-separator v-if="index < filteredUnidades.length - 1" />
+            </template>
+          </div>
+        </q-card>
+      </q-card-section>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, computed } from "vue";
+import { useDialogPluginComponent } from "quasar";
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
+
+const search = ref("");
+const selectedIndex = ref(0);
+
+const unidades = [
+  { id: 1, unidade: "São Paulo", frequencia: "47%", status: "Baixa" },
+  { id: 2, unidade: "Maringá", frequencia: "20 %", status: "Baixa" },
+  { id: 3, unidade: "Curitiba", frequencia: "79 %", status: "Alto" },
+  { id: 4, unidade: "Foz Iguaçu", frequencia: "98%", status: "Alto" },
+];
+
+const filteredUnidades = computed(() => {
+  if (!search.value) return unidades;
+  const q = search.value.toLowerCase();
+  return unidades.filter(
+    (u) =>
+      u.unidade.toLowerCase().includes(q) ||
+      u.status.toLowerCase().includes(q),
+  );
+});
+</script>
+
+<style scoped>
+.list-header {
+  display: grid;
+  grid-template-columns: 1fr 1fr 80px;
+  align-items: center;
+}
+
+.list-row {
+  display: grid;
+  grid-template-columns: 1fr 1fr 80px;
+  align-items: center;
+  cursor: pointer;
+  transition: background-color 0.15s;
+}
+
+.list-row:hover {
+  background-color: #f5f5f5;
+}
+
+.row-selected {
+  background-color: #b2dfdb !important;
+}
+</style>

+ 163 - 0
src/pages/dashboard/components/TicketsAbertoDialog.vue

@@ -0,0 +1,163 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card style="width: 800px; max-width: 95vw; border-radius: 12px">
+      <q-bar class="bg-transparent q-px-md" style="height: 55px">
+        <span class="text-h6 text-dark" style="font-weight: 600"
+          >Tickets em Aberto</span
+        >
+        <q-space />
+        <q-btn dense flat icon="mdi-close" @click="onDialogCancel" />
+      </q-bar>
+
+      <q-card-section class="q-pt-none q-pb-md q-px-md">
+        <q-card flat bordered style="border-radius: 8px">
+          <q-card-section class="q-pb-sm">
+            <q-input
+              v-model="search"
+              dense
+              borderless
+              placeholder="Busque por ticket, nome, data , setor"
+            >
+              <template #prepend>
+                <q-icon name="mdi-magnify" color="grey-6" />
+              </template>
+            </q-input>
+          </q-card-section>
+
+          <q-separator />
+
+          <div class="list-header q-px-md q-py-xs">
+            <span class="text-caption text-grey-7">Ticket</span>
+            <span class="text-caption text-grey-7">Prioridade</span>
+            <span class="text-caption text-grey-7">Data</span>
+            <span class="text-caption text-grey-7">Setor</span>
+            <span class="text-caption text-grey-7">Status</span>
+            <span class="text-caption text-grey-7">Ações</span>
+          </div>
+
+          <q-separator />
+
+          <div style="max-height: 320px; overflow-y: auto">
+            <template v-for="(ticket, index) in filteredTickets" :key="ticket.id">
+              <div
+                class="list-row q-px-md q-py-sm"
+                :class="{ 'row-selected': index === selectedIndex }"
+                @click="selectedIndex = index"
+              >
+                <span class="text-body2 text-dark">{{ ticket.numero }}</span>
+                <q-badge
+                  :label="ticket.prioridade"
+                  :color="prioridadeColor(ticket.prioridade)"
+                  style="border-radius: 8px; font-size: 11px; padding: 4px 8px; width: max-content"
+                />
+                <span class="text-caption text-dark">{{ ticket.data }}</span>
+                <span class="text-caption text-dark">{{ ticket.setor }}</span>
+                <q-badge
+                  :label="ticket.status"
+                  :color="statusColor(ticket.status)"
+                  style="border-radius: 8px; font-size: 11px; padding: 4px 8px; width: max-content"
+                />
+                <div class="row" style="gap: 6px">
+                  <q-btn
+                    flat
+                    dense
+                    icon="mdi-clipboard-outline"
+                    color="dark"
+                    style="border: 1px solid #ccc; border-radius: 6px; width: 30px; height: 30px"
+                  />
+                  <q-btn
+                    flat
+                    dense
+                    icon="mdi-delete-outline"
+                    color="dark"
+                    style="border: 1px solid #ccc; border-radius: 6px; width: 30px; height: 30px"
+                  />
+                </div>
+              </div>
+              <q-separator v-if="index < filteredTickets.length - 1" />
+            </template>
+          </div>
+        </q-card>
+      </q-card-section>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, computed } from "vue";
+import { useDialogPluginComponent } from "quasar";
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
+
+const search = ref("");
+const selectedIndex = ref(0);
+
+const tickets = [
+  {
+    id: 1,
+    numero: "#12577",
+    prioridade: "Baixa",
+    data: "15/02/2026",
+    setor: "Financeiro",
+    status: "Não Resolvido",
+  },
+  {
+    id: 2,
+    numero: "#12578",
+    prioridade: "Alta",
+    data: "16/02/2026",
+    setor: "Comercial",
+    status: "Em Análise",
+  },
+];
+
+const prioridadeColor = (p) => {
+  if (p === "Alta") return "negative";
+  if (p === "Média") return "warning";
+  return "orange";
+};
+
+const statusColor = (s) => {
+  if (s === "Resolvido") return "positive";
+  if (s === "Em Análise") return "warning";
+  return "negative";
+};
+
+const filteredTickets = computed(() => {
+  if (!search.value) return tickets;
+  const q = search.value.toLowerCase();
+  return tickets.filter(
+    (t) =>
+      t.numero.toLowerCase().includes(q) ||
+      t.data.includes(q) ||
+      t.setor.toLowerCase().includes(q) ||
+      t.status.toLowerCase().includes(q),
+  );
+});
+</script>
+
+<style scoped>
+.list-header {
+  display: grid;
+  grid-template-columns: 90px 100px 110px 1fr 140px 90px;
+  align-items: center;
+}
+
+.list-row {
+  display: grid;
+  grid-template-columns: 90px 100px 110px 1fr 140px 90px;
+  align-items: center;
+  cursor: pointer;
+  transition: background-color 0.15s;
+}
+
+.list-row:hover {
+  background-color: #f5f5f5;
+}
+
+.row-selected {
+  background-color: #b2dfdb !important;
+}
+</style>