Files
nas-tools/dashboard/frontend/src/components/opc/TaskModal.svelte
T
Gan, Jimmy 1ca019fa48
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
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
2026-03-31 14:31:31 +08:00

251 lines
8.6 KiB
Svelte

<script>
import { createTask, updateTask } from "../../lib/opc-api.js";
let { task = null, agents = [], onClose, onSave } = $props();
let isEdit = $state(!!task);
let formData = $state({
title: task?.title || "",
description: task?.description || "",
status: task?.status || "parking_lot",
priority: task?.priority || "medium",
assigned_to: task?.assigned_to || "",
assigned_type: task?.assigned_type || "human",
tags: task?.tags || [],
due_date: task?.due_date ? task.due_date.split("T")[0] : ""
});
let tagInput = $state("");
let saving = $state(false);
function addTag() {
if (tagInput.trim() && !formData.tags.includes(tagInput.trim())) {
formData.tags = [...formData.tags, tagInput.trim()];
tagInput = "";
}
}
function removeTag(tag) {
formData.tags = formData.tags.filter(t => t !== tag);
}
async function handleSubmit(e) {
e.preventDefault();
saving = true;
try {
const payload = {
...formData,
due_date: formData.due_date ? new Date(formData.due_date).toISOString() : null,
assigned_to: formData.assigned_to || null
};
if (isEdit) {
await updateTask(task.id, payload);
} else {
await createTask(payload);
}
onSave?.();
onClose?.();
} catch (e) {
alert("Failed to save task: " + e.message);
} finally {
saving = false;
}
}
</script>
<!-- Modal backdrop -->
<div
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
onclick={(e) => e.target === e.currentTarget && onClose?.()}
>
<!-- Modal content -->
<div class="bg-white dark:bg-surface-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<!-- Header -->
<div class="flex items-center justify-between p-4 border-b border-surface-200 dark:border-surface-700">
<h2 class="text-xl font-semibold text-surface-900 dark:text-surface-100">
{isEdit ? "Edit Task" : "Create Task"}
</h2>
<button
onclick={onClose}
class="text-surface-500 hover:text-surface-700 dark:hover:text-surface-300"
>
</button>
</div>
<!-- Form -->
<form onsubmit={handleSubmit} class="p-4 space-y-4">
<!-- Title -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
Title *
</label>
<input
type="text"
bind:value={formData.title}
required
class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
placeholder="Task title"
/>
</div>
<!-- Description -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
Description
</label>
<textarea
bind:value={formData.description}
rows="4"
class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
placeholder="Task description"
></textarea>
</div>
<!-- Status and Priority -->
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
Status
</label>
<select
bind:value={formData.status}
class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
>
<option value="parking_lot">Parking Lot</option>
<option value="in_progress">In Progress</option>
<option value="done">Done</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
Priority
</label>
<select
bind:value={formData.priority}
class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
</div>
</div>
<!-- Assignee -->
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
Assign to
</label>
<select
bind:value={formData.assigned_type}
class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
>
<option value="human">Human</option>
<option value="agent">Agent</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
{formData.assigned_type === "agent" ? "Agent" : "Username"}
</label>
{#if formData.assigned_type === "agent"}
<select
bind:value={formData.assigned_to}
class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
>
<option value="">Unassigned</option>
{#each agents as agent}
<option value={agent.id}>{agent.name}</option>
{/each}
</select>
{:else}
<input
type="text"
bind:value={formData.assigned_to}
class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
placeholder="Username"
/>
{/if}
</div>
</div>
<!-- Due date -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
Due Date
</label>
<input
type="date"
bind:value={formData.due_date}
class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
/>
</div>
<!-- Tags -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
Tags
</label>
<div class="flex gap-2 mb-2">
<input
type="text"
bind:value={tagInput}
onkeydown={(e) => e.key === "Enter" && (e.preventDefault(), addTag())}
class="flex-1 px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
placeholder="Add tag and press Enter"
/>
<button
type="button"
onclick={addTag}
class="px-4 py-2 bg-surface-200 dark:bg-surface-700 text-surface-700 dark:text-surface-300 rounded-lg hover:bg-surface-300 dark:hover:bg-surface-600"
>
Add
</button>
</div>
{#if formData.tags.length > 0}
<div class="flex flex-wrap gap-2">
{#each formData.tags as tag}
<span class="inline-flex items-center gap-1 px-2 py-1 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 rounded text-sm">
{tag}
<button
type="button"
onclick={() => removeTag(tag)}
class="hover:text-primary-900 dark:hover:text-primary-100"
>
</button>
</span>
{/each}
</div>
{/if}
</div>
<!-- Actions -->
<div class="flex justify-end gap-2 pt-4 border-t border-surface-200 dark:border-surface-700">
<button
type="button"
onclick={onClose}
class="px-4 py-2 text-surface-700 dark:text-surface-300 hover:bg-surface-100 dark:hover:bg-surface-700 rounded-lg"
>
Cancel
</button>
<button
type="submit"
disabled={saving}
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
>
{saving ? "Saving..." : isEdit ? "Update" : "Create"}
</button>
</div>
</form>
</div>
</div>