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,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}
|
||||
Reference in New Issue
Block a user