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
+40
View File
@@ -0,0 +1,40 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { kappa } from '@/services/kappa-api'
import type { KappaLoginPayload } from '@/types/kappa'
export const useAuthStore = defineStore('auth', () => {
const token = ref<string | null>(localStorage.getItem('kappa_token'))
const user = ref<{ name: string; email: string } | null>(null)
const loading = ref(false)
const error = ref<string | null>(null)
const isAuthenticated = computed(() => !!token.value)
async function login(payload: KappaLoginPayload) {
loading.value = true
error.value = null
try {
const data = await kappa.login(payload)
token.value = kappa['token'] // sync with the service instance
user.value = {
name: `${data.user?.first_name || ''} ${data.user?.last_name || ''}`.trim(),
email: payload.email,
}
return true
} catch (e: any) {
error.value = e.message
return false
} finally {
loading.value = false
}
}
function logout() {
kappa.logout()
token.value = null
user.value = null
}
return { token, user, loading, error, isAuthenticated, login, logout }
})
+36
View File
@@ -0,0 +1,36 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { kappa } from '@/services/kappa-api'
import type { KappaInitiative } from '@/types/kappa'
export const useProjectsStore = defineStore('projects', () => {
const projects = ref<KappaInitiative[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const selectedId = ref<number | null>(null)
const selected = computed(() =>
projects.value.find(p => p.id === selectedId.value) ?? null
)
const count = computed(() => projects.value.length)
async function fetchProjects() {
loading.value = true
error.value = null
try {
projects.value = await kappa.getInitiatives()
} catch (e: any) {
error.value = e.message
} finally {
loading.value = false
}
}
function select(id: number) {
selectedId.value = id
localStorage.setItem('kappa_last_project', String(id))
}
return { projects, loading, error, selectedId, selected, count, fetchProjects, select }
})
+101
View File
@@ -0,0 +1,101 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import {
db, startScheduler, stopScheduler, onScheduledTask,
computeNextRun, DEFAULT_RULES,
type ScheduleRule, type ScheduleAction, type ExecutionLog,
} from '@/services/scheduler'
export const useSchedulerStore = defineStore('scheduler', () => {
const rules = ref<ScheduleRule[]>([])
const logs = ref<ExecutionLog[]>([])
const notifications = ref<{ id: number; ruleId: number; message: string; time: string }[]>([])
const running = ref(false)
const enabledRules = computed(() => rules.value.filter(r => r.enabled))
const nextRuns = computed(() => {
return rules.value.map(r => ({
...r,
nextRun: computeNextRun(r),
}))
})
async function loadRules() {
rules.value = await db.rules.toArray()
}
async function loadLogs(limit = 50) {
logs.value = await db.logs.orderBy('executedAt').reverse().limit(limit).toArray()
}
async function seedDefaults() {
const existing = await db.rules.count()
if (existing > 0) return
const now = new Date().toISOString()
for (const def of DEFAULT_RULES) {
await db.rules.add({ ...def, createdAt: now, lastRun: null } as ScheduleRule)
}
await loadRules()
}
async function addRule(rule: Omit<ScheduleRule, 'id' | 'createdAt' | 'lastRun'>) {
await db.rules.add({
...rule,
createdAt: new Date().toISOString(),
lastRun: null,
} as ScheduleRule)
await loadRules()
}
async function updateRule(id: number, changes: Partial<ScheduleRule>) {
await db.rules.update(id, changes)
await loadRules()
}
async function toggleRule(id: number) {
const rule = rules.value.find(r => r.id === id)
if (rule) {
await db.rules.update(id, { enabled: !rule.enabled })
await loadRules()
}
}
async function deleteRule(id: number) {
await db.rules.delete(id)
await loadRules()
}
function start() {
startScheduler()
running.value = true
onScheduledTask((ruleId, message) => {
notifications.value.unshift({
id: Date.now(),
ruleId,
message,
time: new Date().toLocaleTimeString(),
})
loadLogs()
// Mantener solo últimas 20 notificaciones
if (notifications.value.length > 20) {
notifications.value = notifications.value.slice(0, 20)
}
})
}
function stop() {
stopScheduler()
running.value = false
}
function dismissNotification(id: number) {
notifications.value = notifications.value.filter(n => n.id !== id)
}
return {
rules, logs, notifications, running, enabledRules, nextRuns,
loadRules, loadLogs, seedDefaults, addRule, updateRule,
toggleRule, deleteRule, start, stop, dismissNotification,
}
})
+25
View File
@@ -0,0 +1,25 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { kappa } from '@/services/kappa-api'
import type { KappaUserStory } from '@/types/kappa'
export const useWorkItemsStore = defineStore('workitems', () => {
const creating = ref(false)
const error = ref<string | null>(null)
async function createUserStory(story: KappaUserStory): Promise<KappaUserStory | null> {
creating.value = true
error.value = null
try {
const result = await kappa.createUserStory(story)
return result
} catch (e: any) {
error.value = e.message
return null
} finally {
creating.value = false
}
}
return { creating, error, createUserStory }
})