feat: add OPC (One Person Company) management system with Kanban board
Phase 1 MVP implementation: - PostgreSQL database schema for tasks, agents, executions, time tracking - Backend API: task CRUD, agent management, automatic time tracking - Frontend: Kanban board with drag-and-drop (Parking Lot → In Progress → Done) - Task modal for creating/editing tasks with tags, priority, assignee - 3 core agents seeded: PM, CTO, COO - Agent approval workflow: CxO level requires approval, others auto-execute - Automatic time tracking: starts on in_progress, stops on done - RBAC integration: OPC page accessible to admin/member roles Features: - Drag-and-drop tasks between columns - Assign tasks to humans or AI agents - Priority badges (low, medium, high, urgent) - Tags and due dates - Task history tracking - Time entry tracking with automatic timer - Agent execution records with approval workflow Next steps: - Initialize OPC database on NAS - Implement agent executor service - Add WebSocket for real-time updates - Add email notifications - Add PDF invoice generation
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
<script>
|
||||
import { deleteTask, assignTask, getTaskTime } from "../../lib/opc-api.js";
|
||||
|
||||
let { task, agents = [], onEdit, onDelete, onAssign } = $props();
|
||||
|
||||
let showActions = $state(false);
|
||||
let timeInfo = $state(null);
|
||||
|
||||
const priorityColors = {
|
||||
low: "bg-slate-200 text-slate-700",
|
||||
medium: "bg-blue-100 text-blue-700",
|
||||
high: "bg-amber-100 text-amber-700",
|
||||
urgent: "bg-rose-100 text-rose-700"
|
||||
};
|
||||
|
||||
async function loadTimeInfo() {
|
||||
if (task.status === "in_progress") {
|
||||
try {
|
||||
timeInfo = await getTaskTime(task.id);
|
||||
} catch (e) {
|
||||
console.error("Failed to load time info:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
loadTimeInfo();
|
||||
});
|
||||
|
||||
async function handleDelete() {
|
||||
if (confirm(`Delete task "${task.title}"?`)) {
|
||||
try {
|
||||
await deleteTask(task.id);
|
||||
onDelete?.(task.id);
|
||||
} catch (e) {
|
||||
alert("Failed to delete task: " + e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAssignAgent(agentId) {
|
||||
try {
|
||||
await assignTask(task.id, agentId, "agent");
|
||||
onAssign?.(task.id, agentId, "agent");
|
||||
} catch (e) {
|
||||
alert("Failed to assign agent: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function getAgentIcon(agentId) {
|
||||
const icons = {
|
||||
pm: "📋",
|
||||
cto: "💻",
|
||||
coo: "⚙️",
|
||||
ceo: "🎯",
|
||||
marketing: "📢",
|
||||
social_media: "📱"
|
||||
};
|
||||
return icons[agentId] || "🤖";
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="bg-white dark:bg-surface-800 rounded-lg p-3 mb-2 shadow-sm border border-surface-200 dark:border-surface-700 cursor-move hover:shadow-md transition-shadow"
|
||||
onmouseenter={() => showActions = true}
|
||||
onmouseleave={() => showActions = false}
|
||||
>
|
||||
<!-- Priority badge -->
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<span class="text-xs px-2 py-0.5 rounded {priorityColors[task.priority] || priorityColors.medium}">
|
||||
{task.priority}
|
||||
</span>
|
||||
{#if showActions}
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
onclick={() => onEdit?.(task)}
|
||||
class="text-xs text-surface-600 hover:text-primary-600 dark:text-surface-400"
|
||||
title="Edit"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
onclick={handleDelete}
|
||||
class="text-xs text-surface-600 hover:text-rose-600 dark:text-surface-400"
|
||||
title="Delete"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<h4 class="font-medium text-surface-900 dark:text-surface-100 mb-1 text-sm">
|
||||
{task.title}
|
||||
</h4>
|
||||
|
||||
<!-- Description preview -->
|
||||
{#if task.description}
|
||||
<p class="text-xs text-surface-600 dark:text-surface-400 mb-2 line-clamp-2">
|
||||
{task.description}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<!-- Tags -->
|
||||
{#if task.tags && task.tags.length > 0}
|
||||
<div class="flex flex-wrap gap-1 mb-2">
|
||||
{#each task.tags as tag}
|
||||
<span class="text-xs px-1.5 py-0.5 rounded bg-surface-100 dark:bg-surface-700 text-surface-700 dark:text-surface-300">
|
||||
{tag}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Assignee and time -->
|
||||
<div class="flex items-center justify-between text-xs text-surface-600 dark:text-surface-400">
|
||||
<div class="flex items-center gap-1">
|
||||
{#if task.assigned_to}
|
||||
{#if task.assigned_type === "agent"}
|
||||
<span title={task.assigned_to}>
|
||||
{getAgentIcon(task.assigned_to)}
|
||||
</span>
|
||||
{:else}
|
||||
<span>👤 {task.assigned_to}</span>
|
||||
{/if}
|
||||
{:else}
|
||||
<button
|
||||
onclick={() => showActions = true}
|
||||
class="text-surface-500 hover:text-primary-600"
|
||||
title="Assign"
|
||||
>
|
||||
Unassigned
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if timeInfo && timeInfo.total_hours > 0}
|
||||
<span class="text-xs text-surface-500">
|
||||
⏱️ {timeInfo.total_hours}h
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Due date -->
|
||||
{#if task.due_date}
|
||||
<div class="text-xs text-surface-500 mt-1">
|
||||
📅 {new Date(task.due_date).toLocaleDateString()}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Quick assign to agent (when hovering) -->
|
||||
{#if showActions && !task.assigned_to}
|
||||
<div class="mt-2 pt-2 border-t border-surface-200 dark:border-surface-700">
|
||||
<div class="text-xs text-surface-600 mb-1">Assign to agent:</div>
|
||||
<div class="flex gap-1">
|
||||
{#each agents as agent}
|
||||
<button
|
||||
onclick={() => handleAssignAgent(agent.id)}
|
||||
class="text-lg hover:scale-110 transition-transform"
|
||||
title={agent.name}
|
||||
>
|
||||
{getAgentIcon(agent.id)}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user