Phase 5: Tasks API + Kanban board + Card UI
This commit is contained in:
parent
3805caaa30
commit
a73f1b143b
6 changed files with 113 additions and 29 deletions
32
backend/api/tasks.py
Normal file
32
backend/api/tasks.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
from fastapi import APIRouter
|
||||
from typing import List
|
||||
from models import Task
|
||||
|
||||
router = APIRouter(prefix="/api/v1", tags=["tasks"])
|
||||
|
||||
tasks_db: List[Task] = [
|
||||
Task(id="t1", title="Реализовать EMA-кроссовер", description="Бэктест стратегии на Brent", status="in_progress", priority="high", assignee_agent_id="worker-code", project_id="p1"),
|
||||
Task(id="t2", title="Подготовить отчёт", description="Результаты бэктеста за Q3", status="todo", priority="medium", project_id="p1"),
|
||||
Task(id="t3", title="Настроить CI/CD", description="GitHub Actions для автотестов", status="review", priority="high", assignee_agent_id="worker-fast", project_id="p1"),
|
||||
Task(id="t4", title="Обновить вики", description="Документация по API", status="done", priority="low", project_id="p1"),
|
||||
Task(id="t5", title="Добавить OAuth", description="GitHub + Google интеграция", status="todo", priority="medium", project_id="p1"),
|
||||
]
|
||||
|
||||
@router.get("/tasks", response_model=List[Task])
|
||||
async def get_tasks():
|
||||
return tasks_db
|
||||
|
||||
@router.post("/tasks", response_model=Task)
|
||||
async def create_task(task: Task):
|
||||
tasks_db.append(task)
|
||||
return task
|
||||
|
||||
@router.patch("/tasks/{task_id}")
|
||||
async def update_task(task_id: str, update: dict):
|
||||
for task in tasks_db:
|
||||
if task.id == task_id:
|
||||
for key, val in update.items():
|
||||
if hasattr(task, key):
|
||||
setattr(task, key, val)
|
||||
return task
|
||||
return {"error": "Task not found"}
|
||||
|
|
@ -8,7 +8,7 @@ from pathlib import Path
|
|||
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from api import runs, agents, memory, skills, connectors
|
||||
from api import runs, agents, memory, skills, connectors, tasks
|
||||
import asyncio, json
|
||||
|
||||
app = FastAPI(
|
||||
|
|
@ -22,6 +22,7 @@ app.include_router(agents.router)
|
|||
app.include_router(memory.router)
|
||||
app.include_router(skills.router)
|
||||
app.include_router(connectors.router)
|
||||
app.include_router(tasks.router)
|
||||
|
||||
# WebSocket для real-time обновлений
|
||||
@app.websocket("/ws")
|
||||
|
|
|
|||
|
|
@ -11,3 +11,12 @@ class Agent(BaseModel):
|
|||
|
||||
class AgentUpdate(BaseModel):
|
||||
status: Literal["online", "offline", "busy"]
|
||||
|
||||
class Task(BaseModel):
|
||||
id: str
|
||||
title: str
|
||||
description: str = ""
|
||||
status: Literal["todo", "in_progress", "review", "done"] = "todo"
|
||||
priority: Literal["low", "medium", "high"] = "medium"
|
||||
assignee_agent_id: Optional[str] = None
|
||||
project_id: str = "default"
|
||||
|
|
|
|||
|
|
@ -1,37 +1,36 @@
|
|||
import { CheckCircle2, Circle, Plus } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { TaskCard } from './TaskCard'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
interface Task { id: string; title: string; done: boolean; project: string }
|
||||
const columns = ['todo', 'in_progress', 'review', 'done']
|
||||
|
||||
export function Kanban() {
|
||||
const [tasks] = useState<Task[]>([
|
||||
{ id: '1', title: 'Реализовать EMA-кроссовер', done: true, project: 'Trading' },
|
||||
{ id: '2', title: 'Настроить CI/CD пайплайн', done: false, project: 'ACC' },
|
||||
{ id: '3', title: 'Добавить health-check эндпоинт', done: true, project: 'ACC' },
|
||||
{ id: '4', title: 'Обновить документацию API', done: false, project: 'ACC' },
|
||||
])
|
||||
const { data: tasks = [] } = useQuery({
|
||||
queryKey: ['tasks'],
|
||||
queryFn: async () => {
|
||||
const res = await fetch('/api/v1/tasks')
|
||||
return res.json()
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">Tasks</h3>
|
||||
<button className="p-1.5 rounded-lg hover:bg-secondary text-muted-foreground hover:text-foreground transition-colors">
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{tasks.map(task => (
|
||||
<div key={task.id} className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-secondary/50 transition-colors cursor-pointer group">
|
||||
{task.done
|
||||
? <CheckCircle2 className="w-4 h-4 text-emerald-500 shrink-0" />
|
||||
: <Circle className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
}
|
||||
<span className={`text-sm flex-1 ${task.done ? 'line-through text-muted-foreground' : ''}`}>
|
||||
{task.title}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{task.project}
|
||||
</span>
|
||||
<h2 className="text-2xl font-semibold mb-6">Project Kanban</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
{columns.map((status) => (
|
||||
<div key={status} className="bg-card border border-border rounded-3xl p-4">
|
||||
<h3 className="uppercase text-xs tracking-widest text-muted-foreground mb-4 px-1">
|
||||
{status.replace('_', ' ')}
|
||||
<span className="ml-2 text-zinc-600">
|
||||
{tasks.filter((t: any) => t.status === status).length}
|
||||
</span>
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{tasks
|
||||
.filter((t: any) => t.status === status)
|
||||
.map((task: any) => (
|
||||
<TaskCard key={task.id} task={task} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
29
frontend/src/components/tasks/TaskCard.tsx
Normal file
29
frontend/src/components/tasks/TaskCard.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { Card } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
todo: 'bg-zinc-700',
|
||||
in_progress: 'bg-blue-600',
|
||||
review: 'bg-amber-600',
|
||||
done: 'bg-emerald-600',
|
||||
}
|
||||
|
||||
export function TaskCard({ task }: { task: any }) {
|
||||
return (
|
||||
<Card className="p-4 hover:shadow-lg transition-shadow cursor-pointer border-border hover:border-primary/30">
|
||||
<div className="flex justify-between items-start">
|
||||
<h4 className="font-medium text-sm">{task.title}</h4>
|
||||
<Badge variant="outline">{task.status.replace('_', ' ')}</Badge>
|
||||
</div>
|
||||
{task.description && (
|
||||
<p className="text-xs text-muted-foreground mt-2 line-clamp-2">{task.description}</p>
|
||||
)}
|
||||
{task.assignee_agent_id && (
|
||||
<div className="mt-3 text-xs text-muted-foreground flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500" />
|
||||
{task.assignee_agent_id}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
14
frontend/src/components/ui/card.tsx
Normal file
14
frontend/src/components/ui/card.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface CardProps {
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function Card({ className, children }: CardProps) {
|
||||
return (
|
||||
<div className={cn('bg-card border border-border rounded-2xl', className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue