Alpha v0.1.0 — KAPPA Hub inicial
- Auth con KAPPA (login + token Bearer) - Cliente HTTP para 10 endpoints (proyectos, HUs, bitácoras, planeaciones) - Dashboard multi-proyecto con concepto médico Teloprax - Calendario colombiano con 19 feriados (Ley Emiliani + Pascua) - Scheduler tipo cron con Dexie (reglas recurrentes, toasts, log) - Diseño marca Teloprax: Inter, Space Grotesk, #1A1A2E, rojo #E63946 - Stack: Vue 3 + TypeScript + Pinia + Vite + Bun
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { getColombianHolidays, isHoliday } from '@/services/calendar'
|
||||
|
||||
const today = new Date()
|
||||
const currentYear = ref(today.getFullYear())
|
||||
const currentMonth = ref(today.getMonth())
|
||||
const selectedDate = ref<Date | null>(null)
|
||||
|
||||
const MONTHS = [
|
||||
'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
|
||||
'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre',
|
||||
]
|
||||
const WEEKDAYS = ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb']
|
||||
|
||||
const holidays = computed(() => {
|
||||
const h = getColombianHolidays(currentYear.value)
|
||||
return new Map(h.map(h => [`${h.date.getFullYear()}-${h.date.getMonth()}-${h.date.getDate()}`, h.name]))
|
||||
})
|
||||
|
||||
const daysInMonth = computed(() => {
|
||||
const year = currentYear.value
|
||||
const month = currentMonth.value
|
||||
const firstDay = new Date(year, month, 1).getDay()
|
||||
const totalDays = new Date(year, month + 1, 0).getDate()
|
||||
|
||||
return Array.from({ length: firstDay + totalDays }, (_, i) => {
|
||||
if (i < firstDay) return { day: null, isToday: false, isHoliday: false, isWeekend: false, isSelected: false, holidayName: undefined as string | undefined }
|
||||
const d = i - firstDay + 1
|
||||
const date = new Date(year, month, d)
|
||||
const key = `${year}-${month}-${d}`
|
||||
const h = holidays.value.get(key)
|
||||
const dow = date.getDay()
|
||||
return {
|
||||
day: d,
|
||||
isToday: d === today.getDate() && month === today.getMonth() && year === today.getFullYear(),
|
||||
isHoliday: !!h || dow === 0,
|
||||
holidayName: h,
|
||||
isWeekend: dow === 0 || dow === 6,
|
||||
isSelected: selectedDate.value
|
||||
? selectedDate.value.getDate() === d && selectedDate.value.getMonth() === month && selectedDate.value.getFullYear() === year
|
||||
: false,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const selectedHoliday = computed(() => {
|
||||
if (!selectedDate.value) return null
|
||||
const h = isHoliday(selectedDate.value)
|
||||
return h.holiday ? h.name : null
|
||||
})
|
||||
|
||||
function isWorkingDay(date: Date): boolean {
|
||||
const dow = date.getDay()
|
||||
if (dow === 0 || dow === 6) return false
|
||||
return !isHoliday(date).holiday
|
||||
}
|
||||
|
||||
function prevMonth() {
|
||||
if (currentMonth.value === 0) { currentMonth.value = 11; currentYear.value-- }
|
||||
else currentMonth.value--
|
||||
}
|
||||
|
||||
function nextMonth() {
|
||||
if (currentMonth.value === 11) { currentMonth.value = 0; currentYear.value++ }
|
||||
else currentMonth.value++
|
||||
}
|
||||
|
||||
function selectDay(day: number | null) {
|
||||
if (day !== null) selectedDate.value = new Date(currentYear.value, currentMonth.value, day)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cal-view">
|
||||
<div class="cal-nav">
|
||||
<button @click="prevMonth" class="cal-nav-btn">←</button>
|
||||
<h2>{{ MONTHS[currentMonth] }} {{ currentYear }}</h2>
|
||||
<button @click="nextMonth" class="cal-nav-btn">→</button>
|
||||
<button class="cal-today" @click="currentMonth = today.getMonth(); currentYear = today.getFullYear()">Hoy</button>
|
||||
</div>
|
||||
|
||||
<div class="cal-grid">
|
||||
<div v-for="w in WEEKDAYS" :key="w" class="cal-header">{{ w }}</div>
|
||||
<div
|
||||
v-for="(d, i) in daysInMonth"
|
||||
:key="i"
|
||||
:class="['cal-day', { empty: d.day === null, today: d.isToday, weekend: d.isWeekend && !d.isHoliday, holiday: d.isHoliday, selected: d.isSelected }]"
|
||||
@click="selectDay(d.day)"
|
||||
>
|
||||
<span v-if="d.day" class="cal-num">{{ d.day }}</span>
|
||||
<span v-if="d.holidayName" class="cal-hol">{{ d.holidayName.slice(0, 3) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cal-legend">
|
||||
<span><span class="leg-dot w-day"></span> Laboral</span>
|
||||
<span><span class="leg-dot w-end"></span> Fin de semana</span>
|
||||
<span><span class="leg-dot w-hol"></span> Feriado</span>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedDate" class="cal-info">
|
||||
<h3>{{ selectedDate.toLocaleDateString('es-CO', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) }}</h3>
|
||||
<p v-if="selectedHoliday">Feriado: <strong>{{ selectedHoliday }}</strong></p>
|
||||
<p v-else-if="isWorkingDay(selectedDate)">Día laboral</p>
|
||||
<p v-else>Fin de semana</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cal-nav { display: flex; align-items: center; gap: 10px; margin-bottom: 18px; }
|
||||
.cal-nav h2 { margin: 0; font-size: 18px; color: var(--text-primary); min-width: 180px; font-weight: 700; }
|
||||
.cal-nav-btn {
|
||||
padding: 5px 14px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
.cal-nav-btn:hover { color: var(--text-primary); border-color: var(--border-hover); }
|
||||
.cal-today { margin-left: auto; padding: 5px 14px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); color: var(--accent); cursor: pointer; font-size: 13px; font-weight: 500; }
|
||||
.cal-today:hover { background: var(--bg-tertiary); }
|
||||
|
||||
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
|
||||
.cal-header { padding: 9px 4px; text-align: center; font-size: 11px; font-weight: 600; color: var(--text-muted); background: var(--bg-secondary); text-transform: uppercase; letter-spacing: 0.3px; }
|
||||
.cal-day { aspect-ratio: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 4px; background: var(--bg-primary); cursor: pointer; transition: background 0.1s; }
|
||||
.cal-day:hover { background: var(--bg-tertiary); }
|
||||
.cal-day.empty { cursor: default; background: var(--bg-primary); }
|
||||
.cal-day.today { background: #1E1040; }
|
||||
.cal-day.today .cal-num { color: var(--accent); font-weight: 700; }
|
||||
.cal-day.weekend { color: var(--text-muted); background: #161630; }
|
||||
.cal-day.holiday { color: var(--accent); }
|
||||
.cal-day.holiday .cal-hol { font-size: 8px; opacity: 0.8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
|
||||
.cal-day.selected { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 2px; }
|
||||
.cal-num { font-size: 14px; font-weight: 500; }
|
||||
|
||||
.cal-legend { display: flex; gap: 18px; margin-top: 12px; font-size: 11px; color: var(--text-muted); }
|
||||
.leg-dot { display: inline-block; width: 9px; height: 9px; border-radius: 2px; margin-right: 6px; vertical-align: -1px; }
|
||||
.w-day { background: var(--border); }
|
||||
.w-end { background: #161630; }
|
||||
.w-hol { background: var(--accent); }
|
||||
|
||||
.cal-info { margin-top: 22px; padding: 18px 20px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); }
|
||||
.cal-info h3 { margin: 0 0 6px; font-size: 15px; color: var(--text-primary); text-transform: capitalize; }
|
||||
.cal-info p { margin: 0; font-size: 13px; color: var(--text-secondary); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user