1ca019fa48
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
170 lines
4.5 KiB
Svelte
170 lines
4.5 KiB
Svelte
<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>
|