reorganizar sidebar: Métricas + Proyectos reales de KAPPA, eliminar Tablero duplicado

This commit is contained in:
Ricardo Gonzalez
2026-05-25 21:11:11 -05:00
parent d0ace4515d
commit 87808577b5
8 changed files with 200 additions and 41 deletions
+44 -5
View File
@@ -23,6 +23,43 @@ El proyecto vive en iCloud Drive: `com~apple~CloudDocs/AI/Teloprax/02_productos/
- **Token**: localStorage del navegador. Loguearse una vez por máquina.
- **Datos locales futuros** (borradores, caché): se guardarán como archivos en `data/` dentro del proyecto, sincronizados vía iCloud. Ver `../rumbo/sincronizacion.md`.
## Calendarios externos (K-21)
| Servicio | Auth | Capacidad |
|----------|------|-----------|
| Google Calendar | OAuth2 | Leer eventos, escribir eventos |
| Microsoft Graph (Outlook/Teams) | OAuth2 | Leer eventos, escribir eventos |
| iCal (.ics) | Archivo | Importar/exportar |
### Flujo post-reunión (K-22)
```
1. RUMBO lee calendario (Google/Outlook)
2. Detecta: "Reunión con Cliente X, 8-9am"
3. Después de la reunión (+15/30 min):
→ 🔔 "Terminó tu reunión con Cliente X"
→ "¿Ya capturaste las notas?"
→ "¿Hay transcripción?"
→ "¿Se extrajeron las tareas?"
4. Si no → RUMBO guía a completar
```
**Analogía**: RUMBO como Apple Watch — te alerta cuando llevas mucho tiempo sentado (reuniones sin seguimiento = deuda de contexto).
### API de recordatorios (futuro)
```typescript
interface Reminder {
id: string
trigger_at: string // ISO datetime
title: string
body: string
action_url?: string // deep link a la vista correspondiente
dismissed: boolean
source: 'calendar' | 'scheduler' | 'hu_deadline'
}
```
## APIs KAPPA integradas
| Endpoint | Método | Uso en el hub |
@@ -72,8 +109,10 @@ Abre http://localhost:5173. El proxy de Vite redirige `/api/*` a `https://kappa.
## Próximos pasos
1. Agregar Dexie.js para cache offline de proyectos y HUs
2. Pipeline de transcripciones (.docx/.vtt/.md → análisis → HU)
3. Dashboard multi-proyecto con resumen unificado
4. Priorizador diario (¿qué hacer hoy?)
5. Generador de reportes de estado
1. ~~Agregar Dexie.js para cache offline~~ (K-15)
2. ~~Pipeline de transcripciones~~ (K-10)
3. ~~Dashboard multi-proyecto~~ (K-11)
4. ~~Priorizador diario~~ (K-12)
5. ~~Generador de reportes~~ (K-13)
6. **Integración calendario Google/Outlook** (K-21)
7. **Alertas post-reunión** (K-22)
-13
View File
@@ -1,17 +1,4 @@
<script setup lang="ts">
import {
IconChartBar,
IconDashboard,
IconDatabase,
IconFileAi,
IconFileDescription,
IconFolder,
IconListDetails,
IconReport,
IconSettings,
IconUsers,
} from "@tabler/icons-vue"
import NavDocuments from "@/components/dashboard/NavDocuments.vue"
import NavMain from "@/components/dashboard/NavMain.vue"
import NavSecondary from "@/components/dashboard/NavSecondary.vue"
+2 -2
View File
@@ -3,7 +3,7 @@ import type { Component } from "vue"
import { useI18n } from "vue-i18n"
import {
IconCirclePlusFilled,
IconDashboard,
IconLayoutKanban,
IconFolder,
IconListDetails,
IconChartBar,
@@ -21,7 +21,7 @@ import {
const { t } = useI18n()
const mainNavItems = [
{ title: 'nav.dashboard', icon: IconDashboard, id: 'dashboard' },
{ title: 'nav.board', icon: IconLayoutKanban, id: 'metrics' },
{ title: 'nav.projects', icon: IconFolder, id: 'projects' },
{ title: 'nav.lifecycle', icon: IconListDetails, id: 'lifecycle' },
{ title: 'nav.analytics', icon: IconChartBar, id: 'analytics' },
+1 -1
View File
@@ -28,7 +28,7 @@ const props = defineProps<{
}>()
const tabLabels: Record<string, string> = {
dashboard: 'Tablero',
metrics: 'Métricas',
projects: 'Proyectos',
lifecycle: 'Ciclo de Vida',
analytics: 'Analíticas',
+1 -1
View File
@@ -1,7 +1,7 @@
{
"nav": {
"quickCreate": "Create project",
"dashboard": "Dashboard",
"board": "Metrics",
"projects": "Projects",
"lifecycle": "Lifecycle",
"analytics": "Analytics",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"nav": {
"quickCreate": "Crear proyecto",
"dashboard": "Tablero",
"board": "Métricas",
"projects": "Proyectos",
"lifecycle": "Ciclo de vida",
"analytics": "Analíticas",
+36 -18
View File
@@ -5,28 +5,28 @@ import AppSidebar from "@/components/dashboard/AppSidebar.vue"
import SiteHeader from "@/components/dashboard/SiteHeader.vue"
import SectionCards from "@/components/dashboard/SectionCards.vue"
import DashboardView from "@/views/DashboardView.vue"
import ProjectListView from "@/views/ProjectListView.vue"
const sidebarStyle = {
'--sidebar-width': '16rem',
'--header-height': '3rem',
}
const activeTab = ref('dashboard')
const activeTab = ref('metrics')
const viewingProject = ref(false)
function openProject() {
viewingProject.value = true
}
function backToProjects() {
viewingProject.value = false
}
const tabContent: Record<string, { title: string; description: string; cards: { label: string; value: string; trend: string; up: boolean }[] }> = {
dashboard: {
title: 'Tablero',
description: 'Resumen general del proyecto',
cards: [
{ label: 'Total Revenue', value: '$1,250.00', trend: '+12.5%', up: true },
{ label: 'New Customers', value: '1,234', trend: '-20%', up: false },
{ label: 'Active Accounts', value: '45,678', trend: '+12.5%', up: true },
{ label: 'Growth Rate', value: '4.5%', trend: '+4.5%', up: true },
],
},
projects: {
title: 'Proyectos',
description: 'Gestión de proyectos activos',
metrics: {
title: 'Métricas',
description: 'Indicadores y KPIs generales',
cards: [
{ label: 'Proyectos activos', value: '12', trend: '+3', up: true },
{ label: 'Completados', value: '48', trend: '+8', up: true },
@@ -34,6 +34,11 @@ const tabContent: Record<string, { title: string; description: string; cards: {
{ label: 'Por iniciar', value: '7', trend: '+1', up: true },
],
},
projects: {
title: 'Proyectos',
description: 'Proyectos asignados en KAPPA',
cards: [],
},
lifecycle: {
title: 'Ciclo de Vida',
description: 'Seguimiento del ciclo de vida de los proyectos',
@@ -120,17 +125,30 @@ const tabContent: Record<string, { title: string; description: string; cards: {
<template>
<SidebarProvider :style="sidebarStyle">
<AppSidebar
:active-tab="activeTab"
@update:active-tab="activeTab = $event"
:active-tab="viewingProject ? 'projects' : activeTab"
@update:active-tab="activeTab = $event; viewingProject = false"
/>
<SidebarInset>
<SiteHeader :active-tab="activeTab" />
<SiteHeader :active-tab="viewingProject ? 'projects' : activeTab" />
<div class="flex flex-1 flex-col">
<div class="@container/main flex flex-1 flex-col gap-2">
<div class="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
<template v-if="activeTab === 'dashboard'">
<template v-if="viewingProject">
<div class="px-4 lg:px-6">
<button
class="mb-4 inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
@click="backToProjects"
>
Volver a Proyectos
</button>
</div>
<DashboardView />
</template>
<template v-else-if="activeTab === 'projects'">
<ProjectListView
@select-project="openProject()"
/>
</template>
<template v-else>
<SectionCards :cards="tabContent[activeTab]?.cards ?? []" />
</template>
+115
View File
@@ -0,0 +1,115 @@
<script setup lang="ts">
import { onMounted } from "vue"
import { useProjectsStore } from "@/stores/projects"
import { IconFolder, IconExclamationCircle } from "@tabler/icons-vue"
import {
Card,
CardHeader,
CardTitle,
CardDescription,
} from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Skeleton } from "@/components/ui/skeleton"
const projects = useProjectsStore()
const emit = defineEmits<{
'select-project': [id: number]
}>()
onMounted(() => {
projects.fetchProjects()
})
function getStatusVariant(status?: string) {
switch (status) {
case 'active': return 'default'
case 'completed': return 'secondary'
case 'paused': return 'outline'
default: return 'default'
}
}
</script>
<template>
<div class="flex flex-col gap-6 px-4 lg:px-6">
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold tracking-tight">Proyectos</h2>
<p class="text-muted-foreground">
Proyectos asignados en KAPPA
</p>
</div>
<Badge variant="outline" class="text-sm">
{{ projects.count }} proyecto{{ projects.count !== 1 ? 's' : '' }}
</Badge>
</div>
<!-- Loading -->
<div v-if="projects.loading" class="grid grid-cols-1 gap-4 @lg:grid-cols-2 @3xl:grid-cols-3">
<Card v-for="i in 6" :key="i">
<CardHeader>
<Skeleton class="h-5 w-3/4" />
<Skeleton class="h-4 w-1/2" />
</CardHeader>
</Card>
</div>
<!-- Error -->
<div v-else-if="projects.error" class="flex flex-col items-center gap-3 py-12 text-center">
<IconExclamationCircle class="size-10 text-destructive" />
<p class="text-lg font-medium">Error al cargar proyectos</p>
<p class="text-sm text-muted-foreground">{{ projects.error }}</p>
<button
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
@click="projects.fetchProjects()"
>
Reintentar
</button>
</div>
<!-- Empty -->
<div v-else-if="projects.count === 0" class="flex flex-col items-center gap-3 py-12 text-center">
<IconFolder class="size-10 text-muted-foreground" />
<p class="text-lg font-medium">Sin proyectos asignados</p>
<p class="text-sm text-muted-foreground">
No tienes proyectos activos en KAPPA.
</p>
</div>
<!-- Project Grid -->
<div v-else class="grid grid-cols-1 gap-4 @lg:grid-cols-2 @3xl:grid-cols-3">
<Card
v-for="p in projects.projects"
:key="p.id"
:class="[
'cursor-pointer transition-colors hover:border-primary/50',
projects.selectedId === p.id ? 'border-primary' : '',
]"
@click="projects.select(p.id); emit('select-project', p.id)"
>
<CardHeader>
<div class="flex items-start justify-between gap-2">
<CardTitle class="text-base">
{{ p.initiative_name || p.name || `Proyecto ${p.id}` }}
</CardTitle>
<Badge :variant="getStatusVariant(p.status)">
{{ p.status || 'active' }}
</Badge>
</div>
<CardDescription class="line-clamp-2">
{{ p.description || 'Sin descripción' }}
</CardDescription>
<div v-if="p.key" class="flex items-center gap-2 pt-1">
<Badge variant="secondary" class="font-mono text-xs">
{{ p.key }}
</Badge>
<span v-if="p.start_date" class="text-xs text-muted-foreground">
{{ p.start_date }}
</span>
</div>
</CardHeader>
</Card>
</div>
</div>
</template>