|
|
@@ -1,58 +1,80 @@
|
|
|
<template>
|
|
|
<div>
|
|
|
- <DefaultHeaderPage class="q-pa-sm" />
|
|
|
+ <DefaultHeaderPage />
|
|
|
|
|
|
<div class="q-pa-sm">
|
|
|
<div class="stat-cards-row q-mb-md">
|
|
|
<DashboardStatCard
|
|
|
- title="Total alunos (contratos ativos)"
|
|
|
- icon="mdi-account-multiple-outline"
|
|
|
- :value="String(totalAlunos)"
|
|
|
:badge="`${totalAlunos} ativos`"
|
|
|
+ :value="String(totalAlunos)"
|
|
|
+ icon="mdi-account-multiple-outline"
|
|
|
+ title="Total alunos (contratos ativos)"
|
|
|
/>
|
|
|
+
|
|
|
+ <!-- TODO: usar dados reais de receita (zerado por enquanto). -->
|
|
|
+
|
|
|
<DashboardStatCard
|
|
|
- title="Receita Total"
|
|
|
icon="mdi-currency-usd"
|
|
|
- value="R$ 0,00"
|
|
|
subtitle="0 pagamentos pendentes"
|
|
|
+ title="Receita Total"
|
|
|
+ value="R$ 0,00"
|
|
|
/>
|
|
|
+
|
|
|
+ <!-- TODO: usar dados reais de ticket médio (zerado por enquanto). -->
|
|
|
+
|
|
|
<DashboardStatCard
|
|
|
- title="Ticket Médio"
|
|
|
icon="mdi-calendar-blank"
|
|
|
- value="R$ 12,00"
|
|
|
subtitle="Estável"
|
|
|
+ title="Ticket Médio"
|
|
|
+ value="R$ 0,00"
|
|
|
/>
|
|
|
+
|
|
|
<DashboardStatCard
|
|
|
- title="Aniversariantes"
|
|
|
+ :value="String(aniversariantes.length)"
|
|
|
icon="mdi-emoticon-happy-outline"
|
|
|
- value="0"
|
|
|
subtitle="Fortaleça seus relacionamentos"
|
|
|
+ title="Aniversariantes"
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
<div class="row q-col-gutter-md q-mb-md items-stretch">
|
|
|
<div class="col-12 col-md-5">
|
|
|
- <DashboardChartCard title="Faturamento Serviço / Materiais" style="height: 100%">
|
|
|
+ <DashboardChartCard
|
|
|
+ style="height: 100%"
|
|
|
+ title="Faturamento Serviço / Materiais"
|
|
|
+ >
|
|
|
<GroupedBarChart
|
|
|
- :labels="faturamentoChart.labels"
|
|
|
:datasets="faturamentoChart.datasets"
|
|
|
- label-y="R$"
|
|
|
+ :labels="faturamentoChart.labels"
|
|
|
:tick-formatter="formatCurrencyTick"
|
|
|
:tooltip-formatter="formatCurrencyTooltip"
|
|
|
class="full-width full-height"
|
|
|
+ label-y="R$"
|
|
|
/>
|
|
|
</DashboardChartCard>
|
|
|
</div>
|
|
|
|
|
|
<div class="col-12 col-md-4">
|
|
|
- <q-card flat bordered class="full-height">
|
|
|
- <q-card-section class="row justify-between items-center q-pb-xs">
|
|
|
- <span class="text-subtitle2 text-weight-medium"
|
|
|
- >Contratos Ativos</span
|
|
|
- >
|
|
|
- <q-icon name="mdi-trending-up" color="grey-5" />
|
|
|
+ <q-card
|
|
|
+ bordered
|
|
|
+ class="full-height"
|
|
|
+ flat
|
|
|
+ >
|
|
|
+ <q-card-section
|
|
|
+ class="row justify-between items-center q-pb-xs"
|
|
|
+ >
|
|
|
+ <span class="text-subtitle2 text-weight-medium">
|
|
|
+ Contratos Ativos
|
|
|
+ </span>
|
|
|
+
|
|
|
+ <q-icon
|
|
|
+ color="grey-5"
|
|
|
+ name="mdi-trending-up"
|
|
|
+ />
|
|
|
</q-card-section>
|
|
|
+
|
|
|
<q-separator />
|
|
|
+
|
|
|
<q-card-section
|
|
|
class="flex flex-center q-pt-sm"
|
|
|
style="height: calc(100% - 57px); position: relative"
|
|
|
@@ -64,176 +86,219 @@
|
|
|
:plugins="[gaugeNeedlePlugin]"
|
|
|
/>
|
|
|
</div>
|
|
|
+
|
|
|
<div class="gauge-label">
|
|
|
- <div class="text-h5 text-bold">70</div>
|
|
|
- <div class="text-caption text-grey-6">Grade</div>
|
|
|
+ <div class="text-h5 text-bold">
|
|
|
+ {{ activeContracts }}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="text-caption text-grey-6">
|
|
|
+ Ativos
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</q-card-section>
|
|
|
</q-card>
|
|
|
</div>
|
|
|
|
|
|
<div class="col-12 col-md-3">
|
|
|
- <q-card flat bordered class="full-height">
|
|
|
- <q-card-section class="row justify-between items-center q-pb-xs">
|
|
|
- <span class="text-subtitle2 text-weight-medium"
|
|
|
- >Atalhos rápidos</span
|
|
|
- >
|
|
|
- <q-icon name="mdi-apps" color="grey-5" />
|
|
|
+ <q-card
|
|
|
+ bordered
|
|
|
+ class="full-height"
|
|
|
+ flat
|
|
|
+ >
|
|
|
+ <q-card-section
|
|
|
+ class="row justify-between items-center q-pb-xs"
|
|
|
+ >
|
|
|
+ <span class="text-subtitle2 text-weight-medium">
|
|
|
+ Atalhos rápidos
|
|
|
+ </span>
|
|
|
+
|
|
|
+ <q-icon
|
|
|
+ color="grey-5"
|
|
|
+ name="mdi-apps"
|
|
|
+ />
|
|
|
</q-card-section>
|
|
|
+
|
|
|
<q-separator />
|
|
|
+
|
|
|
<q-card-section class="q-pt-md column q-gutter-sm">
|
|
|
<q-btn
|
|
|
- unelevated
|
|
|
+ v-if="canAdd"
|
|
|
+ class="full-width"
|
|
|
color="primary"
|
|
|
label="Criar contrato"
|
|
|
no-caps
|
|
|
- class="full-width"
|
|
|
+ unelevated
|
|
|
+ @click="onCriarContrato"
|
|
|
/>
|
|
|
+
|
|
|
<q-btn
|
|
|
- unelevated
|
|
|
+ v-if="canEditClasses"
|
|
|
+ class="full-width"
|
|
|
color="primary"
|
|
|
label="Registrar presença"
|
|
|
no-caps
|
|
|
- class="full-width"
|
|
|
- />
|
|
|
- <q-btn
|
|
|
unelevated
|
|
|
- color="primary"
|
|
|
- label="Novo pedido"
|
|
|
- no-caps
|
|
|
- class="full-width"
|
|
|
+ @click="onRegistrarPresenca"
|
|
|
/>
|
|
|
+
|
|
|
+ <div
|
|
|
+ v-if="canAddOrders"
|
|
|
+ class="full-width cursor-not-allowed"
|
|
|
+ >
|
|
|
+ <q-btn
|
|
|
+ class="full-width"
|
|
|
+ color="primary"
|
|
|
+ disable
|
|
|
+ label="Novo pedido"
|
|
|
+ no-caps
|
|
|
+ unelevated
|
|
|
+ />
|
|
|
+
|
|
|
+ <q-tooltip>
|
|
|
+ Esta funcionalidade ainda não foi desenvolvida.
|
|
|
+ </q-tooltip>
|
|
|
+ </div>
|
|
|
</q-card-section>
|
|
|
</q-card>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- Row 3: Bottom -->
|
|
|
<div class="row q-col-gutter-md items-stretch">
|
|
|
<div class="col-12 col-md-5">
|
|
|
- <DashboardChartCard title="Matrículas por Período" style="height: 100%">
|
|
|
- <GroupedBarChart
|
|
|
- :labels="matriculasChart.labels"
|
|
|
- :datasets="matriculasChart.datasets"
|
|
|
- :bar-radius="50"
|
|
|
- :show-datalabels="true"
|
|
|
- :max-bar-thickness="44"
|
|
|
- :category-percentage="0.6"
|
|
|
- :bar-percentage="0.85"
|
|
|
- class="full-width full-height"
|
|
|
- />
|
|
|
- </DashboardChartCard>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="col-12 col-md-4">
|
|
|
- <AniversariantesCard :people="aniversariantes" style="height: 100%" />
|
|
|
- </div>
|
|
|
+ <q-card class="card-ring full-height feriados-card" flat>
|
|
|
+ <div class="flex justify-between items-center no-wrap q-mb-sm">
|
|
|
+ <span class="text-subtitle2 text-weight-regular">
|
|
|
+ Feriados do mês
|
|
|
+ </span>
|
|
|
|
|
|
- <div class="col-12 col-md-3">
|
|
|
- <q-card flat bordered class="full-height">
|
|
|
- <q-card-section class="row justify-between items-center q-pb-xs">
|
|
|
- <span class="text-subtitle2 text-weight-medium"
|
|
|
- >Feriados do mês</span
|
|
|
- >
|
|
|
<q-btn
|
|
|
- flat
|
|
|
- round
|
|
|
+ color="grey-5"
|
|
|
dense
|
|
|
+ flat
|
|
|
icon="mdi-calendar-star"
|
|
|
- color="grey-5"
|
|
|
+ round
|
|
|
@click="openFeriadosDialog"
|
|
|
/>
|
|
|
- </q-card-section>
|
|
|
- <q-separator />
|
|
|
- <q-card-section class="q-pt-md">
|
|
|
- <q-btn
|
|
|
- unelevated
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <q-btn
|
|
|
+ v-if="canAdd"
|
|
|
+ class="full-width q-mb-md"
|
|
|
+ color="primary"
|
|
|
+ label="Nova data"
|
|
|
+ no-caps
|
|
|
+ unelevated
|
|
|
+ @click="openFeriadosDialog"
|
|
|
+ />
|
|
|
+
|
|
|
+ <div
|
|
|
+ v-if="feriadosLoading"
|
|
|
+ class="flex flex-center q-py-md"
|
|
|
+ >
|
|
|
+ <q-spinner
|
|
|
color="primary"
|
|
|
- label="Nova data"
|
|
|
- no-caps
|
|
|
- class="full-width q-mb-md"
|
|
|
- @click="openFeriadosDialog"
|
|
|
+ size="24px"
|
|
|
/>
|
|
|
- <div v-if="feriadosLoading" class="flex flex-center q-py-md">
|
|
|
- <q-spinner color="primary" size="24px" />
|
|
|
- </div>
|
|
|
- <div v-else-if="feriadosMes.length === 0" class="text-caption text-grey-5 text-center">
|
|
|
- Nenhum feriado neste mês.
|
|
|
- </div>
|
|
|
- <div v-else class="row q-gutter-sm">
|
|
|
- <div
|
|
|
- v-for="feriado in feriadosMes"
|
|
|
- :key="feriado.id"
|
|
|
- class="column items-center"
|
|
|
- style="min-width: 52px"
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div
|
|
|
+ v-else-if="feriadosMes.length === 0"
|
|
|
+ class="text-caption text-grey-5 text-center q-mt-sm"
|
|
|
+ >
|
|
|
+ Nenhum feriado neste mês.
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div
|
|
|
+ v-else
|
|
|
+ class="row q-gutter-sm"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ v-for="feriado in feriadosMes"
|
|
|
+ :key="feriado.id"
|
|
|
+ class="column items-center cursor-pointer"
|
|
|
+ style="min-width: 52px"
|
|
|
+ @click="openEditFromDashboard(feriado)"
|
|
|
+ >
|
|
|
+ <q-badge
|
|
|
+ class="text-subtitle1 text-bold q-pa-sm"
|
|
|
+ color="deep-orange"
|
|
|
+ style="min-width: 40px; justify-content: center"
|
|
|
>
|
|
|
- <q-badge
|
|
|
- color="deep-orange"
|
|
|
- class="text-subtitle1 text-bold q-pa-sm"
|
|
|
- style="min-width: 40px; justify-content: center"
|
|
|
- >
|
|
|
- {{ feriado.dia }}
|
|
|
- </q-badge>
|
|
|
- <div class="text-caption q-mt-xs text-center">
|
|
|
- {{ feriado.nome }}
|
|
|
- </div>
|
|
|
+ {{ feriado.dia }}
|
|
|
+ </q-badge>
|
|
|
+
|
|
|
+ <div class="text-caption q-mt-xs text-center">
|
|
|
+ {{ feriado.nome }}
|
|
|
</div>
|
|
|
</div>
|
|
|
- </q-card-section>
|
|
|
+ </div>
|
|
|
</q-card>
|
|
|
</div>
|
|
|
+
|
|
|
+ <div class="col-12 col-md-4">
|
|
|
+ <DashboardChartCard
|
|
|
+ style="height: 100%"
|
|
|
+ title="Matrículas por Período"
|
|
|
+ >
|
|
|
+ <GroupedBarChart
|
|
|
+ :bar-percentage="0.85"
|
|
|
+ :bar-radius="50"
|
|
|
+ :category-percentage="0.6"
|
|
|
+ :datasets="matriculasChart.datasets"
|
|
|
+ :labels="matriculasChart.labels"
|
|
|
+ :max-bar-thickness="44"
|
|
|
+ :show-datalabels="true"
|
|
|
+ class="full-width full-height"
|
|
|
+ />
|
|
|
+ </DashboardChartCard>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="col-12 col-md-3">
|
|
|
+ <AniversariantesCard
|
|
|
+ :people="aniversariantes"
|
|
|
+ style="height: 100%"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, computed, onMounted } from "vue";
|
|
|
+import { ArcElement, Chart as ChartJS, Legend, Tooltip } from "chart.js";
|
|
|
+import { computed, onMounted, ref, watch } from "vue";
|
|
|
import { Doughnut } from "vue-chartjs";
|
|
|
-import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";
|
|
|
+import { getDashboardSummary } from "src/api/franchisee_dashboard";
|
|
|
+import { getHolidays } from "src/api/holiday";
|
|
|
+import { permissionStore } from "src/stores/permission";
|
|
|
import { useQuasar } from "quasar";
|
|
|
-import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
|
|
|
-import DashboardStatCard from "src/components/charts/DashboardStatCard.vue";
|
|
|
+import { useRouter } from "vue-router";
|
|
|
+import { userStore } from "src/stores/user";
|
|
|
+
|
|
|
+import AniversariantesCard from "src/components/charts/AniversariantesCard.vue";
|
|
|
import DashboardChartCard from "src/components/charts/DashboardChartCard.vue";
|
|
|
+import DashboardStatCard from "src/components/charts/DashboardStatCard.vue";
|
|
|
import GroupedBarChart from "src/components/charts/normal/GroupedBarChart.vue";
|
|
|
-import AniversariantesCard from "src/components/charts/AniversariantesCard.vue";
|
|
|
+import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
|
|
|
+import AddEditContractDialog from "src/pages/students/components/AddEditContractDialog.vue";
|
|
|
import FeriadosDialog from "./components/FeriadosDialog.vue";
|
|
|
-import { getHolidays } from "src/api/holiday";
|
|
|
-import { getStudents } from "src/api/student";
|
|
|
+import FeriadosEditDialog from "./components/FeriadosEditDialog.vue";
|
|
|
|
|
|
ChartJS.register(ArcElement, Tooltip, Legend);
|
|
|
|
|
|
const $q = useQuasar();
|
|
|
+const router = useRouter();
|
|
|
|
|
|
-const faturamentoChart = {
|
|
|
- labels: [
|
|
|
- "17/02", "18/02", "19/02", "20/02", "21/02",
|
|
|
- "22/02", "23/02", "24/02", "25/02", "26/02",
|
|
|
- "27/02", "28/02",
|
|
|
- ],
|
|
|
- datasets: [
|
|
|
- {
|
|
|
- label: "Serviço",
|
|
|
- data: [120, 185, 95, 210, 155, 200, 170, 130, 195, 160, 145, 180],
|
|
|
- color: "#a274f1",
|
|
|
- },
|
|
|
- {
|
|
|
- label: "Materiais",
|
|
|
- data: [75, 115, 60, 140, 95, 125, 105, 85, 135, 100, 90, 115],
|
|
|
- color: "#ff9999",
|
|
|
- },
|
|
|
- ],
|
|
|
-};
|
|
|
-
|
|
|
-const formatCurrencyTick = (value) => {
|
|
|
- if (value >= 1000) return `R$ ${(value / 1000).toFixed(0)}k`;
|
|
|
- return `R$ ${value}`;
|
|
|
-};
|
|
|
+const permissions = permissionStore();
|
|
|
+const store = userStore();
|
|
|
|
|
|
-const formatCurrencyTooltip = (context) => {
|
|
|
- const value = context.parsed.y;
|
|
|
- return ` ${context.dataset.label}: R$ ${value.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}`;
|
|
|
-};
|
|
|
+const activeContracts = ref(0);
|
|
|
+const allHolidays = ref([]);
|
|
|
+const aniversariantes = ref([]);
|
|
|
+const feriadosLoading = ref(false);
|
|
|
+const students = ref([]);
|
|
|
+const totalAlunos = ref(0);
|
|
|
|
|
|
const gaugeData = ref({
|
|
|
datasets: [
|
|
|
@@ -250,134 +315,319 @@ const gaugeData = ref({
|
|
|
"#D01616",
|
|
|
"#8A0000",
|
|
|
],
|
|
|
- data: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
|
|
- needleValue: 7,
|
|
|
borderColor: "transparent",
|
|
|
+ data: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
|
|
+ needleValue: 0,
|
|
|
},
|
|
|
],
|
|
|
});
|
|
|
|
|
|
const gaugeOptions = ref({
|
|
|
- rotation: 270,
|
|
|
circumference: 180,
|
|
|
cutout: "50%",
|
|
|
- responsive: true,
|
|
|
maintainAspectRatio: false,
|
|
|
plugins: {
|
|
|
- tooltip: { enabled: false },
|
|
|
- legend: { display: false },
|
|
|
datalabels: {
|
|
|
color: "black",
|
|
|
- font: { size: 14, weight: "bold" },
|
|
|
- formatter: (_value, ctx) => ctx.dataIndex,
|
|
|
+ font: {
|
|
|
+ size: 14,
|
|
|
+ weight: "bold",
|
|
|
+ },
|
|
|
+ formatter: (_value, context) => context.dataIndex,
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ display: false,
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ enabled: false,
|
|
|
},
|
|
|
},
|
|
|
+ responsive: true,
|
|
|
+ rotation: 270,
|
|
|
+});
|
|
|
+
|
|
|
+const canAdd = computed(() =>
|
|
|
+ permissions.getAccess("franchisee_dashboard", "add"),
|
|
|
+);
|
|
|
+
|
|
|
+const canAddOrders = computed(() =>
|
|
|
+ permissions.getAccess("franchisee_orders", "add"),
|
|
|
+);
|
|
|
+
|
|
|
+const canEditClasses = computed(() =>
|
|
|
+ permissions.getAccess("franchisee_classes", "edit"),
|
|
|
+);
|
|
|
+
|
|
|
+const feriadosMes = computed(() => {
|
|
|
+ const now = new Date();
|
|
|
+ const month = now.getMonth() + 1;
|
|
|
+ const year = now.getFullYear();
|
|
|
+
|
|
|
+ return allHolidays.value
|
|
|
+ .filter((holiday) => {
|
|
|
+ const date = new Date(`${holiday.holiday_date}T00:00:00`);
|
|
|
+
|
|
|
+ return (
|
|
|
+ date.getMonth() + 1 === month &&
|
|
|
+ date.getFullYear() === year
|
|
|
+ );
|
|
|
+ })
|
|
|
+ .sort(
|
|
|
+ (firstHoliday, secondHoliday) =>
|
|
|
+ new Date(firstHoliday.holiday_date) -
|
|
|
+ new Date(secondHoliday.holiday_date),
|
|
|
+ )
|
|
|
+ .map((holiday) => ({
|
|
|
+ base_holiday_id: holiday.base_holiday_id ?? null,
|
|
|
+ description: holiday.description,
|
|
|
+ dia: new Date(`${holiday.holiday_date}T00:00:00`).getDate(),
|
|
|
+ holiday_date: holiday.holiday_date,
|
|
|
+ id: holiday.id,
|
|
|
+ nome: holiday.description,
|
|
|
+ type: holiday.type,
|
|
|
+ }));
|
|
|
+});
|
|
|
+
|
|
|
+const matriculasChart = computed(() => {
|
|
|
+ const now = new Date();
|
|
|
+ const start = new Date(now.getFullYear(), now.getMonth() - 5, 1);
|
|
|
+
|
|
|
+ const counts = {};
|
|
|
+
|
|
|
+ for (const student of students.value) {
|
|
|
+ if (!student.created_at) continue;
|
|
|
+
|
|
|
+ const date = new Date(student.created_at.replace(" ", "T"));
|
|
|
+
|
|
|
+ if (date < start) continue;
|
|
|
+
|
|
|
+ const key = `${date.getFullYear()}-${date.getMonth()}`;
|
|
|
+
|
|
|
+ counts[key] = (counts[key] ?? 0) + 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ const data = [];
|
|
|
+ const labels = [];
|
|
|
+
|
|
|
+ for (let index = 0; index < 6; index++) {
|
|
|
+ const month = new Date(
|
|
|
+ start.getFullYear(),
|
|
|
+ start.getMonth() + index,
|
|
|
+ 1,
|
|
|
+ );
|
|
|
+
|
|
|
+ const key = `${month.getFullYear()}-${month.getMonth()}`;
|
|
|
+
|
|
|
+ labels.push(MONTHS_PT[month.getMonth()]);
|
|
|
+ data.push(counts[key] ?? 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ datasets: [
|
|
|
+ {
|
|
|
+ color: MATRICULA_COLORS,
|
|
|
+ data,
|
|
|
+ label: "Matrículas",
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ labels,
|
|
|
+ };
|
|
|
});
|
|
|
|
|
|
+// Faturamento Serviço / Materiais — zerado (sem fluxo de dados ainda),
|
|
|
+// igual ao Franqueador.
|
|
|
+// TODO: alimentar com dados reais de faturamento de serviço/materiais.
|
|
|
+const faturamentoChart = {
|
|
|
+ datasets: [
|
|
|
+ {
|
|
|
+ color: "#a274f1",
|
|
|
+ data: [],
|
|
|
+ label: "Serviço",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ color: "#ff9999",
|
|
|
+ data: [],
|
|
|
+ label: "Materiais",
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ labels: [],
|
|
|
+};
|
|
|
+
|
|
|
const gaugeNeedlePlugin = {
|
|
|
id: "gaugeNeedle",
|
|
|
+
|
|
|
afterDatasetsDraw(chart) {
|
|
|
const { ctx, data } = chart;
|
|
|
- ctx.save();
|
|
|
- const needleValue = data.datasets[0].needleValue;
|
|
|
+
|
|
|
const meta = chart.getDatasetMeta(0).data[0];
|
|
|
+ const needleValue = data.datasets[0].needleValue;
|
|
|
+ const outerRadius = meta.outerRadius - 20;
|
|
|
const xCenter = meta.x;
|
|
|
const yCenter = meta.y;
|
|
|
- const outerRadius = meta.outerRadius - 20;
|
|
|
+
|
|
|
const circumference =
|
|
|
- (meta.circumference / Math.PI / data.datasets[0].data[0]) * needleValue;
|
|
|
+ (meta.circumference / Math.PI / data.datasets[0].data[0]) *
|
|
|
+ needleValue;
|
|
|
+
|
|
|
const angle = Math.PI;
|
|
|
+
|
|
|
+ ctx.save();
|
|
|
ctx.translate(xCenter, yCenter);
|
|
|
ctx.rotate(angle * (circumference + 1.5));
|
|
|
+
|
|
|
ctx.beginPath();
|
|
|
- ctx.strokeStyle = "grey";
|
|
|
ctx.fillStyle = "grey";
|
|
|
+ ctx.strokeStyle = "grey";
|
|
|
ctx.moveTo(-3, 0);
|
|
|
ctx.lineTo(0, -outerRadius);
|
|
|
ctx.lineTo(3, 0);
|
|
|
ctx.stroke();
|
|
|
ctx.fill();
|
|
|
+
|
|
|
ctx.beginPath();
|
|
|
ctx.arc(0, 0, 6, 0, 2 * Math.PI);
|
|
|
ctx.fillStyle = "grey";
|
|
|
ctx.fill();
|
|
|
+
|
|
|
ctx.restore();
|
|
|
},
|
|
|
};
|
|
|
|
|
|
-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 MATRICULA_COLORS = [
|
|
|
+ "#3B82F6",
|
|
|
+ "#EF4444",
|
|
|
+ "#A855F7",
|
|
|
+ "#374151",
|
|
|
+ "#EAB308",
|
|
|
+ "#06B6D4",
|
|
|
+];
|
|
|
|
|
|
-const aniversariantes = ref([
|
|
|
- { 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 MONTHS_PT = [
|
|
|
+ "JAN",
|
|
|
+ "FEV",
|
|
|
+ "MAR",
|
|
|
+ "ABR",
|
|
|
+ "MAI",
|
|
|
+ "JUN",
|
|
|
+ "JUL",
|
|
|
+ "AGO",
|
|
|
+ "SET",
|
|
|
+ "OUT",
|
|
|
+ "NOV",
|
|
|
+ "DEZ",
|
|
|
+];
|
|
|
|
|
|
-// Alunos
|
|
|
-const totalAlunos = ref(0);
|
|
|
+const fetchHolidays = async () => {
|
|
|
+ feriadosLoading.value = true;
|
|
|
|
|
|
-async function fetchAlunos() {
|
|
|
try {
|
|
|
- const students = await getStudents();
|
|
|
- totalAlunos.value = students.length;
|
|
|
+ allHolidays.value = await getHolidays();
|
|
|
} catch {
|
|
|
- // silencioso
|
|
|
+ $q.notify({
|
|
|
+ message: "Erro ao carregar feriados.",
|
|
|
+ type: "negative",
|
|
|
+ });
|
|
|
+ } finally {
|
|
|
+ feriadosLoading.value = false;
|
|
|
}
|
|
|
-}
|
|
|
+};
|
|
|
|
|
|
-// Feriados
|
|
|
-const allHolidays = ref([]);
|
|
|
-const feriadosLoading = ref(false);
|
|
|
+const fetchSummary = async () => {
|
|
|
+ try {
|
|
|
+ const summary = await getDashboardSummary();
|
|
|
|
|
|
-const feriadosMes = computed(() => {
|
|
|
- const now = new Date();
|
|
|
- const month = now.getMonth() + 1;
|
|
|
- const year = now.getFullYear();
|
|
|
- return allHolidays.value
|
|
|
- .filter((h) => {
|
|
|
- const d = new Date(h.holiday_date + "T00:00:00");
|
|
|
- return d.getMonth() + 1 === month && d.getFullYear() === year;
|
|
|
- })
|
|
|
- .sort((a, b) => new Date(a.holiday_date) - new Date(b.holiday_date))
|
|
|
- .map((h) => ({
|
|
|
- id: h.id,
|
|
|
- dia: new Date(h.holiday_date + "T00:00:00").getDate(),
|
|
|
- nome: h.description,
|
|
|
- }));
|
|
|
-});
|
|
|
+ activeContracts.value = summary.contracts.active;
|
|
|
+ aniversariantes.value = summary.birthdays;
|
|
|
+ students.value = summary.enrollments;
|
|
|
+ totalAlunos.value = summary.students_count;
|
|
|
|
|
|
-async function fetchHolidays() {
|
|
|
- feriadosLoading.value = true;
|
|
|
- try {
|
|
|
- allHolidays.value = await getHolidays();
|
|
|
+ gaugeData.value.datasets[0].needleValue = Math.min(
|
|
|
+ activeContracts.value,
|
|
|
+ 10,
|
|
|
+ );
|
|
|
} catch {
|
|
|
- $q.notify({ type: "negative", message: "Erro ao carregar feriados." });
|
|
|
- } finally {
|
|
|
- feriadosLoading.value = false;
|
|
|
+ // silencioso
|
|
|
}
|
|
|
-}
|
|
|
+};
|
|
|
|
|
|
-function openFeriadosDialog() {
|
|
|
- $q.dialog({ component: FeriadosDialog }).onOk(() => {
|
|
|
- fetchHolidays();
|
|
|
+const formatCurrencyTick = (value) => {
|
|
|
+ if (value >= 1000) {
|
|
|
+ return `R$ ${(value / 1000).toFixed(0)}k`;
|
|
|
+ }
|
|
|
+
|
|
|
+ return `R$ ${value}`;
|
|
|
+};
|
|
|
+
|
|
|
+const formatCurrencyTooltip = (context) => {
|
|
|
+ const value = context.parsed.y;
|
|
|
+
|
|
|
+ return ` ${context.dataset.label}: R$ ${value.toLocaleString("pt-BR", {
|
|
|
+ minimumFractionDigits: 2,
|
|
|
+ })}`;
|
|
|
+};
|
|
|
+
|
|
|
+const onCriarContrato = () => {
|
|
|
+ $q.dialog({
|
|
|
+ component: AddEditContractDialog,
|
|
|
+ componentProps: {
|
|
|
+ selectStudent: true,
|
|
|
+ },
|
|
|
+ }).onOk(fetchSummary);
|
|
|
+};
|
|
|
+
|
|
|
+const onRegistrarPresenca = () => {
|
|
|
+ router.push({ name: "ClassPage" });
|
|
|
+};
|
|
|
+
|
|
|
+const openEditFromDashboard = (feriado) => {
|
|
|
+ $q.dialog({
|
|
|
+ component: FeriadosEditDialog,
|
|
|
+ componentProps: {
|
|
|
+ holiday: feriado,
|
|
|
+ },
|
|
|
+ }).onOk(({ action, holiday: updated, id }) => {
|
|
|
+ if (action === "update") {
|
|
|
+ const index = allHolidays.value.findIndex(
|
|
|
+ (holiday) => holiday.id === updated.id,
|
|
|
+ );
|
|
|
+
|
|
|
+ if (index !== -1) {
|
|
|
+ allHolidays.value[index] = {
|
|
|
+ ...allHolidays.value[index],
|
|
|
+ ...updated,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (action === "delete") {
|
|
|
+ allHolidays.value = allHolidays.value.filter(
|
|
|
+ (holiday) => holiday.id !== id,
|
|
|
+ );
|
|
|
+ }
|
|
|
});
|
|
|
-}
|
|
|
+};
|
|
|
+
|
|
|
+const openFeriadosDialog = () => {
|
|
|
+ $q.dialog({
|
|
|
+ component: FeriadosDialog,
|
|
|
+ }).onOk(fetchHolidays);
|
|
|
+};
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => store.selectedUnit?.id,
|
|
|
+ (newId, oldId) => {
|
|
|
+ if (!newId || newId === oldId) return;
|
|
|
+
|
|
|
+ fetchSummary();
|
|
|
+ fetchHolidays();
|
|
|
+ },
|
|
|
+);
|
|
|
|
|
|
onMounted(() => {
|
|
|
fetchHolidays();
|
|
|
- fetchAlunos();
|
|
|
+ fetchSummary();
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
@@ -393,21 +643,29 @@ onMounted(() => {
|
|
|
min-width: 0;
|
|
|
}
|
|
|
|
|
|
+.gauge-label {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 28%;
|
|
|
+ left: 50%;
|
|
|
+ pointer-events: none;
|
|
|
+ text-align: center;
|
|
|
+ transform: translateX(-50%);
|
|
|
+}
|
|
|
+
|
|
|
+.feriados-card {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ padding: 20px 24px;
|
|
|
+ border-radius: 12px;
|
|
|
+}
|
|
|
+
|
|
|
@media (max-width: 599px) {
|
|
|
.stat-cards-row {
|
|
|
flex-wrap: wrap;
|
|
|
}
|
|
|
+
|
|
|
.stat-cards-row > * {
|
|
|
flex: 1 1 calc(50% - 8px);
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
-.gauge-label {
|
|
|
- position: absolute;
|
|
|
- bottom: 28%;
|
|
|
- left: 50%;
|
|
|
- transform: translateX(-50%);
|
|
|
- text-align: center;
|
|
|
- pointer-events: none;
|
|
|
-}
|
|
|
-</style>
|
|
|
+</style>
|