Просмотр исходного кода

Merge branch 'feature/GINC-GAB-CB002-dashboard-e-feriados' of Softpar/sfp_vue_franchisor_ginastica_cerebro into development

Gabriel Alves 1 месяц назад
Родитель
Сommit
c661505a9a

+ 21 - 0
src/api/holiday.js

@@ -0,0 +1,21 @@
+import api from "src/api";
+
+export const getBaseHolidays = async () => {
+  const { data } = await api.get("/base-holiday");
+  return data.payload;
+};
+
+export const createBaseHoliday = async (payload) => {
+  const { data } = await api.post("/base-holiday", payload);
+  return data.payload;
+};
+
+export const updateBaseHoliday = async (id, payload) => {
+  const { data } = await api.put(`/base-holiday/${id}`, payload);
+  return data.payload;
+};
+
+export const deleteBaseHoliday = async (id) => {
+  const { data } = await api.delete(`/base-holiday/${id}`);
+  return data;
+};

+ 8 - 8
src/components/charts/AniversariantesCard.vue

@@ -1,7 +1,7 @@
 <template>
   <q-card flat class="dashboard-chart-card card-ring">
     <div class="flex justify-between items-center no-wrap q-mb-sm">
-      <span class="text-subtitle2 text-weight-regular">Aniversariantes do Mês</span>
+      <span class="text-subtitle2 text-weight-regular">Aniversariantes do Mês de {{ currentMonth }}</span>
       <q-icon name="mdi-cake-variant-outline" color="secondary" size="sm" />
     </div>
 
@@ -10,7 +10,6 @@
         <q-icon name="mdi-calendar-outline" size="14px" color="secondary" />
         <span>Nome</span>
       </div>
-      <span>Ações</span>
     </div>
     <q-separator />
 
@@ -23,12 +22,6 @@
           <q-item-section>
             <q-item-label class="text-body2">{{ person.name }}</q-item-label>
           </q-item-section>
-          <q-item-section side>
-            <div class="flex items-center no-wrap" style="gap: 4px">
-              <q-btn flat round dense size="sm" icon="mdi-whatsapp" color="dark" />
-              <q-btn flat round dense size="sm" icon="mdi-email-outline" color="dark" />
-            </div>
-          </q-item-section>
         </q-item>
         <q-separator v-if="index < people.length - 1" />
       </template>
@@ -37,6 +30,13 @@
 </template>
 
 <script setup>
