142 lines
5.9 KiB
TypeScript
142 lines
5.9 KiB
TypeScript
import Layout from "@/components/Layout";
|
|
import { useAuth } from "@/contexts/AuthContext";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { useNavigate, useParams } from "react-router-dom";
|
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { aethexCollabService } from "@/lib/aethex-collab-service";
|
|
import LoadingScreen from "@/components/LoadingScreen";
|
|
|
|
const columns: { key: "todo"|"doing"|"done"|"blocked"; title: string; hint: string }[] = [
|
|
{ key: "todo", title: "To do", hint: "Planned" },
|
|
{ key: "doing", title: "In progress", hint: "Active" },
|
|
{ key: "done", title: "Done", hint: "Completed" },
|
|
{ key: "blocked", title: "Blocked", hint: "Needs attention" },
|
|
];
|
|
|
|
export default function ProjectBoard() {
|
|
const { user, loading } = useAuth();
|
|
const navigate = useNavigate();
|
|
const { projectId } = useParams<{ projectId: string }>();
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [tasks, setTasks] = useState<any[]>([]);
|
|
const [title, setTitle] = useState("");
|
|
const [description, setDescription] = useState("");
|
|
const [creating, setCreating] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!loading && !user) navigate("/login", { replace: true });
|
|
}, [loading, user, navigate]);
|
|
|
|
const load = async () => {
|
|
if (!projectId) return;
|
|
setIsLoading(true);
|
|
try {
|
|
const rows = await aethexCollabService.listProjectTasks(projectId);
|
|
setTasks(rows);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
load();
|
|
}, [projectId]);
|
|
|
|
const grouped = useMemo(() => {
|
|
const map: Record<string, any[]> = { todo: [], doing: [], done: [], blocked: [] };
|
|
for (const t of tasks) {
|
|
map[t.status || "todo"].push(t);
|
|
}
|
|
return map;
|
|
}, [tasks]);
|
|
|
|
const handleCreate = async () => {
|
|
if (!user?.id || !projectId) return;
|
|
if (!title.trim()) return;
|
|
setCreating(true);
|
|
try {
|
|
await aethexCollabService.createTask(projectId, title.trim(), description.trim() || null, null, null);
|
|
setTitle("");
|
|
setDescription("");
|
|
await load();
|
|
} finally {
|
|
setCreating(false);
|
|
}
|
|
};
|
|
|
|
const move = async (taskId: string, status: "todo"|"doing"|"done"|"blocked") => {
|
|
await aethexCollabService.updateTaskStatus(taskId, status);
|
|
await load();
|
|
};
|
|
|
|
if (loading || isLoading) return <LoadingScreen message="Loading project..." showProgress duration={800} />;
|
|
if (!user) return null;
|
|
|
|
return (
|
|
<Layout>
|
|
<div className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(110,141,255,0.12),transparent_60%)] py-10">
|
|
<div className="mx-auto w-full max-w-6xl px-4 lg:px-6 space-y-6">
|
|
<section className="rounded-3xl border border-border/40 bg-background/80 p-6 shadow-2xl backdrop-blur">
|
|
<h1 className="text-3xl font-semibold text-foreground">Project Board</h1>
|
|
<p className="mt-1 text-sm text-muted-foreground">Track tasks by status. Drag-and-drop coming next.</p>
|
|
</section>
|
|
|
|
<div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)]">
|
|
{columns.map((col) => (
|
|
<Card key={col.key} className="rounded-3xl border-border/40 bg-background/70 shadow-xl backdrop-blur-lg">
|
|
<CardHeader>
|
|
<CardTitle className="text-base flex items-center gap-2">
|
|
{col.title}
|
|
<Badge variant="outline" className="border-border/50 text-xs">{grouped[col.key].length}</Badge>
|
|
</CardTitle>
|
|
<CardDescription>{col.hint}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
{grouped[col.key].length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">No tasks.</p>
|
|
) : (
|
|
grouped[col.key].map((t) => (
|
|
<div key={t.id} className="rounded-2xl border border-border/30 bg-background/60 p-3">
|
|
<div className="font-medium text-foreground">{t.title}</div>
|
|
{t.description ? (
|
|
<p className="text-xs text-muted-foreground mt-1">{t.description}</p>
|
|
) : null}
|
|
<div className="mt-2 flex flex-wrap gap-2">
|
|
{columns.map((k) => (
|
|
<Button key={`${t.id}-${k.key}`} size="xs" variant="outline" onClick={() => move(t.id, k.key)}>
|
|
{k.title}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
<Card className="rounded-3xl border-border/40 bg-background/70 shadow-xl backdrop-blur-lg">
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Add task</CardTitle>
|
|
<CardDescription>Keep titles concise; details optional.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
<Input placeholder="Task title" value={title} onChange={(e) => setTitle(e.target.value)} />
|
|
<Textarea placeholder="Description (optional)" value={description} onChange={(e) => setDescription(e.target.value)} />
|
|
<div className="flex justify-end">
|
|
<Button onClick={handleCreate} disabled={creating || !title.trim()} className="rounded-full bg-gradient-to-r from-aethex-500 to-neon-blue text-white">
|
|
{creating ? "Creating..." : "Create task"}
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</Layout>
|
|
);
|
|
}
|