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,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>
|
||||
Reference in New Issue
Block a user