/**
* 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 += ``;
}
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];