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:
2026-05-22 20:18:54 -05:00
commit 66fd4e175a
26 changed files with 2227 additions and 0 deletions
+230
View File
@@ -0,0 +1,230 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useProjectsStore } from '@/stores/projects'
import { useAuthStore } from '@/stores/auth'
import DashboardView from '@/views/DashboardView.vue'
import CalendarView from '@/views/CalendarView.vue'
import SchedulerView from '@/views/SchedulerView.vue'
const projects = useProjectsStore()
const auth = useAuthStore()
const activeTab = ref<'dashboard' | 'calendar' | 'scheduler'>('dashboard')
const tabs = [
{ id: 'dashboard' as const, label: 'Diagnóstico' },
{ id: 'calendar' as const, label: 'Calendario' },
{ id: 'scheduler' as const, label: 'Recetas' },
]
onMounted(() => {
projects.fetchProjects()
})
</script>
<template>
<div class="shell">
<aside class="sidebar">
<div class="sb-brand">
<div class="sb-circles">
<span class="c c1"></span>
<span class="c c2"></span>
<span class="c c3"></span>
</div>
<div class="sb-wordmark">
<span class="wm-name">teloprax</span>
<span class="wm-tag">Tecnología con prescripción</span>
</div>
</div>
<div class="sb-nav">
<button
v-for="t in tabs"
:key="t.id"
:class="['sb-nav-item', { active: activeTab === t.id }]"
@click="activeTab = t.id"
>
{{ t.label }}
</button>
</div>
<div class="sb-projects">
<div class="sb-label">Pacientes ({{ projects.count }})</div>
<button
v-for="p in projects.projects"
:key="p.id"
:class="['sb-item', { active: projects.selectedId === p.id }]"
@click="projects.select(p.id); activeTab = 'dashboard'"
>
<span class="sb-dot"></span>
{{ p.name }}
</button>
</div>
<div class="sb-footer">
<div class="sb-user">{{ auth.user?.name }}</div>
<button class="sb-logout" @click="auth.logout">Salir</button>
</div>
</aside>
<main class="content">
<DashboardView v-if="activeTab === 'dashboard' && projects.selected" />
<div v-else-if="activeTab === 'dashboard' && !projects.selected" class="empty">
<p v-if="projects.loading">Cargando pacientes...</p>
<p v-else-if="projects.error" class="err">{{ projects.error }}</p>
<p v-else>Seleccioná un paciente del panel</p>
</div>
<CalendarView v-else-if="activeTab === 'calendar'" />
<SchedulerView v-else-if="activeTab === 'scheduler'" />
</main>
</div>
</template>
<style scoped>
.shell {
display: flex;
height: 100vh;
background: #1A1A2E;
}
.sidebar {
width: 260px;
background: #141428;
border-right: 1px solid #2A2A45;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
/* Brand */
.sb-brand {
display: flex;
align-items: center;
gap: 12px;
padding: 18px 20px;
border-bottom: 1px solid #2A2A45;
}
.sb-circles {
display: flex;
align-items: center;
gap: 3px;
flex-shrink: 0;
}
.c {
display: block;
border-radius: 50%;
border: 2px solid #E63946;
}
.c1 { width: 8px; height: 8px; opacity: 0.4; }
.c2 { width: 11px; height: 11px; opacity: 0.7; }
.c3 { width: 14px; height: 14px; background: #E63946; border: none; }
.sb-wordmark {
display: flex;
flex-direction: column;
min-width: 0;
}
.wm-name {
font-family: 'Space Grotesk', sans-serif;
font-weight: 700;
font-size: 17px;
color: #FFFFFF;
letter-spacing: -0.3px;
}
.wm-tag {
font-size: 10px;
color: #8888AA;
white-space: nowrap;
}
/* Nav */
.sb-nav {
padding: 8px 0;
border-bottom: 1px solid #2A2A45;
}
.sb-nav-item {
display: block;
width: 100%;
padding: 9px 20px;
background: none;
border: none;
border-left: 2px solid transparent;
color: #8888AA;
font-size: 13px;
cursor: pointer;
text-align: left;
transition: all 0.15s;
}
.sb-nav-item:hover { background: rgba(230,57,70,0.05); color: #E6EDF3; }
.sb-nav-item.active {
background: rgba(230,57,70,0.08);
color: #E63946;
border-left-color: #E63946;
font-weight: 600;
}
/* Projects */
.sb-projects { flex: 1; overflow-y: auto; padding: 8px 0; }
.sb-label {
font-size: 10px;
color: #666688;
text-transform: uppercase;
letter-spacing: 0.8px;
padding: 10px 20px 6px;
font-weight: 600;
}
.sb-item {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 7px 20px;
background: none;
border: none;
color: #8888AA;
font-size: 13px;
cursor: pointer;
text-align: left;
}
.sb-item:hover { background: #1E1E36; color: #E6EDF3; }
.sb-item.active { background: #1E1E36; color: #E6EDF3; font-weight: 500; }
.sb-dot {
width: 6px; height: 6px;
border-radius: 50%;
background: #2A2A45;
flex-shrink: 0;
}
.sb-item.active .sb-dot { background: #E63946; }
/* Footer */
.sb-footer {
padding: 12px 20px;
border-top: 1px solid #2A2A45;
display: flex;
align-items: center;
justify-content: space-between;
}
.sb-user { font-size: 12px; color: #8888AA; }
.sb-logout {
background: none;
border: none;
color: #555577;
font-size: 11px;
cursor: pointer;
}
.sb-logout:hover { color: #E63946; }
/* Content */
.content {
flex: 1;
overflow-y: auto;
padding: 36px;
}
.empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #8888AA;
font-size: 14px;
}
.err { color: #F85149; }
</style>