+const MONTHS_PT = [
+  'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho',
+  'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro',
+];
+
+const currentMonth = MONTHS_PT[new Date().getMonth()];
+
 defineProps({
   people: {
     type: Array,

+ 135 - 0
src/components/charts/FeriadosCard.vue

@@ -0,0 +1,135 @@
+<template>
+  <q-card flat class="feriados-card card-ring">
+    <div class="flex justify-between items-center no-wrap q-mb-sm">
+      <div class="flex items-center q-gutter-x-sm">
+        <q-icon name="mdi-calendar-star-outline" color="dark" size="sm" />
+        <span class="text-subtitle1">Feriados do Mês</span>
+      </div>
+      <q-btn
+        icon="mdi-plus"
+        color="primary"
+        style="width: 40px; height: 40px"
+        @click="openDialog"
+      />
+    </div>
+
+    <q-separator />
+
+    <div
+      v-if="currentMonthHolidays.length === 0"
+      class="text-caption text-grey-5 text-center q-mt-md"
+    >
+      Sem feriados para este mês.
+    </div>
+
+    <q-list v-else class="q-mt-md" dense>
+      <template v-for="(holiday, index) in currentMonthHolidays" :key="holiday.id">
+        <q-item class="q-px-none person-item cursor-pointer" clickable @click="openEdit(holiday)">
+          <q-item-section avatar style="min-width: 36px">
+            <div class="day-badge">{{ holiday.day }}</div>
+          </q-item-section>
+          <q-item-section>
+            <q-item-label class="text-body2">{{ holiday.description }}</q-item-label>
+            <q-item-label caption>{{ typeLabel(holiday.type) }}</q-item-label>
+          </q-item-section>
+          <q-item-section side>
+            <q-icon name="mdi-pencil-outline" color="grey-5" size="xs" />
+          </q-item-section>
+        </q-item>
+        <q-separator v-if="index < currentMonthHolidays.length - 1" />
+      </template>
+    </q-list>
+  </q-card>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from "vue";
+import { useQuasar } from "quasar";
+import FeriadosDialog from "src/pages/dashboard/components/FeriadosDialog.vue";
+import FeriadosEditDialog from "src/pages/dashboard/components/FeriadosEditDialog.vue";
+import { getBaseHolidays } from "src/api/holiday";
+
+const $q = useQuasar();
+
+const now = new Date();
+const holidays = ref([]);
+
+function toFrontend(h) {
+  const [year, month, day] = h.holiday_date.split("-").map(Number);
+  return { id: h.id, day, month, year, description: h.description, type: h.type };
+}
+
+async function loadHolidays() {
+  try {
+    const items = await getBaseHolidays();
+    holidays.value = items.map(toFrontend);
+  } catch {
+    holidays.value = [];
+  }
+}
+
+onMounted(loadHolidays);
+
+const currentMonthHolidays = computed(() =>
+  holidays.value
+    .filter((h) => h.month === now.getMonth() + 1 && h.year === now.getFullYear())
+    .sort((a, b) => a.day - b.day),
+);
+
+function typeLabel(type) {
+  return type === "facultativo" ? "Ponto Facultativo" : "Feriado";
+}
+
+function openDialog() {
+  $q.dialog({
+    component: FeriadosDialog,
+    componentProps: { initialHolidays: holidays.value },
+  }).onOk((updated) => {
+    holidays.value = updated;
+  });
+}
+
+function openEdit(holiday) {
+  $q.dialog({
+    component: FeriadosEditDialog,
+    componentProps: { holiday },
+  }).onOk(({ action, holiday: updated, id }) => {
+    if (action === "update") {
+      const idx = holidays.value.findIndex((h) => h.id === updated.id);
+      if (idx !== -1) holidays.value[idx] = updated;
+    } else if (action === "delete") {
+      holidays.value = holidays.value.filter((h) => h.id !== id);
+    }
+  });
+}
+</script>
+
+<style scoped>
+.feriados-card {
+  border-radius: 12px;
+  padding: 20px 24px;
+  display: flex;
+  flex-direction: column;
+  max-height: 370px;
+  overflow: hidden;
+}
+
+.person-item {
+  padding-top: 6px;
+  padding-bottom: 6px;
+}
+
+.day-badge {
+  width: 30px;
+  height: 30px;
+  border-radius: 50%;
+  background-color: #e64a19;
+  color: #fff;
+  font-size: 12px;
+  font-weight: 600;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+</style>

+ 35 - 32
src/pages/dashboard/DashboardPage.vue

@@ -85,34 +85,6 @@
         />
       </div>
 
-      <div class="charts-row">
-        <DashboardChartCard title="Faturamento Serviço / Materiais">
-          <GroupedBarChart
-            :labels="faturamentoChart.labels"
-            :datasets="faturamentoChart.datasets"
-            label-y="R$"
-            :tick-formatter="formatCurrencyTick"
-            :tooltip-formatter="formatCurrencyTooltip"
-            class="full-width full-height"
-          />
-        </DashboardChartCard>
-
-        <DashboardChartCard title="Matrículas por Período">
-          <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>
-
-        <AniversariantesCard :people="aniversariantes" />
-      </div>
-
       <div class="stat-cards-row">
         <DashboardStatCard
           title="Frequência Média"
@@ -146,6 +118,35 @@
           @click="openTicketsAbertoDialog"
         />
       </div>
+
+      <div class="charts-row">
+        <DashboardChartCard title="Faturamento Serviço / Materiais">
+          <GroupedBarChart
+            :labels="faturamentoChart.labels"
+            :datasets="faturamentoChart.datasets"
+            label-y="R$"
+            :tick-formatter="formatCurrencyTick"
+            :tooltip-formatter="formatCurrencyTooltip"
+            class="full-width full-height"
+          />
+        </DashboardChartCard>
+
+        <DashboardChartCard title="Matrículas por Período">
+          <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>
+
+        <FeriadosCard />
+        <AniversariantesCard :people="aniversariantes" />
+      </div>
     </div>
 
     <div v-else class="flex flex-center full-width q-pa-xl">
@@ -166,6 +167,7 @@ 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 FeriadosCard from "src/components/charts/FeriadosCard.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";
@@ -471,17 +473,18 @@ onMounted(async () => {
 }
 
 .charts-row > *:nth-child(1) {
-  flex: 0 0 calc(41.6667% - 11px);
+  flex: 0 0 calc(55.5556% - 8px);
   min-width: 0;
 }
 
 .charts-row > *:nth-child(2) {
-  flex: 0 0 calc(33.3333% - 11px);
+  flex: 0 0 calc(44.4444% - 8px);
   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;
 }
 

+ 407 - 0
src/pages/dashboard/components/FeriadosDialog.vue

@@ -0,0 +1,407 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="q-dialog-plugin feriados-dialog">
+      <template v-if="view === 'calendar'">
+        <DefaultDialogHeader title="Feriados" @close="onClose" />
+
+        <q-card-section class="row q-col-gutter-md q-pt-none">
+          <div class="col-12 col-sm-7">
+            <div class="row items-center justify-between q-mb-md">
+              <q-btn
+                flat
+                round
+                dense
+                icon="mdi-chevron-left"
+                color="grey-7"
+                @click="prevMonth"
+              />
+              <div class="row items-center q-gutter-xs">
+                <q-select
+                  v-model="currentMonth"
+                  :options="monthOptions"
+                  emit-value
+                  map-options
+                  dense
+                  outlined
+                  style="min-width: 100px"
+                />
+                <q-select
+                  v-model="currentYear"
+                  :options="yearOptions"
+                  emit-value
+                  map-options
+                  dense
+                  outlined
+                  style="min-width: 80px"
+                />
+              </div>
+              <q-btn
+                flat
+                round
+                dense
+                icon="mdi-chevron-right"
+                color="grey-7"
+                @click="nextMonth"
+              />
+            </div>
+
+            <div class="calendar-grid q-mb-xs">
+              <div
+                v-for="day in weekDays"
+                :key="day"
+                class="cal-header text-caption text-grey-6 text-center text-weight-medium"
+              >
+                {{ day }}
+              </div>
+            </div>
+
+            <div class="calendar-grid">
+              <div
+                v-for="(cell, index) in calendarCells"
+                :key="index"
+                class="cal-cell"
+                :class="{
+                  'cal-cell--empty': !cell.day,
+                  'cal-cell--holiday': cell.isHoliday,
+                }"
+                @click="cell.day && !cell.isHoliday && openNewRecord(cell.day)"
+              >
+                <span v-if="cell.day" class="cal-cell__number">{{ cell.day }}</span>
+              </div>
+            </div>
+          </div>
+
+          <div class="col-12 col-sm-5">
+            <div class="text-subtitle2 q-mb-sm">Resumo</div>
+            <q-separator class="q-mb-sm" />
+
+            <div
+              v-if="monthHolidays.length === 0"
+              class="text-caption text-grey-5 text-center q-mt-lg q-px-sm"
+            >
+              Clique em um dia no calendário para adicionar um registro.
+            </div>
+
+            <q-list v-else separator>
+              <q-item
+                v-for="holiday in monthHolidays"
+                :key="holiday.id"
+                class="q-pa-sm cursor-pointer"
+                clickable
+                @click="openEdit(holiday)"
+              >
+                <q-item-section avatar>
+                  <q-avatar
+                    size="36px"
+                    color="deep-orange"
+                    text-color="white"
+                    class="text-weight-bold"
+                  >
+                    {{ holiday.day }}
+                  </q-avatar>
+                </q-item-section>
+                <q-item-section>
+                  <q-item-label class="text-body2">{{ holiday.description }}</q-item-label>
+                  <q-item-label caption>{{ typeLabel(holiday.type) }}</q-item-label>
+                </q-item-section>
+                <q-item-section side>
+                  <q-icon name="mdi-pencil-outline" color="grey-5" size="xs" />
+                </q-item-section>
+              </q-item>
+            </q-list>
+
+            <div
+              v-if="monthHolidays.length > 0"
+              class="text-caption text-grey-5 text-center q-mt-md q-px-sm"
+            >
+              Clique em um dia no calendário para adicionar um registro.
+            </div>
+          </div>
+        </q-card-section>
+
+        <q-separator />
+
+        <q-card-actions align="right">
+          <q-btn outline color="primary" label="FECHAR" no-caps @click="onClose" />
+        </q-card-actions>
+      </template>
+
+      <template v-else>
+        <DefaultDialogHeader
+          :title="`Novo Registro: ${formattedSelectedDate}`"
+          @close="cancelNewRecord"
+        />
+
+        <q-card-section class="column q-gutter-md q-pt-sm">
+          <DefaultInput
+            v-model="newDescription"
+            label="Nome do Evento"
+            placeholder="Ex: Ponto facultativo, Natal..."
+            icon="mdi-pencil-outline"
+            outlined
+            autofocus
+          />
+
+          <DefaultSelect
+            v-model="newType"
+            label="Selecione o Tipo."
+            :options="typeOptions"
+            emit-value
+            map-options
+            outlined
+          />
+        </q-card-section>
+
+        <q-separator />
+
+        <q-card-actions align="right">
+          <q-btn outline color="primary" label="CANCELAR" no-caps @click="cancelNewRecord" />
+          <q-btn
+            unelevated
+            color="primary"
+            label="SALVAR"
+            no-caps
+            :disable="!newDescription.trim()"
+            :loading="saving"
+            @click="saveNewRecord"
+          />
+        </q-card-actions>
+      </template>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, computed } from "vue";
+import { useDialogPluginComponent, useQuasar } from "quasar";
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
+import DefaultInput from "src/components/defaults/DefaultInput.vue";
+import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
+import { createBaseHoliday } from "src/api/holiday";
+import FeriadosEditDialog from "src/pages/dashboard/components/FeriadosEditDialog.vue";
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
+const $q = useQuasar();
+
+const now = new Date();
+const currentMonth = ref(now.getMonth() + 1);
+const currentYear = ref(now.getFullYear());
+
+const view = ref("calendar");
+const selectedDay = ref(null);
+const newDescription = ref("");
+const newType = ref("feriado");
+const saving = ref(false);
+
+const props = defineProps({
+  initialHolidays: {
+    type: Array,
+    default: () => [],
+  },
+});
+
+const holidays = ref([...props.initialHolidays]);
+
+const weekDays = ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"];
+
+const monthOptions = [
+  { label: "Jan", value: 1 },
+  { label: "Fev", value: 2 },
+  { label: "Mar", value: 3 },
+  { label: "Abr", value: 4 },
+  { label: "Mai", value: 5 },
+  { label: "Jun", value: 6 },
+  { label: "Jul", value: 7 },
+  { label: "Ago", value: 8 },
+  { label: "Set", value: 9 },
+  { label: "Out", value: 10 },
+  { label: "Nov", value: 11 },
+  { label: "Dez", value: 12 },
+];
+
+const typeOptions = [
+  { label: "Feriado", value: "feriado" },
+  { label: "Ponto Facultativo", value: "facultativo" },
+];
+
+const yearOptions = computed(() => {
+  const base = now.getFullYear();
+  return Array.from({ length: 7 }, (_, i) => {
+    const y = base - 2 + i;
+    return { label: String(y), value: y };
+  });
+});
+
+const monthHolidays = computed(() =>
+  holidays.value
+    .filter((h) => h.month === currentMonth.value && h.year === currentYear.value)
+    .sort((a, b) => a.day - b.day),
+);
+
+const calendarCells = computed(() => {
+  const year = currentYear.value;
+  const month = currentMonth.value;
+  const firstDay = new Date(year, month - 1, 1).getDay();
+  const daysInMonth = new Date(year, month, 0).getDate();
+  const holidayDays = new Set(monthHolidays.value.map((h) => h.day));
+
+  const cells = [];
+  for (let i = 0; i < firstDay; i++) cells.push({ day: null, isHoliday: false });
+  for (let d = 1; d <= daysInMonth; d++) cells.push({ day: d, isHoliday: holidayDays.has(d) });
+  return cells;
+});
+
+const formattedSelectedDate = computed(() => {
+  if (!selectedDay.value) return "";
+  const dd = String(selectedDay.value).padStart(2, "0");
+  const mm = String(currentMonth.value).padStart(2, "0");
+  return `${dd}/${mm}/${currentYear.value}`;
+});
+
+function typeLabel(type) {
+  return type === "facultativo" ? "Ponto Facultativo" : "Feriado";
+}
+
+function prevMonth() {
+  if (currentMonth.value === 1) {
+    currentMonth.value = 12;
+    currentYear.value -= 1;
+  } else {
+    currentMonth.value -= 1;
+  }
+}
+
+function nextMonth() {
+  if (currentMonth.value === 12) {
+    currentMonth.value = 1;
+    currentYear.value += 1;
+  } else {
+    currentMonth.value += 1;
+  }
+}
+
+function openNewRecord(day) {
+  selectedDay.value = day;
+  newDescription.value = "";
+  newType.value = "feriado";
+  view.value = "new-record";
+}
+
+function cancelNewRecord() {
+  view.value = "calendar";
+  selectedDay.value = null;
+}
+
+async function saveNewRecord() {
+  const desc = newDescription.value.trim();
+  if (!desc || selectedDay.value === null) return;
+
+  const mm = String(currentMonth.value).padStart(2, "0");
+  const dd = String(selectedDay.value).padStart(2, "0");
+
+  saving.value = true;
+  try {
+    const created = await createBaseHoliday({
+      description: desc,
+      holiday_date: `${currentYear.value}-${mm}-${dd}`,
+      type: newType.value,
+    });
+
+    holidays.value.push({
+      id: created.id,
+      day: selectedDay.value,
+      month: currentMonth.value,
+      year: currentYear.value,
+      description: created.description,
+      type: created.type,
+    });
+
+    view.value = "calendar";
+    selectedDay.value = null;
+  } catch {
+    $q.notify({ type: "negative", message: "Erro ao salvar feriado." });
+  } finally {
+    saving.value = false;
+  }
+}
+
+function openEdit(holiday) {
+  $q.dialog({
+    component: FeriadosEditDialog,
+    componentProps: { holiday },
+  }).onOk(({ action, holiday: updated, id }) => {
+    if (action === "update") {
+      const idx = holidays.value.findIndex((h) => h.id === updated.id);
+      if (idx !== -1) holidays.value[idx] = updated;
+    } else if (action === "delete") {
+      holidays.value = holidays.value.filter((h) => h.id !== id);
+    }
+  });
+}
+
+function onClose() {
+  onDialogOK(holidays.value);
+}
+</script>
+
+<style scoped>
+.feriados-dialog {
+  width: 900px;
+  max-width: 95vw;
+}
+
+.calendar-grid {
+  display: grid;
+  grid-template-columns: repeat(7, 1fr);
+  gap: 2px;
+}
+
+.cal-header {
+  padding: 6px 0 4px;
+  font-weight: 600;
+  text-align: center;
+}
+
+.cal-cell {
+  aspect-ratio: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 50%;
+  cursor: pointer;
+  transition: background 0.15s;
+}
+
+.cal-cell:hover:not(.cal-cell--empty):not(.cal-cell--holiday) {
+  background: #f5f5f5;
+}
+
+.cal-cell--empty {
+  cursor: default;
+  pointer-events: none;
+}
+
+.cal-cell__number {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 32px;
+  height: 32px;
+  border-radius: 50%;
+  font-size: 13px;
+  font-weight: 500;
+  line-height: 1;
+}
+
+.cal-cell--holiday .cal-cell__number {
+  background: #e64a19;
+  color: #fff;
+  font-weight: 700;
+}
+
+.cal-cell--holiday {
+  cursor: default;
+}
+</style>

+ 145 - 0
src/pages/dashboard/components/FeriadosEditDialog.vue

@@ -0,0 +1,145 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="q-dialog-plugin feriados-edit-dialog">
+      <DefaultDialogHeader :title="dialogTitle" @close="onDialogCancel" />
+
+      <q-card-section class="column q-gutter-md q-pt-sm">
+        <DefaultInput
+          v-model="description"
+          label="Nome do Evento"
+          placeholder="Ex: Ponto facultativo, Natal..."
+          icon="mdi-pencil-outline"
+          outlined
+          autofocus
+        />
+
+        <DefaultSelect
+          v-model="type"
+          label="Selecione o Tipo."
+          :options="typeOptions"
+          emit-value
+          map-options
+          outlined
+        />
+      </q-card-section>
+
+      <q-separator />
+
+      <q-card-actions align="between">
+        <q-btn
+          outline
+          color="negative"
+          label="EXCLUIR"
+          no-caps
+          :loading="deleting"
+          @click="confirmDelete"
+        />
+        <div class="row q-gutter-sm">
+          <q-btn outline color="primary" label="CANCELAR" no-caps @click="onDialogCancel" />
+          <q-btn
+            unelevated
+            color="primary"
+            label="SALVAR"
+            no-caps
+            :disable="!description.trim()"
+            :loading="saving"
+            @click="save"
+          />
+        </div>
+      </q-card-actions>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, computed } from "vue";
+import { useDialogPluginComponent, useQuasar } from "quasar";
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
+import DefaultInput from "src/components/defaults/DefaultInput.vue";
+import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
+import { updateBaseHoliday, deleteBaseHoliday } from "src/api/holiday";
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+const $q = useQuasar();
+
+const props = defineProps({
+  holiday: {
+    type: Object,
+    required: true,
+  },
+});
+
+const description = ref(props.holiday.description);
+const type = ref(props.holiday.type ?? "feriado");
+const saving = ref(false);
+const deleting = ref(false);
+
+const typeOptions = [
+  { label: "Feriado", value: "feriado" },
+  { label: "Ponto Facultativo", value: "facultativo" },
+];
+
+const dialogTitle = computed(() => {
+  const dd = String(props.holiday.day).padStart(2, "0");
+  const mm = String(props.holiday.month).padStart(2, "0");
+  return `${dd}/${mm}/${props.holiday.year}`;
+});
+
+async function save() {
+  const desc = description.value.trim();
+  if (!desc) return;
+
+  const mm = String(props.holiday.month).padStart(2, "0");
+  const dd = String(props.holiday.day).padStart(2, "0");
+
+  saving.value = true;
+  try {
+    const updated = await updateBaseHoliday(props.holiday.id, {
+      description: desc,
+      holiday_date: `${props.holiday.year}-${mm}-${dd}`,
+      type: type.value,
+    });
+
+    onDialogOK({
+      action: "update",
+      holiday: {
+        ...props.holiday,
+        description: updated.description,
+        type: updated.type,
+      },
+    });
+  } catch {
+    $q.notify({ type: "negative", message: "Erro ao salvar feriado." });
+  } finally {
+    saving.value = false;
+  }
+}
+
+function confirmDelete() {
+  $q.dialog({
+    title: "Excluir feriado",
+    message: `Deseja excluir "${props.holiday.description}"?`,
+    cancel: { label: "Cancelar", flat: true, color: "primary" },
+    ok: { label: "Excluir", unelevated: true, color: "negative" },
+  }).onOk(async () => {
+    deleting.value = true;
+    try {
+      await deleteBaseHoliday(props.holiday.id);
+      onDialogOK({ action: "delete", id: props.holiday.id });
+    } catch {
+      $q.notify({ type: "negative", message: "Erro ao excluir feriado." });
+    } finally {
+      deleting.value = false;
+    }
+  });
+}
+</script>
+
+<style scoped>
+.feriados-edit-dialog {
+  width: 420px;
+  max-width: 95vw;
+}
+</style>