Forráskód Böngészése

feat(dashboard): enhance attendance and ticket dialogs with dynamic data binding and improved user feedback

ebagabee 1 hete
szülő
commit
610fe9457a

+ 22 - 5
src/pages/dashboard/DashboardPage.vue

@@ -116,7 +116,7 @@
         <DashboardStatCard
           title="Frequência Média"
           icon="mdi-account-multiple-outline"
-          value="0%"
+          :value="`${averageAttendance}%`"
           clickable
           @click="openAverageAttendanceDialog"
         />
@@ -137,7 +137,7 @@
           title="Tickets Abertos"
           icon="mdi-calendar-outline"
           :value="openTickets"
-          subtitle="Estável"
+          :subtitle="`${openTickets} aguardando atendimento`"
           clickable
           @click="openTicketsDialog"
         />
@@ -239,6 +239,10 @@ const cancelledContracts = ref(0);
 const openTickets      = ref(0);
 const pendingTasks     = ref(0);
 const productStock     = ref(0);
+const averageAttendance = ref(0);
+const attendanceByUnit = ref([]);
+const products         = ref([]);
+const tickets          = ref([]);
 const generalRevenue   = ref({ value: 0, pending_count: 0 });
 const unitsSummary     = ref({
   total: 0,
@@ -356,15 +360,24 @@ const openCancelledContractsDialog = () => {
 };
 
 const openAverageAttendanceDialog = () => {
-  $q.dialog({ component: AverageAttendanceDialog });
+  $q.dialog({
+    component: AverageAttendanceDialog,
+    componentProps: { units: attendanceByUnit.value },
+  });
 };
 
 const openProductStockDialog = () => {
-  $q.dialog({ component: ProductStockDialog });
+  $q.dialog({
+    component: ProductStockDialog,
+    componentProps: { products: products.value },
+  });
 };
 
 const openTicketsDialog = () => {
-  $q.dialog({ component: OpenTicketsDialog });
+  $q.dialog({
+    component: OpenTicketsDialog,
+    componentProps: { tickets: tickets.value },
+  });
 };
 
 // Matrículas por Período — preenchido pela API (últimos 6 meses).
@@ -391,6 +404,10 @@ const fetchMetrics = async () => {
   openTickets.value    = dashboard.open_tickets ?? 0;
   pendingTasks.value   = dashboard.pending_tasks ?? 0;
   productStock.value   = dashboard.product_stock ?? 0;
+  averageAttendance.value = dashboard.attendance?.average ?? 0;
+  attendanceByUnit.value = dashboard.attendance?.units ?? [];
+  products.value       = dashboard.products ?? [];
+  tickets.value        = dashboard.tickets ?? [];
   generalRevenue.value = dashboard.general_revenue ?? { value: 0, pending_count: 0 };
   unitsSummary.value   = dashboard.units_summary ?? unitsSummary.value;
   birthdays.value      = dashboard.birthdays ?? [];

+ 24 - 18
src/pages/dashboard/components/AverageAttendanceDialog.vue

@@ -35,20 +35,20 @@
           <q-separator />
 
           <div>
-            <template
-              v-for="(item, index) in filteredUnidades"
-              :key="item.id"
-            >
+            <div v-if="filteredUnits.length === 0" class="text-grey-6 text-center q-pa-lg">
+              Nenhuma unidade encontrada.
+            </div>
+            <template v-for="(item, index) in filteredUnits" :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>
+                <span class="text-body2 text-dark">{{ item.unit }}</span>
+                <span class="text-body2 text-dark">{{ item.frequency }}%</span>
                 <q-badge
                   :label="item.status"
-                  :color="item.status === 'Alto' ? 'secondary' : 'orange'"
+                  :color="statusColor(item.status)"
                   style="
                     border-radius: 8px;
                     font-size: 11px;
@@ -57,7 +57,7 @@
                   "
                 />
               </div>
-              <q-separator v-if="index < filteredUnidades.length - 1" />
+              <q-separator v-if="index < filteredUnits.length - 1" />
             </template>
           </div>
         </q-card>
@@ -74,22 +74,28 @@ defineEmits([...useDialogPluginComponent.emits]);
 
 const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
 
+const props = defineProps({
+  units: {
+    type: Array,
+    default: () => [],
+  },
+});
+
 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 statusColor = (status) => {
+  if (status === "Alto") return "secondary";
+  if (status === "Sem dados") return "grey";
+  return "orange";
+};
 
-const filteredUnidades = computed(() => {
-  if (!search.value) return unidades;
+const filteredUnits = computed(() => {
+  if (!search.value) return props.units;
   const q = search.value.toLowerCase();
-  return unidades.filter(
+  return props.units.filter(
     (u) =>
-      u.unidade.toLowerCase().includes(q) ||
+      u.unit.toLowerCase().includes(q) ||
       u.status.toLowerCase().includes(q),
   );
 });

+ 38 - 53
src/pages/dashboard/components/OpenTicketsDialog.vue

@@ -32,47 +32,33 @@
             <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">
+            <div v-if="filteredTickets.length === 0" class="text-grey-6 text-center q-pa-lg">
+              Nenhum ticket aberto encontrado.
+            </div>
             <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>
+                <span class="text-body2 text-dark">{{ ticket.number }}</span>
                 <q-badge
-                  :label="ticket.prioridade"
-                  :color="prioridadeColor(ticket.prioridade)"
+                  :label="priorityLabel(ticket.priority)"
+                  :color="priorityColor(ticket.priority)"
                   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>
+                <span class="text-caption text-dark">{{ ticket.date }}</span>
+                <span class="text-caption text-dark">{{ ticket.sector }}</span>
                 <q-badge
-                  :label="ticket.status"
+                  :label="statusLabel(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>
@@ -91,48 +77,47 @@ defineEmits([...useDialogPluginComponent.emits]);
 
 const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
 
+const props = defineProps({
+  tickets: {
+    type: Array,
+    default: () => [],
+  },
+});
+
 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 priorityLabel = (priority) =>
+  ({ high: "Alta", medium: "Média", low: "Baixa" })[priority] ?? priority;
 
-const prioridadeColor = (p) => {
-  if (p === "Alta") return "negative";
-  if (p === "Média") return "warning";
+const priorityColor = (priority) => {
+  if (priority === "high") return "negative";
+  if (priority === "medium") return "warning";
   return "orange";
 };
 
 const statusColor = (s) => {
-  if (s === "Resolvido") return "positive";
-  if (s === "Em Análise") return "warning";
+  if (s === "resolved") return "positive";
+  if (s === "in_progress") return "warning";
   return "negative";
 };
 
+const statusLabel = (status) =>
+  ({
+    in_progress: "Em andamento",
+    resolved: "Resolvido",
+    unresolved: "Não resolvido",
+  })[status] ?? status;
+
 const filteredTickets = computed(() => {
-  if (!search.value) return tickets;
+  if (!search.value) return props.tickets;
   const q = search.value.toLowerCase();
-  return tickets.filter(
+  return props.tickets.filter(
     (t) =>
-      t.numero.toLowerCase().includes(q) ||
-      t.data.includes(q) ||
-      t.setor.toLowerCase().includes(q) ||
+      t.number.toLowerCase().includes(q) ||
+      t.title.toLowerCase().includes(q) ||
+      t.date.includes(q) ||
+      t.sector.toLowerCase().includes(q) ||
       t.status.toLowerCase().includes(q),
   );
 });
@@ -141,13 +126,13 @@ const filteredTickets = computed(() => {
 <style scoped>
 .list-header {
   display: grid;
-  grid-template-columns: 90px 100px 110px 1fr 140px 90px;
+  grid-template-columns: 90px 100px 110px 1fr 140px;
   align-items: center;
 }
 
 .list-row {
   display: grid;
-  grid-template-columns: 90px 100px 110px 1fr 140px 90px;
+  grid-template-columns: 90px 100px 110px 1fr 140px;
   align-items: center;
   cursor: pointer;
   transition: background-color 0.15s;

+ 15 - 29
src/pages/dashboard/components/ProductStockDialog.vue

@@ -19,16 +19,18 @@
             <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 v-if="props.products.length === 0" class="text-grey-6 text-center q-pa-lg">
+              Nenhum produto cadastrado.
+            </div>
+            <template v-for="(item, index) in props.products" :key="item.id">
               <div class="list-row q-px-md q-py-sm">
-                <span class="text-body2 text-dark">{{ item.nome }}</span>
+                <span class="text-body2 text-dark">{{ item.name }}</span>
                 <div class="row items-center" style="gap: 12px">
-                  <span class="text-body2 text-dark">{{ item.quantidade }}</span>
+                  <span class="text-body2 text-dark">{{ item.quantity }}</span>
                   <q-badge
                     :label="item.status"
                     :outline="item.status === 'Limitado'"
@@ -36,24 +38,8 @@
                     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" />
+              <q-separator v-if="index < props.products.length - 1" />
             </template>
           </q-card>
         </q-card>
@@ -69,12 +55,12 @@ 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 props = defineProps({
+  products: {
+    type: Array,
+    default: () => [],
+  },
+});
 
 const statusColor = (status) => {
   if (status === "Alto") return "primary";
@@ -86,13 +72,13 @@ const statusColor = (status) => {
 <style scoped>
 .list-header {
   display: grid;
-  grid-template-columns: 1fr 1fr 90px;
+  grid-template-columns: 1fr 1fr;
   align-items: center;
 }
 
 .list-row {
   display: grid;
-  grid-template-columns: 1fr 1fr 90px;
+  grid-template-columns: 1fr 1fr;
   align-items: center;
 }
 </style>