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,85 @@
|
||||
<script>
|
||||
import TaskCard from "./TaskCard.svelte";
|
||||
|
||||
let {
|
||||
title,
|
||||
status,
|
||||
tasks = [],
|
||||
agents = [],
|
||||
onEdit,
|
||||
onDelete,
|
||||
onAssign,
|
||||
onDrop
|
||||
} = $props();
|
||||
|
||||
const columnColors = {
|
||||
parking_lot: "bg-slate-100 dark:bg-slate-800 border-slate-300",
|
||||
in_progress: "bg-blue-50 dark:bg-blue-900/20 border-blue-300",
|
||||
done: "bg-emerald-50 dark:bg-emerald-900/20 border-emerald-300"
|
||||
};
|
||||
|
||||
function handleDragOver(e) {
|
||||
e.preventDefault();
|
||||
e.currentTarget.classList.add("ring-2", "ring-primary-400");
|
||||
}
|
||||
|
||||
function handleDragLeave(e) {
|
||||
e.currentTarget.classList.remove("ring-2", "ring-primary-400");
|
||||
}
|
||||
|
||||
function handleDrop(e) {
|
||||
e.preventDefault();
|
||||
e.currentTarget.classList.remove("ring-2", "ring-primary-400");
|
||||
|
||||
const taskId = parseInt(e.dataTransfer.getData("taskId"));
|
||||
const fromStatus = e.dataTransfer.getData("fromStatus");
|
||||
|
||||
if (fromStatus !== status) {
|
||||
onDrop?.(taskId, status);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragStart(e, task) {
|
||||
e.dataTransfer.setData("taskId", task.id);
|
||||
e.dataTransfer.setData("fromStatus", task.status);
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Column header -->
|
||||
<div class="flex items-center justify-between mb-3 pb-2 border-b border-surface-300 dark:border-surface-600">
|
||||
<h3 class="font-semibold text-surface-900 dark:text-surface-100">
|
||||
{title}
|
||||
</h3>
|
||||
<span class="text-sm text-surface-600 dark:text-surface-400 bg-surface-200 dark:bg-surface-700 px-2 py-0.5 rounded-full">
|
||||
{tasks.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Drop zone -->
|
||||
<div
|
||||
class="flex-1 min-h-[200px] p-2 rounded-lg border-2 border-dashed transition-all {columnColors[status] || columnColors.parking_lot}"
|
||||
ondragover={handleDragOver}
|
||||
ondragleave={handleDragLeave}
|
||||
ondrop={handleDrop}
|
||||
>
|
||||
{#if tasks.length === 0}
|
||||
<div class="flex items-center justify-center h-32 text-surface-400 dark:text-surface-500 text-sm">
|
||||
Drop tasks here
|
||||
</div>
|
||||
{:else}
|
||||
{#each tasks as task (task.id)}
|
||||
<div draggable="true" ondragstart={(e) => handleDragStart(e, task)}>
|
||||
<TaskCard
|
||||
{task}
|
||||
{agents}
|
||||
{onEdit}
|
||||
{onDelete}
|
||||
{onAssign}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user