66fd4e175a
- 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
202 lines
5.1 KiB
TypeScript
202 lines
5.1 KiB
TypeScript
/**
|
|
* Scheduler tipo cron para KAPPA Hub.
|
|
*
|
|
* Las tareas se ejecutan solo cuando la app está abierta (web app).
|
|
* En RUMBO (Tauri) esto se vuelve un proceso en segundo plano real.
|
|
*/
|
|
|
|
import Dexie, { type EntityTable } from 'dexie'
|
|
|
|
interface ScheduleRule {
|
|
id?: number
|
|
name: string
|
|
description?: string
|
|
/** 0=dom, 1=lun, ..., 6=sáb */
|
|
daysOfWeek: number[]
|
|
/** 0-23 */
|
|
hour: number
|
|
/** 0-59 */
|
|
minute: number
|
|
action: ScheduleAction
|
|
enabled: boolean
|
|
lastRun: string | null
|
|
createdAt: string
|
|
}
|
|
|
|
type ScheduleAction =
|
|
| { type: 'generate_progress_report' }
|
|
| { type: 'check_hus' }
|
|
| { type: 'daily_prep' }
|
|
| { type: 'reminder'; message: string }
|
|
|
|
interface ExecutionLog {
|
|
id?: number
|
|
ruleId: number
|
|
ruleName: string
|
|
executedAt: string
|
|
success: boolean
|
|
message: string
|
|
}
|
|
|
|
const db = new Dexie('kappa-hub-scheduler') as Dexie & {
|
|
rules: EntityTable<ScheduleRule, 'id'>
|
|
logs: EntityTable<ExecutionLog, 'id'>
|
|
}
|
|
|
|
db.version(1).stores({
|
|
rules: '++id, enabled, hour, minute',
|
|
logs: '++id, ruleId, executedAt',
|
|
})
|
|
|
|
export { db }
|
|
export type { ScheduleRule, ScheduleAction, ExecutionLog }
|
|
|
|
// ─── Engine ─────────────────────────────────────────────────
|
|
|
|
type Subscriber = (ruleId: number, message: string) => void
|
|
|
|
let intervalId: ReturnType<typeof setInterval> | null = null
|
|
let subscribers: Subscriber[] = []
|
|
|
|
function now(): string {
|
|
return new Date().toISOString()
|
|
}
|
|
|
|
function matchesRule(rule: ScheduleRule, date: Date): boolean {
|
|
if (!rule.enabled) return false
|
|
if (!rule.daysOfWeek.includes(date.getDay())) return false
|
|
if (rule.hour !== date.getHours()) return false
|
|
if (rule.minute !== date.getMinutes()) return false
|
|
return true
|
|
}
|
|
|
|
async function executeRule(rule: ScheduleRule): Promise<string> {
|
|
const projectNames: string[] = [] // se llenará del store
|
|
|
|
switch (rule.action.type) {
|
|
case 'generate_progress_report':
|
|
return `📊 Informe de avance generado para ${projectNames.length || 'todos los'} proyectos.`
|
|
|
|
case 'check_hus': {
|
|
// En la iteración real consulta KAPPA API
|
|
return `🎫 HUs revisadas. ${projectNames.length || 'N'} proyectos actualizados.`
|
|
}
|
|
|
|
case 'daily_prep':
|
|
return `📋 Daily prep: recordatorio de revisar HUs pendientes y bloqueos para hoy.`
|
|
|
|
case 'reminder':
|
|
return `⏰ ${rule.action.message}`
|
|
|
|
default:
|
|
return `✅ Tarea "${rule.name}" ejecutada.`
|
|
}
|
|
}
|
|
|
|
async function runRule(rule: ScheduleRule) {
|
|
let success = true
|
|
let message = ''
|
|
try {
|
|
message = await executeRule(rule)
|
|
} catch (e: any) {
|
|
success = false
|
|
message = e.message
|
|
}
|
|
|
|
await db.rules.update(rule.id!, { lastRun: now() })
|
|
await db.logs.add({
|
|
ruleId: rule.id!,
|
|
ruleName: rule.name,
|
|
executedAt: now(),
|
|
success,
|
|
message,
|
|
})
|
|
|
|
for (const sub of subscribers) {
|
|
sub(rule.id!, message)
|
|
}
|
|
}
|
|
|
|
async function tick() {
|
|
const date = new Date()
|
|
const rules = await db.rules.where('enabled').equals(1).toArray()
|
|
for (const rule of rules) {
|
|
if (matchesRule(rule, date)) {
|
|
await runRule(rule)
|
|
}
|
|
}
|
|
}
|
|
|
|
export function startScheduler() {
|
|
if (intervalId) return
|
|
tick() // ejecutar inmediatamente por si acaba de pasar el minuto
|
|
intervalId = setInterval(tick, 60_000) // cada minuto
|
|
}
|
|
|
|
export function stopScheduler() {
|
|
if (intervalId) {
|
|
clearInterval(intervalId)
|
|
intervalId = null
|
|
}
|
|
}
|
|
|
|
export function onScheduledTask(cb: Subscriber) {
|
|
subscribers.push(cb)
|
|
return () => {
|
|
subscribers = subscribers.filter(s => s !== cb)
|
|
}
|
|
}
|
|
|
|
// ─── Helpers ────────────────────────────────────────────────
|
|
|
|
export function computeNextRun(rule: ScheduleRule): Date | null {
|
|
if (!rule.enabled || rule.daysOfWeek.length === 0) return null
|
|
|
|
const now = new Date()
|
|
const candidate = new Date(now)
|
|
candidate.setHours(rule.hour, rule.minute, 0, 0)
|
|
|
|
// Buscar el próximo día que coincida
|
|
for (let i = 0; i < 8; i++) {
|
|
const check = new Date(candidate)
|
|
check.setDate(check.getDate() + i)
|
|
|
|
if (rule.daysOfWeek.includes(check.getDay())) {
|
|
if (check > now) return check
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
export const DAY_LABELS = ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb']
|
|
|
|
export const DEFAULT_RULES: Omit<ScheduleRule, 'id' | 'createdAt' | 'lastRun'>[] = [
|
|
{
|
|
name: 'Informe semanal de avance',
|
|
description: 'Genera un resumen de estado de cada proyecto activo',
|
|
daysOfWeek: [5], // viernes
|
|
hour: 9,
|
|
minute: 0,
|
|
action: { type: 'generate_progress_report' },
|
|
enabled: true,
|
|
},
|
|
{
|
|
name: 'Revisión de HUs pendientes',
|
|
description: 'Revisa HUs en progreso, bloqueadas y vencidas en todos los proyectos',
|
|
daysOfWeek: [1, 2, 3, 4, 5], // lun-vie
|
|
hour: 8,
|
|
minute: 0,
|
|
action: { type: 'check_hus' },
|
|
enabled: true,
|
|
},
|
|
{
|
|
name: 'Daily prep',
|
|
description: 'Prepara el resumen diario: qué HUs toca hoy, bloqueos, entregables próximos',
|
|
daysOfWeek: [1, 2, 3, 4, 5],
|
|
hour: 7,
|
|
minute: 30,
|
|
action: { type: 'daily_prep' },
|
|
enabled: true,
|
|
},
|
|
]
|