158 lines
5.9 KiB
TypeScript
158 lines
5.9 KiB
TypeScript
/**
|
|
* 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 `
|
|
<div style="text-align:center">
|
|
<div style="font-size:28px;font-weight:700;color:#e2e8f0">${done}/${total}</div>
|
|
<div style="font-size:10px;color:#94a3b8;margin:4px 0 8px">tasks complete</div>
|
|
<div style="background:#334155;border-radius:3px;height:6px;overflow:hidden">
|
|
<div style="background:#0f766e;width:${pct}%;height:100%;border-radius:3px;transition:width 0.3s"></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
},
|
|
onInputReceived(portName, value, ctx) {
|
|
if (portName === "board-in" && value && typeof value === "object") {
|
|
const board = value as Record<string, unknown>;
|
|
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 `
|
|
<div style="text-align:center">
|
|
<div style="font-size:32px;font-weight:700;color:#e2e8f0">${count}</div>
|
|
<div style="font-size:10px;color:#94a3b8;margin:4px 0">due today</div>
|
|
<div style="font-size:11px;font-weight:500;color:${urgColor}">${urgent} urgent</div>
|
|
</div>
|
|
`;
|
|
},
|
|
onInputReceived(portName, value, ctx) {
|
|
if (portName === "board-in" && value && typeof value === "object") {
|
|
ctx.emitOutput("tasks-out", value);
|
|
}
|
|
},
|
|
};
|
|
|
|
// ── Resource Coverage ──
|
|
|
|
const SKILL_COLORS: Record<string, string> = {
|
|
facilitation: '#8b5cf6', design: '#ec4899', tech: '#3b82f6',
|
|
outreach: '#10b981', logistics: '#f59e0b',
|
|
};
|
|
|
|
interface CoverageTask {
|
|
id: string;
|
|
title: string;
|
|
needs: Record<string, number>;
|
|
fulfilled: Record<string, number>;
|
|
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 `<div style="text-align:center;color:#64748b;font-style:italic;padding:16px 0">Connect coverage-in to see task status</div>`;
|
|
}
|
|
|
|
const unresourced = summary.total - summary.ready - summary.partial;
|
|
let html = `<div style="display:flex;gap:6px;justify-content:center;margin-bottom:8px;font-size:10px;font-weight:600">
|
|
<span style="color:#22c55e">${summary.ready} ready</span>
|
|
<span style="color:#94a3b8">·</span>
|
|
<span style="color:#f59e0b">${summary.partial} partial</span>
|
|
<span style="color:#94a3b8">·</span>
|
|
<span style="color:#ef4444">${unresourced} unresourced</span>
|
|
</div>`;
|
|
|
|
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 `<span style="display:inline-block;padding:0 4px;border-radius:3px;font-size:8px;margin-right:2px;background:${filled ? color + '30' : '#1e293b'};color:${filled ? color : '#64748b'};border:1px solid ${filled ? color + '50' : '#334155'}">${sk} ${got}/${need}</span>`;
|
|
}).join('');
|
|
|
|
html += `<div style="margin-bottom:4px;padding:3px 6px;background:#0f172a;border-radius:4px;display:flex;align-items:center;gap:6px">
|
|
<span style="display:inline-block;padding:1px 5px;border-radius:8px;font-size:8px;font-weight:600;background:${pillColor}20;color:${pillColor};border:1px solid ${pillColor}40;white-space:nowrap">${pillLabel}</span>
|
|
<div style="flex:1;min-width:0">
|
|
<div style="font-size:10px;font-weight:600;color:#e2e8f0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${t.title}</div>
|
|
<div style="margin-top:1px">${skillTags}</div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
if (tasks.length > 6) {
|
|
html += `<div style="font-size:9px;color:#64748b;text-align:center">+${tasks.length - 6} more</div>`;
|
|
}
|
|
return html;
|
|
},
|
|
|
|
onInputReceived(portName, value, ctx) {
|
|
if (portName === "coverage-in" && value && typeof value === "object") {
|
|
const coverage = value as { tasks?: CoverageTask[]; summary?: Record<string, number> };
|
|
// 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];
|