|
@@ -1,126 +1,181 @@
|
|
|
<template>
|
|
<template>
|
|
|
<div>
|
|
<div>
|
|
|
- <DefaultHeaderPage show-filter-icon title="Dashboard" />
|
|
|
|
|
|
|
+ <DefaultHeaderPage
|
|
|
|
|
+ title="Dashboard"
|
|
|
|
|
+ :filter-open="showFilter"
|
|
|
|
|
+ @show-filter="showFilter = !showFilter"
|
|
|
|
|
+ />
|
|
|
|
|
|
|
|
<div class="q-pa-sm">
|
|
<div class="q-pa-sm">
|
|
|
<div class="filter-row">
|
|
<div class="filter-row">
|
|
|
- <q-select
|
|
|
|
|
- v-model="selectedPeriod"
|
|
|
|
|
- :options="periodOptions"
|
|
|
|
|
- option-value="value"
|
|
|
|
|
- option-label="label"
|
|
|
|
|
- emit-value
|
|
|
|
|
- map-options
|
|
|
|
|
- dense
|
|
|
|
|
- label="Selecione o Período"
|
|
|
|
|
- color="secondary"
|
|
|
|
|
- class="filter-item"
|
|
|
|
|
- hide-dropdown-icon
|
|
|
|
|
- >
|
|
|
|
|
- <template #append>
|
|
|
|
|
- <q-icon name="mdi-chevron-down" color="secondary" />
|
|
|
|
|
- </template>
|
|
|
|
|
- </q-select>
|
|
|
|
|
-
|
|
|
|
|
- <template v-if="selectedPeriod === 'custom'">
|
|
|
|
|
- <q-input
|
|
|
|
|
- v-model="startDate"
|
|
|
|
|
|
|
+ <template v-if="showFilter">
|
|
|
|
|
+ <!-- Unidade: ao selecionar, limpa o grupo -->
|
|
|
|
|
+ <UnitSelect
|
|
|
|
|
+ v-model="selectedUnit"
|
|
|
dense
|
|
dense
|
|
|
- label="Data Inicial"
|
|
|
|
|
- mask="##/##/####"
|
|
|
|
|
- placeholder="DD/MM/AAAA"
|
|
|
|
|
|
|
+ label="Unidade"
|
|
|
|
|
+ class="filter-item"
|
|
|
color="secondary"
|
|
color="secondary"
|
|
|
|
|
+ @update:model-value="onUnitSelected"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Grupo: ao selecionar, limpa a unidade -->
|
|
|
|
|
+ <GroupSelect
|
|
|
|
|
+ v-model="selectedGroup"
|
|
|
|
|
+ dense
|
|
|
|
|
+ label="Grupo"
|
|
|
class="filter-item"
|
|
class="filter-item"
|
|
|
- >
|
|
|
|
|
- <template #append>
|
|
|
|
|
- <q-icon
|
|
|
|
|
- name="mdi-calendar"
|
|
|
|
|
- color="secondary"
|
|
|
|
|
- class="cursor-pointer"
|
|
|
|
|
- >
|
|
|
|
|
- <q-popup-proxy
|
|
|
|
|
- cover
|
|
|
|
|
- transition-show="scale"
|
|
|
|
|
- transition-hide="scale"
|
|
|
|
|
- >
|
|
|
|
|
- <q-date
|
|
|
|
|
- v-model="startDate"
|
|
|
|
|
- mask="DD/MM/YYYY"
|
|
|
|
|
- color="secondary"
|
|
|
|
|
- >
|
|
|
|
|
- <div class="row items-center justify-end">
|
|
|
|
|
- <q-btn v-close-popup label="OK" color="secondary" flat />
|
|
|
|
|
- </div>
|
|
|
|
|
- </q-date>
|
|
|
|
|
- </q-popup-proxy>
|
|
|
|
|
- </q-icon>
|
|
|
|
|
- </template>
|
|
|
|
|
- </q-input>
|
|
|
|
|
-
|
|
|
|
|
- <q-input
|
|
|
|
|
- v-model="endDate"
|
|
|
|
|
|
|
+ color="secondary"
|
|
|
|
|
+ @update:model-value="onGroupSelected"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <DefaultSelect
|
|
|
|
|
+ v-model="selectedPeriod"
|
|
|
|
|
+ :options="periodOptions"
|
|
|
|
|
+ option-value="value"
|
|
|
|
|
+ option-label="label"
|
|
|
|
|
+ emit-value
|
|
|
|
|
+ map-options
|
|
|
dense
|
|
dense
|
|
|
- label="Data Final"
|
|
|
|
|
- mask="##/##/####"
|
|
|
|
|
- placeholder="DD/MM/AAAA"
|
|
|
|
|
|
|
+ label="Selecione o Período"
|
|
|
color="secondary"
|
|
color="secondary"
|
|
|
class="filter-item"
|
|
class="filter-item"
|
|
|
- >
|
|
|
|
|
- <template #append>
|
|
|
|
|
- <q-icon
|
|
|
|
|
- name="mdi-calendar"
|
|
|
|
|
- color="secondary"
|
|
|
|
|
- class="cursor-pointer"
|
|
|
|
|
- >
|
|
|
|
|
- <q-popup-proxy
|
|
|
|
|
- cover
|
|
|
|
|
- transition-show="scale"
|
|
|
|
|
- transition-hide="scale"
|
|
|
|
|
- >
|
|
|
|
|
- <q-date v-model="endDate" mask="DD/MM/YYYY" color="secondary">
|
|
|
|
|
- <div class="row items-center justify-end">
|
|
|
|
|
- <q-btn v-close-popup label="OK" color="secondary" flat />
|
|
|
|
|
- </div>
|
|
|
|
|
- </q-date>
|
|
|
|
|
- </q-popup-proxy>
|
|
|
|
|
- </q-icon>
|
|
|
|
|
- </template>
|
|
|
|
|
- </q-input>
|
|
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <template v-if="selectedPeriod === 'custom'">
|
|
|
|
|
+ <DefaultInputDatePicker
|
|
|
|
|
+ v-model="startDate"
|
|
|
|
|
+ v-model:untreated-date="startDateIso"
|
|
|
|
|
+ dense
|
|
|
|
|
+ label="Data Inicial"
|
|
|
|
|
+ color="secondary"
|
|
|
|
|
+ class="filter-item"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <DefaultInputDatePicker
|
|
|
|
|
+ v-model="endDate"
|
|
|
|
|
+ v-model:untreated-date="endDateIso"
|
|
|
|
|
+ dense
|
|
|
|
|
+ label="Data Final"
|
|
|
|
|
+ color="secondary"
|
|
|
|
|
+ class="filter-item"
|
|
|
|
|
+ />
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <q-btn
|
|
|
|
|
+ outline
|
|
|
|
|
+ color="secondary"
|
|
|
|
|
+ icon="mdi-filter-remove-outline"
|
|
|
|
|
+ label="Limpar filtros"
|
|
|
|
|
+ class="clear-filters-button"
|
|
|
|
|
+ @click="clearFilters"
|
|
|
|
|
+ />
|
|
|
</template>
|
|
</template>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Chip de contexto ativo -->
|
|
|
|
|
+ <div v-if="showFilter && filterLabel" class="q-mt-xs q-ml-xs">
|
|
|
|
|
+ <q-chip
|
|
|
|
|
+ dense
|
|
|
|
|
+ color="secondary"
|
|
|
|
|
+ text-color="white"
|
|
|
|
|
+ icon="mdi-filter"
|
|
|
|
|
+ :label="filterLabel"
|
|
|
|
|
+ removable
|
|
|
|
|
+ @remove="clearUnitFilters"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div v-if="!isLoading" class="column gap q-pa-sm">
|
|
<div v-if="!isLoading" class="column gap q-pa-sm">
|
|
|
|
|
+ <div class="stat-cards-row">
|
|
|
|
|
+ <DashboardStatCard
|
|
|
|
|
+ title="Total de Unidades"
|
|
|
|
|
+ icon="mdi-store-outline"
|
|
|
|
|
+ :value="unitsSummary.total"
|
|
|
|
|
+ subtitle="Unidades cadastradas"
|
|
|
|
|
+ />
|
|
|
|
|
+ <DashboardStatCard
|
|
|
|
|
+ title="Faturamento Recebido pelas Unidades"
|
|
|
|
|
+ icon="mdi-cash-multiple"
|
|
|
|
|
+ :value="formatToBRLCurrency(unitsSummary.revenue)"
|
|
|
|
|
+ :subtitle="revenuePeriodLabel"
|
|
|
|
|
+ />
|
|
|
|
|
+ <DashboardStatCard
|
|
|
|
|
+ title="Faturamento a Receber pelas Unidades"
|
|
|
|
|
+ icon="mdi-cash-clock"
|
|
|
|
|
+ :value="formatToBRLCurrency(unitsSummary.missing_receivables)"
|
|
|
|
|
+ :subtitle="receivablesPeriodLabel"
|
|
|
|
|
+ />
|
|
|
|
|
+ <DashboardStatCard
|
|
|
|
|
+ title="Contratos das Unidades"
|
|
|
|
|
+ icon="mdi-file-document-check-outline"
|
|
|
|
|
+ :value="`${unitsSummary.active_contracts} vigentes`"
|
|
|
|
|
+ :subtitle="`${unitsSummary.without_active_contract} sem contrato vigente`"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
<div class="stat-cards-row">
|
|
<div class="stat-cards-row">
|
|
|
<DashboardStatCard
|
|
<DashboardStatCard
|
|
|
title="Total alunos (contratos ativos)"
|
|
title="Total alunos (contratos ativos)"
|
|
|
icon="mdi-account-multiple-outline"
|
|
icon="mdi-account-multiple-outline"
|
|
|
- value="4.527"
|
|
|
|
|
- badge="3.200 ativos"
|
|
|
|
|
|
|
+ :value="totalStudents"
|
|
|
|
|
+ :badge="`${activeContracts} ativos`"
|
|
|
clickable
|
|
clickable
|
|
|
- @click="openAlunosDialog"
|
|
|
|
|
|
|
+ @click="openActiveStudentsDialog"
|
|
|
/>
|
|
/>
|
|
|
<DashboardStatCard
|
|
<DashboardStatCard
|
|
|
title="Contratos Congelados"
|
|
title="Contratos Congelados"
|
|
|
icon="mdi-snowflake"
|
|
icon="mdi-snowflake"
|
|
|
- value="57"
|
|
|
|
|
|
|
+ :value="frozenContracts"
|
|
|
subtitle="É hora de incentivar nossos alunos"
|
|
subtitle="É hora de incentivar nossos alunos"
|
|
|
clickable
|
|
clickable
|
|
|
- @click="openContratosCongeladosDialog"
|
|
|
|
|
|
|
+ @click="openFrozenContractsDialog"
|
|
|
/>
|
|
/>
|
|
|
<DashboardStatCard
|
|
<DashboardStatCard
|
|
|
title="Contratos Cancelados"
|
|
title="Contratos Cancelados"
|
|
|
icon="mdi-cancel"
|
|
icon="mdi-cancel"
|
|
|
- value="57"
|
|
|
|
|
|
|
+ :value="cancelledContracts"
|
|
|
subtitle="É hora de incentivar nossos alunos"
|
|
subtitle="É hora de incentivar nossos alunos"
|
|
|
clickable
|
|
clickable
|
|
|
- @click="openContratosCanceladosDialog"
|
|
|
|
|
|
|
+ @click="openCancelledContractsDialog"
|
|
|
/>
|
|
/>
|
|
|
<DashboardStatCard
|
|
<DashboardStatCard
|
|
|
title="Receita Geral"
|
|
title="Receita Geral"
|
|
|
icon="mdi-currency-usd"
|
|
icon="mdi-currency-usd"
|
|
|
- value="R$ 51.548,80"
|
|
|
|
|
- subtitle="0 pagamentos pendentes"
|
|
|
|
|
|
|
+ :value="formatToBRLCurrency(generalRevenue.value)"
|
|
|
|
|
+ :subtitle="`${generalRevenue.pending_count} pagamentos pendentes`"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="stat-cards-row">
|
|
|
|
|
+ <DashboardStatCard
|
|
|
|
|
+ title="Frequência Média"
|
|
|
|
|
+ icon="mdi-account-multiple-outline"
|
|
|
|
|
+ :value="`${averageAttendance}%`"
|
|
|
|
|
+ clickable
|
|
|
|
|
+ @click="openAverageAttendanceDialog"
|
|
|
|
|
+ />
|
|
|
|
|
+ <DashboardStatCard
|
|
|
|
|
+ title="Estoque Geral de Produtos"
|
|
|
|
|
+ icon="mdi-currency-usd"
|
|
|
|
|
+ :value="productStock"
|
|
|
|
|
+ clickable
|
|
|
|
|
+ @click="openProductStockDialog"
|
|
|
|
|
+ />
|
|
|
|
|
+ <DashboardStatCard
|
|
|
|
|
+ title="Tarefas Pendentes"
|
|
|
|
|
+ icon="mdi-draw"
|
|
|
|
|
+ :value="pendingTasks"
|
|
|
|
|
+ subtitle="Não deixe para amanhã"
|
|
|
|
|
+ />
|
|
|
|
|
+ <DashboardStatCard
|
|
|
|
|
+ title="Tickets Abertos"
|
|
|
|
|
+ icon="mdi-calendar-outline"
|
|
|
|
|
+ :value="openTickets"
|
|
|
|
|
+ :subtitle="`${openTickets} aguardando atendimento`"
|
|
|
|
|
+ clickable
|
|
|
|
|
+ @click="openTicketsDialog"
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -149,41 +204,8 @@
|
|
|
/>
|
|
/>
|
|
|
</DashboardChartCard>
|
|
</DashboardChartCard>
|
|
|
|
|
|
|
|
- <AniversariantesCard :people="aniversariantes" />
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="stat-cards-row">
|
|
|
|
|
- <DashboardStatCard
|
|
|
|
|
- title="Frequência Média"
|
|
|
|
|
- icon="mdi-account-multiple-outline"
|
|
|
|
|
- value="87%"
|
|
|
|
|
- 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"
|
|
|
|
|
- icon="mdi-draw"
|
|
|
|
|
- value="4"
|
|
|
|
|
- subtitle="Não deixe para amanhã"
|
|
|
|
|
- />
|
|
|
|
|
- <DashboardStatCard
|
|
|
|
|
- title="Tickets Abertos"
|
|
|
|
|
- icon="mdi-calendar-outline"
|
|
|
|
|
- value="2"
|
|
|
|
|
- subtitle="Estável"
|
|
|
|
|
- clickable
|
|
|
|
|
- @click="openTicketsAbertoDialog"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <HolidaysCard />
|
|
|
|
|
+ <BirthdaysCard :people="birthdays" />
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -194,111 +216,265 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
-import { onMounted, ref, watch } from "vue";
|
|
|
|
|
|
|
+import { onMounted, ref, computed, watch } from "vue";
|
|
|
import { useQuasar } from "quasar";
|
|
import { useQuasar } from "quasar";
|
|
|
import { useI18n } from "vue-i18n";
|
|
import { useI18n } from "vue-i18n";
|
|
|
import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
|
|
import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
|
|
|
|
|
+import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
|
|
|
|
|
+import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
|
|
|
|
|
+import UnitSelect from "src/components/selects/UnitSelect.vue";
|
|
|
|
|
+import GroupSelect from "src/components/selects/GroupSelect.vue";
|
|
|
import DashboardStatCard from "src/components/charts/DashboardStatCard.vue";
|
|
import DashboardStatCard from "src/components/charts/DashboardStatCard.vue";
|
|
|
import DashboardChartCard from "src/components/charts/DashboardChartCard.vue";
|
|
import DashboardChartCard from "src/components/charts/DashboardChartCard.vue";
|
|
|
import GroupedBarChart from "src/components/charts/normal/GroupedBarChart.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();
|
|
|
|
|
-
|
|
|
|
|
|
|
+import BirthdaysCard from "src/components/charts/BirthdaysCard.vue";
|
|
|
|
|
+import HolidaysCard from "src/components/charts/HolidaysCard.vue";
|
|
|
|
|
+import ActiveStudentsDialog from "src/pages/dashboard/components/ActiveStudentsDialog.vue";
|
|
|
|
|
+import FrozenContractsDialog from "src/pages/dashboard/components/FrozenContractsDialog.vue";
|
|
|
|
|
+import CancelledContractsDialog from "src/pages/dashboard/components/CancelledContractsDialog.vue";
|
|
|
|
|
+import AverageAttendanceDialog from "src/pages/dashboard/components/AverageAttendanceDialog.vue";
|
|
|
|
|
+import ProductStockDialog from "src/pages/dashboard/components/ProductStockDialog.vue";
|
|
|
|
|
+import OpenTicketsDialog from "src/pages/dashboard/components/OpenTicketsDialog.vue";
|
|
|
|
|
+import { getStudentSummaryFranchisor } from "src/api/student";
|
|
|
|
|
+import { getFranchisorContractSummary } from "src/api/student_contract";
|
|
|
|
|
+import { getFranchisorDashboard } from "src/api/dashboard";
|
|
|
|
|
+import { formatToBRLCurrency } from "src/helpers/utils";
|
|
|
|
|
+
|
|
|
|
|
+useI18n(); // i18n instance kept for future use
|
|
|
const $q = useQuasar();
|
|
const $q = useQuasar();
|
|
|
|
|
|
|
|
|
|
+// ─── Loading state ───────────────────────────────────────────
|
|
|
const isLoading = ref(true);
|
|
const isLoading = ref(true);
|
|
|
|
|
|
|
|
-const openAlunosDialog = () => {
|
|
|
|
|
- $q.dialog({ component: AlunosAtivosDialog });
|
|
|
|
|
|
|
+// ─── Metric counters ─────────────────────────────────────────
|
|
|
|
|
+const totalStudents = ref(0);
|
|
|
|
|
+const activeContracts = ref(0);
|
|
|
|
|
+const frozenContracts = ref(0);
|
|
|
|
|
+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,
|
|
|
|
|
+ revenue: 0,
|
|
|
|
|
+ missing_receivables: 0,
|
|
|
|
|
+ active_contracts: 0,
|
|
|
|
|
+ without_active_contract: 0,
|
|
|
|
|
+});
|
|
|
|
|
+const birthdays = ref([]);
|
|
|
|
|
+
|
|
|
|
|
+// ─── Filter state ────────────────────────────────────────────
|
|
|
|
|
+const showFilter = ref(false);
|
|
|
|
|
+const selectedUnit = ref(null); // { label, value } | null
|
|
|
|
|
+const selectedGroup = ref(null); // { label, value, unit_ids } | null
|
|
|
|
|
+const selectedPeriod = ref("month");
|
|
|
|
|
+const startDate = ref("");
|
|
|
|
|
+const endDate = ref("");
|
|
|
|
|
+const startDateIso = ref("");
|
|
|
|
|
+const endDateIso = ref("");
|
|
|
|
|
+
|
|
|
|
|
+const periodOptions = [
|
|
|
|
|
+ { label: "Hoje", value: "today" },
|
|
|
|
|
+ { label: "Esta semana", value: "week" },
|
|
|
|
|
+ { label: "Este mês", value: "month" },
|
|
|
|
|
+ { label: "Este ano", value: "year" },
|
|
|
|
|
+ { label: "Personalizado", value: "custom" },
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+const toIsoDate = (date) => {
|
|
|
|
|
+ const year = date.getFullYear();
|
|
|
|
|
+ const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
|
|
|
+ const day = String(date.getDate()).padStart(2, "0");
|
|
|
|
|
+ return `${year}-${month}-${day}`;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const openContratosCongeladosDialog = () => {
|
|
|
|
|
- $q.dialog({ component: ContratosCongeladosDialog });
|
|
|
|
|
|
|
+const revenuePeriod = computed(() => {
|
|
|
|
|
+ const now = new Date();
|
|
|
|
|
+ let start;
|
|
|
|
|
+ let end = now;
|
|
|
|
|
+
|
|
|
|
|
+ if (selectedPeriod.value === "today") {
|
|
|
|
|
+ start = now;
|
|
|
|
|
+ } else if (selectedPeriod.value === "week") {
|
|
|
|
|
+ start = new Date(now);
|
|
|
|
|
+ const day = start.getDay() || 7;
|
|
|
|
|
+ start.setDate(start.getDate() - day + 1);
|
|
|
|
|
+ } else if (selectedPeriod.value === "year") {
|
|
|
|
|
+ start = new Date(now.getFullYear(), 0, 1);
|
|
|
|
|
+ } else if (selectedPeriod.value === "custom") {
|
|
|
|
|
+ if (!startDateIso.value || !endDateIso.value) return {};
|
|
|
|
|
+ return { startDate: startDateIso.value, endDate: endDateIso.value };
|
|
|
|
|
+ } else {
|
|
|
|
|
+ start = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return { startDate: toIsoDate(start), endDate: toIsoDate(end) };
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const revenuePeriodLabel = computed(() => {
|
|
|
|
|
+ if (!revenuePeriod.value.startDate) return "Mês atual";
|
|
|
|
|
+ const format = (value) => value.split("-").reverse().join("/");
|
|
|
|
|
+ if (revenuePeriod.value.startDate === revenuePeriod.value.endDate) {
|
|
|
|
|
+ return `Recebido em ${format(revenuePeriod.value.startDate)}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ return `Recebido de ${format(revenuePeriod.value.startDate)} a ${format(revenuePeriod.value.endDate)}`;
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const receivablesPeriodLabel = computed(() => {
|
|
|
|
|
+ if (!revenuePeriod.value.startDate) return "Mês atual";
|
|
|
|
|
+ const format = (value) => value.split("-").reverse().join("/");
|
|
|
|
|
+ if (revenuePeriod.value.startDate === revenuePeriod.value.endDate) {
|
|
|
|
|
+ return `Vencimento em ${format(revenuePeriod.value.startDate)}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ return `Vencimentos de ${format(revenuePeriod.value.startDate)} a ${format(revenuePeriod.value.endDate)}`;
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Array of unit IDs to use as filter.
|
|
|
|
|
+ * - Group selected → all unit IDs in that group
|
|
|
|
|
+ * - Unit selected → just that unit ID
|
|
|
|
|
+ * - Nothing selected → [] (no filter = all units)
|
|
|
|
|
+ */
|
|
|
|
|
+const activeUnitIds = computed(() => {
|
|
|
|
|
+ if (selectedGroup.value?.unit_ids?.length) {
|
|
|
|
|
+ return selectedGroup.value.unit_ids;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (selectedUnit.value?.value) {
|
|
|
|
|
+ return [selectedUnit.value.value];
|
|
|
|
|
+ }
|
|
|
|
|
+ return [];
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+/** Human-readable label shown in the active-filter chip */
|
|
|
|
|
+const filterLabel = computed(() => {
|
|
|
|
|
+ if (selectedGroup.value) return `Grupo: ${selectedGroup.value.label}`;
|
|
|
|
|
+ if (selectedUnit.value) return `Unidade: ${selectedUnit.value.label}`;
|
|
|
|
|
+ return "";
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// Mutual exclusion handlers
|
|
|
|
|
+const onUnitSelected = (val) => {
|
|
|
|
|
+ if (val) selectedGroup.value = null;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const openContratosCanceladosDialog = () => {
|
|
|
|
|
- $q.dialog({ component: ContratosCanceladosDialog });
|
|
|
|
|
|
|
+const onGroupSelected = (val) => {
|
|
|
|
|
+ if (val) selectedUnit.value = null;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const openFrequenciaMediaDialog = () => {
|
|
|
|
|
- $q.dialog({ component: FrequenciaMediaDialog });
|
|
|
|
|
|
|
+const clearUnitFilters = () => {
|
|
|
|
|
+ selectedUnit.value = null;
|
|
|
|
|
+ selectedGroup.value = null;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const openEstoqueProdutosDialog = () => {
|
|
|
|
|
- $q.dialog({ component: EstoqueProdutosDialog });
|
|
|
|
|
|
|
+const clearFilters = () => {
|
|
|
|
|
+ clearUnitFilters();
|
|
|
|
|
+ selectedPeriod.value = "month";
|
|
|
|
|
+ startDate.value = "";
|
|
|
|
|
+ endDate.value = "";
|
|
|
|
|
+ startDateIso.value = "";
|
|
|
|
|
+ endDateIso.value = "";
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const openTicketsAbertoDialog = () => {
|
|
|
|
|
- $q.dialog({ component: TicketsAbertoDialog });
|
|
|
|
|
|
|
+// ─── Dialog openers ──────────────────────────────────────────
|
|
|
|
|
+const openActiveStudentsDialog = () => {
|
|
|
|
|
+ $q.dialog({ component: ActiveStudentsDialog, componentProps: { unitIds: activeUnitIds.value } });
|
|
|
};
|
|
};
|
|
|
-const defaultPeriod = ref("month");
|
|
|
|
|
-const defaultEventId = ref(1);
|
|
|
|
|
|
|
|
|
|
-const selectedPeriod = ref("custom");
|
|
|
|
|
-const startDate = ref("");
|
|
|
|
|
-const endDate = ref("");
|
|
|
|
|
|
|
+const openFrozenContractsDialog = () => {
|
|
|
|
|
+ $q.dialog({ component: FrozenContractsDialog, componentProps: { unitIds: activeUnitIds.value } });
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
-const periodOptions = [
|
|
|
|
|
- { label: "Hoje", value: "today" },
|
|
|
|
|
- { label: "Esta semana", value: "week" },
|
|
|
|
|
- { label: "Este mês", value: "month" },
|
|
|
|
|
- { label: "Este ano", value: "year" },
|
|
|
|
|
- { label: "Personalizado", value: "custom" },
|
|
|
|
|
-];
|
|
|
|
|
|
|
+const openCancelledContractsDialog = () => {
|
|
|
|
|
+ $q.dialog({ component: CancelledContractsDialog, componentProps: { unitIds: activeUnitIds.value } });
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
-const aniversariantes = [
|
|
|
|
|
- { day: 10, name: "Heloisa Faria" },
|
|
|
|
|
- { day: 11, name: "Juliana Costa" },
|
|
|
|
|
- { day: 16, name: "Juliana Costa" },
|
|
|
|
|
- { day: 23, name: "Fernando Almeida" },
|
|
|
|
|
- { day: 29, name: "Lucas Pereira" },
|
|
|
|
|
- { day: 34, name: "Sofia Martins" },
|
|
|
|
|
-];
|
|
|
|
|
|
|
+const openAverageAttendanceDialog = () => {
|
|
|
|
|
+ $q.dialog({
|
|
|
|
|
+ component: AverageAttendanceDialog,
|
|
|
|
|
+ componentProps: { units: attendanceByUnit.value },
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
-const matriculasChart = {
|
|
|
|
|
- labels: ["JAN", "FEV", "MAR", "ABR", "MAI", "JUN"],
|
|
|
|
|
- datasets: [
|
|
|
|
|
- {
|
|
|
|
|
- label: "Matrículas",
|
|
|
|
|
- data: [120, 200, 150, 80, 70, 110],
|
|
|
|
|
- color: ["#3B82F6", "#EF4444", "#A855F7", "#374151", "#EAB308", "#06B6D4"],
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
|
|
+const openProductStockDialog = () => {
|
|
|
|
|
+ $q.dialog({
|
|
|
|
|
+ component: ProductStockDialog,
|
|
|
|
|
+ componentProps: { products: products.value },
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const openTicketsDialog = () => {
|
|
|
|
|
+ $q.dialog({
|
|
|
|
|
+ component: OpenTicketsDialog,
|
|
|
|
|
+ componentProps: { tickets: tickets.value },
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// Matrículas por Período — preenchido pela API (últimos 6 meses).
|
|
|
|
|
+const MATRICULA_COLORS = ["#3B82F6", "#EF4444", "#A855F7", "#374151", "#EAB308", "#06B6D4"];
|
|
|
|
|
+const matriculasChart = ref({
|
|
|
|
|
+ labels: [],
|
|
|
|
|
+ datasets: [{ label: "Matrículas", data: [], color: MATRICULA_COLORS }],
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// ─── Data fetching ───────────────────────────────────────────
|
|
|
|
|
+const fetchMetrics = async () => {
|
|
|
|
|
+ const ids = activeUnitIds.value;
|
|
|
|
|
+ const [studentSummary, contractSummary, dashboard] = await Promise.all([
|
|
|
|
|
+ getStudentSummaryFranchisor(ids),
|
|
|
|
|
+ getFranchisorContractSummary(ids),
|
|
|
|
|
+ getFranchisorDashboard(ids, revenuePeriod.value),
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ totalStudents.value = studentSummary.total;
|
|
|
|
|
+ activeContracts.value = contractSummary.active ?? 0;
|
|
|
|
|
+ frozenContracts.value = contractSummary.frozen;
|
|
|
|
|
+ cancelledContracts.value = contractSummary.cancelled;
|
|
|
|
|
+
|
|
|
|
|
+ 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 ?? [];
|
|
|
|
|
+
|
|
|
|
|
+ matriculasChart.value = {
|
|
|
|
|
+ labels: dashboard.enrollments?.labels ?? [],
|
|
|
|
|
+ datasets: [
|
|
|
|
|
+ {
|
|
|
|
|
+ label: "Matrículas",
|
|
|
|
|
+ data: dashboard.enrollments?.data ?? [],
|
|
|
|
|
+ color: MATRICULA_COLORS,
|
|
|
|
|
+ },
|
|
|
|
|
+ ],
|
|
|
|
|
+ };
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+// Re-fetch whenever unit/group filter changes
|
|
|
|
|
+watch([activeUnitIds, selectedPeriod, startDateIso, endDateIso], async () => {
|
|
|
|
|
+ if (selectedPeriod.value === "custom" && (!startDateIso.value || !endDateIso.value)) return;
|
|
|
|
|
+ isLoading.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ await fetchMetrics();
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ isLoading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// Faturamento Serviço / Materiais — zerado (sem fluxo de dados ainda).
|
|
|
const faturamentoChart = {
|
|
const faturamentoChart = {
|
|
|
- labels: [
|
|
|
|
|
- "17/02",
|
|
|
|
|
- "18/02",
|
|
|
|
|
- "19/02",
|
|
|
|
|
- "20/02",
|
|
|
|
|
- "21/02",
|
|
|
|
|
- "22/02",
|
|
|
|
|
- "23/02",
|
|
|
|
|
- "24/02",
|
|
|
|
|
- "25/02",
|
|
|
|
|
- "26/02",
|
|
|
|
|
- ],
|
|
|
|
|
|
|
+ labels: [],
|
|
|
datasets: [
|
|
datasets: [
|
|
|
- {
|
|
|
|
|
- label: "Serviço",
|
|
|
|
|
- data: [
|
|
|
|
|
- 18500, 21000, 16400, 22300, 19800, 17200, 15800, 24100, 20500, 27600,
|
|
|
|
|
- ],
|
|
|
|
|
- color: "#a274f1",
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- label: "Materiais",
|
|
|
|
|
- data: [9200, 10500, 8100, 11400, 9800, 8400, 8700, 12200, 10100, 13100],
|
|
|
|
|
- color: "#ff9999",
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ { label: "Serviço", data: [], color: "#a274f1" },
|
|
|
|
|
+ { label: "Materiais", data: [], color: "#ff9999" },
|
|
|
],
|
|
],
|
|
|
};
|
|
};
|
|
|
|
|
|
|
@@ -312,164 +488,14 @@ const formatCurrencyTooltip = (context) => {
|
|
|
return ` ${context.dataset.label}: R$ ${value.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}`;
|
|
return ` ${context.dataset.label}: R$ ${value.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}`;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const ordersChart = ref({});
|
|
|
|
|
-const participantsChart = ref({});
|
|
|
|
|
-const paymentsChart = ref({});
|
|
|
|
|
-const ticketsSoldChart = ref({});
|
|
|
|
|
-const eventTicketsByTypeChart = ref({});
|
|
|
|
|
-const eventParticipantsByCNPJAndCPF = ref({});
|
|
|
|
|
-const salesOverTimeLineChart = ref({});
|
|
|
|
|
-const eventSourcePieChart = ref({});
|
|
|
|
|
-
|
|
|
|
|
-const generateMockData = () => {
|
|
|
|
|
- const createMiniChartData = (currentTotal, percentage) => ({
|
|
|
|
|
- current_total: currentTotal,
|
|
|
|
|
- percentage_change: percentage,
|
|
|
|
|
- trend_data: Array.from({ length: 10 }, () =>
|
|
|
|
|
- Math.floor(Math.random() * 100),
|
|
|
|
|
- ),
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- const barChartDataRaw = [
|
|
|
|
|
- {
|
|
|
|
|
- label: t("dashboard.charts.tickets_by_type.labels.vip"),
|
|
|
|
|
- value: Math.floor(Math.random() * 300),
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- label: t("dashboard.charts.tickets_by_type.labels.track"),
|
|
|
|
|
- value: Math.floor(Math.random() * 800),
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- label: t("dashboard.charts.tickets_by_type.labels.box"),
|
|
|
|
|
- value: Math.floor(Math.random() * 400),
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- label: t("dashboard.charts.tickets_by_type.labels.courtesy"),
|
|
|
|
|
- value: Math.floor(Math.random() * 50),
|
|
|
|
|
- },
|
|
|
|
|
- ];
|
|
|
|
|
-
|
|
|
|
|
- const doughnutDataRaw = [
|
|
|
|
|
- {
|
|
|
|
|
- label: t("common.terms.cpf"),
|
|
|
|
|
- value: Math.floor(Math.random() * 900 + 100),
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- label: t("common.terms.cnpj"),
|
|
|
|
|
- value: Math.floor(Math.random() * 100 + 10),
|
|
|
|
|
- },
|
|
|
|
|
- ];
|
|
|
|
|
- const doughnutTotal = doughnutDataRaw.reduce(
|
|
|
|
|
- (sum, item) => sum + item.value,
|
|
|
|
|
- 0,
|
|
|
|
|
- );
|
|
|
|
|
-
|
|
|
|
|
- const lineChartDataRaw = [
|
|
|
|
|
- {
|
|
|
|
|
- label: t("common.months.january"),
|
|
|
|
|
- value: Math.floor(1200 + Math.random() * 500),
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- label: t("common.months.february"),
|
|
|
|
|
- value: Math.floor(1900 + Math.random() * 500),
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- label: t("common.months.march"),
|
|
|
|
|
- value: Math.floor(3000 + Math.random() * 500),
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- label: t("common.months.april"),
|
|
|
|
|
- value: Math.floor(5000 + Math.random() * 500),
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- label: t("common.months.may"),
|
|
|
|
|
- value: Math.floor(2300 + Math.random() * 500),
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- label: t("common.months.june"),
|
|
|
|
|
- value: Math.floor(3200 + Math.random() * 500),
|
|
|
|
|
- },
|
|
|
|
|
- ];
|
|
|
|
|
-
|
|
|
|
|
- const pieDataRaw = [
|
|
|
|
|
- {
|
|
|
|
|
- label: t("dashboard.charts.registration_source.sources.instagram"),
|
|
|
|
|
- value: Math.floor(450 + Math.random() * 50),
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- label: t("dashboard.charts.registration_source.sources.facebook"),
|
|
|
|
|
- value: Math.floor(250 + Math.random() * 50),
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- label: t("dashboard.charts.registration_source.sources.google"),
|
|
|
|
|
- value: Math.floor(180 + Math.random() * 50),
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- label: t("dashboard.charts.registration_source.sources.referral"),
|
|
|
|
|
- value: Math.floor(120 + Math.random() * 50),
|
|
|
|
|
- },
|
|
|
|
|
- ];
|
|
|
|
|
- const pieTotal = pieDataRaw.reduce((sum, item) => sum + item.value, 0);
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- payments: createMiniChartData(
|
|
|
|
|
- (Math.random() * 20000 + 5000).toFixed(2),
|
|
|
|
|
- (Math.random() * 20 - 5).toFixed(2),
|
|
|
|
|
- ),
|
|
|
|
|
- orders: createMiniChartData(
|
|
|
|
|
- Math.floor(Math.random() * 500 + 50),
|
|
|
|
|
- (Math.random() * 15 - 5).toFixed(2),
|
|
|
|
|
- ),
|
|
|
|
|
- tickets_sold: createMiniChartData(
|
|
|
|
|
- Math.floor(Math.random() * 1500 + 200),
|
|
|
|
|
- (Math.random() * 25 - 5).toFixed(2),
|
|
|
|
|
- ),
|
|
|
|
|
- participants: createMiniChartData(
|
|
|
|
|
- Math.floor(Math.random() * 1000 + 100),
|
|
|
|
|
- (Math.random() * 10 - 5).toFixed(2),
|
|
|
|
|
- ),
|
|
|
|
|
- barData: {
|
|
|
|
|
- chart_data: barChartDataRaw,
|
|
|
|
|
- },
|
|
|
|
|
- doughnutData: {
|
|
|
|
|
- chart_data: doughnutDataRaw,
|
|
|
|
|
- current_total: doughnutTotal,
|
|
|
|
|
- },
|
|
|
|
|
- lineData: {
|
|
|
|
|
- chart_data: lineChartDataRaw,
|
|
|
|
|
- },
|
|
|
|
|
- pieData: {
|
|
|
|
|
- chart_data: pieDataRaw,
|
|
|
|
|
- current_total: pieTotal,
|
|
|
|
|
- },
|
|
|
|
|
- };
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-const updateDashboardData = async () => {
|
|
|
|
|
|
|
+// ─── Init ─────────────────────────────────────────────────────
|
|
|
|
|
+onMounted(async () => {
|
|
|
isLoading.value = true;
|
|
isLoading.value = true;
|
|
|
- setTimeout(() => {
|
|
|
|
|
- const mockData = generateMockData();
|
|
|
|
|
-
|
|
|
|
|
- ordersChart.value = mockData.orders;
|
|
|
|
|
- participantsChart.value = mockData.participants;
|
|
|
|
|
- paymentsChart.value = mockData.payments;
|
|
|
|
|
- ticketsSoldChart.value = mockData.tickets_sold;
|
|
|
|
|
-
|
|
|
|
|
- eventTicketsByTypeChart.value = mockData.barData;
|
|
|
|
|
- eventParticipantsByCNPJAndCPF.value = mockData.doughnutData;
|
|
|
|
|
- salesOverTimeLineChart.value = mockData.lineData;
|
|
|
|
|
- eventSourcePieChart.value = mockData.pieData;
|
|
|
|
|
-
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ await fetchMetrics();
|
|
|
|
|
+ } finally {
|
|
|
isLoading.value = false;
|
|
isLoading.value = false;
|
|
|
- }, 500);
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-watch([defaultPeriod, defaultEventId], async () => {
|
|
|
|
|
- await updateDashboardData();
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-onMounted(async () => {
|
|
|
|
|
- await updateDashboardData();
|
|
|
|
|
|
|
+ }
|
|
|
});
|
|
});
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
@@ -505,17 +531,18 @@ onMounted(async () => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.charts-row > *:nth-child(1) {
|
|
.charts-row > *:nth-child(1) {
|
|
|
- flex: 0 0 calc(41.6667% - 11px);
|
|
|
|
|
|
|
+ flex: 0 0 calc(50% - 8px);
|
|
|
min-width: 0;
|
|
min-width: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.charts-row > *:nth-child(2) {
|
|
.charts-row > *:nth-child(2) {
|
|
|
- flex: 0 0 calc(33.3333% - 11px);
|
|
|
|
|
|
|
+ flex: 0 0 calc(50% - 8px);
|
|
|
min-width: 0;
|
|
min-width: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.charts-row > *:nth-child(3) {
|
|
|
|
|
- flex: 0 0 calc(25% - 11px);
|
|
|
|
|
|
|
+.charts-row > *:nth-child(3),
|
|
|
|
|
+.charts-row > *:nth-child(4) {
|
|
|
|
|
+ flex: 1 1 calc(50% - 8px);
|
|
|
min-width: 0;
|
|
min-width: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -538,10 +565,18 @@ onMounted(async () => {
|
|
|
max-width: 300px;
|
|
max-width: 300px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.clear-filters-button {
|
|
|
|
|
+ margin-left: auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
@media (max-width: 599px) {
|
|
@media (max-width: 599px) {
|
|
|
.filter-item {
|
|
.filter-item {
|
|
|
flex: 1 1 100%;
|
|
flex: 1 1 100%;
|
|
|
max-width: 100%;
|
|
max-width: 100%;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ .clear-filters-button {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|