feat: add OPC (One Person Company) management system with Kanban board
Deploy Dashboard (Dev) / deploy-dev (push) Has been cancelled
Run Tests / Backend Tests (push) Has been cancelled
Run Tests / Frontend Tests (push) Has been cancelled
Run Tests / Test Summary (push) Has been cancelled

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:
Gan, Jimmy
2026-03-31 14:31:31 +08:00
parent 51c4cd1a4e
commit 1ca019fa48
16 changed files with 1771 additions and 3 deletions
+149
View File
@@ -0,0 +1,149 @@
<script>
import { onMount } from "svelte";
import KanbanBoard from "../components/opc/KanbanBoard.svelte";
import TaskModal from "../components/opc/TaskModal.svelte";
import { getTasks, getAgents } from "../lib/opc-api.js";
let tasks = $state([]);
let agents = $state([]);
let loading = $state(true);
let showTaskModal = $state(false);
let editingTask = $state(null);
async function loadData() {
loading = true;
try {
const [tasksRes, agentsRes] = await Promise.all([
getTasks(),
getAgents()
]);
tasks = tasksRes.items || [];
agents = agentsRes.items || [];
} catch (e) {
console.error("Failed to load data:", e);
} finally {
loading = false;
}
}
function handleCreateTask() {
editingTask = null;
showTaskModal = true;
}
function handleEditTask(task) {
editingTask = task;
showTaskModal = true;
}
function handleDeleteTask(taskId) {
tasks = tasks.filter(t => t.id !== taskId);
}
function handleAssignTask(taskId, assignedTo, assignedType) {
const task = tasks.find(t => t.id === taskId);
if (task) {
task.assigned_to = assignedTo;
task.assigned_type = assignedType;
tasks = [...tasks];
}
}
function handleTaskMoved(taskId, newStatus) {
const task = tasks.find(t => t.id === taskId);
if (task) {
task.status = newStatus;
tasks = [...tasks];
}
}
function handleModalClose() {
showTaskModal = false;
editingTask = null;
}
function handleModalSave() {
loadData();
}
onMount(() => {
loadData();
});
</script>
<div class="space-y-6 h-full flex flex-col">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-surface-900 dark:text-surface-100">
OPC Management
</h1>
<p class="text-surface-600 dark:text-surface-400 mt-1">
Manage your tasks with AI agents
</p>
</div>
<button
onclick={handleCreateTask}
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 flex items-center gap-2"
>
<span></span>
<span>New Task</span>
</button>
</div>
<!-- Stats -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
<div class="text-sm text-surface-600 dark:text-surface-400">Total Tasks</div>
<div class="text-2xl font-bold text-surface-900 dark:text-surface-100 mt-1">
{tasks.length}
</div>
</div>
<div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
<div class="text-sm text-surface-600 dark:text-surface-400">Parking Lot</div>
<div class="text-2xl font-bold text-slate-600 dark:text-slate-400 mt-1">
{tasks.filter(t => t.status === "parking_lot").length}
</div>
</div>
<div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
<div class="text-sm text-surface-600 dark:text-surface-400">In Progress</div>
<div class="text-2xl font-bold text-blue-600 dark:text-blue-400 mt-1">
{tasks.filter(t => t.status === "in_progress").length}
</div>
</div>
<div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
<div class="text-sm text-surface-600 dark:text-surface-400">Done</div>
<div class="text-2xl font-bold text-emerald-600 dark:text-emerald-400 mt-1">
{tasks.filter(t => t.status === "done").length}
</div>
</div>
</div>
<!-- Kanban Board -->
{#if loading}
<div class="flex-1 flex items-center justify-center">
<div class="text-surface-600 dark:text-surface-400">Loading...</div>
</div>
{:else}
<div class="flex-1 overflow-hidden">
<KanbanBoard
{tasks}
{agents}
onEdit={handleEditTask}
onDelete={handleDeleteTask}
onAssign={handleAssignTask}
onTaskMoved={handleTaskMoved}
/>
</div>
{/if}
</div>
<!-- Task Modal -->
{#if showTaskModal}
<TaskModal
task={editingTask}
{agents}
onClose={handleModalClose}
onSave={handleModalSave}
/>
{/if}