/** * rTasks applet definitions — Task Counter + Due Today. */ import type { AppletDefinition, AppletLiveData } from "../../shared/applet-types"; const taskCounter: AppletDefinition = { id: "task-counter", label: "Task Counter", icon: "📋", accentColor: "#0f766e", ports: [ { name: "board-in", type: "json", direction: "input" }, { name: "count-out", type: "number", direction: "output" }, ], renderCompact(data: AppletLiveData): string { const { snapshot } = data; const total = (snapshot.total as number) || 0; const done = (snapshot.done as number) || 0; const pct = total > 0 ? Math.round((done / total) * 100) : 0; return `
${done}/${total}
tasks complete
`; }, onInputReceived(portName, value, ctx) { if (portName === "board-in" && value && typeof value === "object") { const board = value as Record; ctx.emitOutput("count-out", Number(board.total) || 0); } }, }; const dueToday: AppletDefinition = { id: "due-today", label: "Due Today", icon: "⏰", accentColor: "#0f766e", ports: [ { name: "board-in", type: "json", direction: "input" }, { name: "tasks-out", type: "json", direction: "output" }, ], renderCompact(data: AppletLiveData): string { const { snapshot } = data; const count = (snapshot.dueCount as number) || 0; const urgent = (snapshot.urgentCount as number) || 0; const urgColor = urgent > 0 ? "#ef4444" : "#22c55e"; return `
${count}
due today
${urgent} urgent
`; }, onInputReceived(portName, value, ctx) { if (portName === "board-in" && value && typeof value === "object") { ctx.emitOutput("tasks-out", value); } }, }; // ── Resource Coverage ── const SKILL_COLORS: Record = { facilitation: '#8b5cf6', design: '#ec4899', tech: '#3b82f6', outreach: '#10b981', logistics: '#f59e0b', }; interface CoverageTask { id: string; title: string; needs: Record; fulfilled: Record; ready: boolean; partial: boolean; } const resourceCoverage: AppletDefinition = { id: "resource-coverage", label: "Resource Coverage", icon: "🎯", accentColor: "#0f766e", ports: [ { name: "coverage-in", type: "json", direction: "input" }, { name: "gaps-out", type: "json", direction: "output" }, ], renderCompact(data: AppletLiveData): string { const { snapshot } = data; const tasks = (snapshot.tasks as CoverageTask[]) || []; const summary = (snapshot.summary as { total: number; ready: number; partial: number }) || { total: 0, ready: 0, partial: 0 }; if (tasks.length === 0) { return `
Connect coverage-in to see task status
`; } const unresourced = summary.total - summary.ready - summary.partial; let html = `
${summary.ready} ready · ${summary.partial} partial · ${unresourced} unresourced
`; for (const t of tasks.slice(0, 6)) { const pillColor = t.ready ? '#22c55e' : t.partial ? '#f59e0b' : '#ef4444'; const pillLabel = t.ready ? 'ready' : t.partial ? 'partial' : 'needs resources'; const skillTags = Object.keys(t.needs).map(sk => { const got = t.fulfilled?.[sk] || 0; const need = t.needs[sk]; const filled = got >= need; const color = SKILL_COLORS[sk] || '#6b7280'; return `${sk} ${got}/${need}`; }).join(''); html += `
${pillLabel}
${t.title}
${skillTags}
`; } if (tasks.length > 6) { html += `
+${tasks.length - 6} more
`; } return html; }, onInputReceived(portName, value, ctx) { if (portName === "coverage-in" && value && typeof value === "object") { const coverage = value as { tasks?: CoverageTask[]; summary?: Record }; // Compute gaps — skills with unfilled needs const gaps: Array<{ taskId: string; taskTitle: string; skill: string; needed: number; have: number }> = []; for (const t of coverage.tasks || []) { for (const [sk, need] of Object.entries(t.needs)) { const have = t.fulfilled?.[sk] || 0; if (have < need) { gaps.push({ taskId: t.id, taskTitle: t.title, skill: sk, needed: need, have }); } } } ctx.emitOutput("gaps-out", gaps); } }, }; export const tasksApplets: AppletDefinition[] = [taskCounter, dueToday, resourceCoverage];