feat(m9): todo/kanban with DnD, assignee notify, completion, tests, e2e

- TodoRepository (column CRUD, item CRUD, soft-delete, ordering, maxOrderIndex,
  project isolation) + TodoService (lazy standard-column init, member/admin
  permission, create/update/move/toggleComplete/delete, todo_assigned notify,
  todo_created/updated/completed activity logs, SSE todo.updated)
- APIs: columns + items (GET/POST/PATCH/DELETE), toggleComplete + move
- KanbanBoard client (HTML5 drag&drop move, new task per column, complete),
  todos page, ProjectNav ToDo link
- Unit tests (TodoRepository, TodoService) + e2e todo-kanban
This commit is contained in:
Ken Yasue
2026-06-25 01:54:24 +02:00
parent d2a4d56543
commit 385b251cc7
13 changed files with 1399 additions and 0 deletions

View File

@ -0,0 +1,49 @@
import { redirect } from 'next/navigation';
import { getCurrentUser, toPublicUser } from '@/lib/auth/getCurrentUser';
import { createTodoService, createProjectService } from '@/lib/api/services';
import { Header } from '@/components/layout/Header';
import { ProjectNav } from '@/components/layout/ProjectNav';
import { KanbanBoard } from '@/components/todo/KanbanBoard';
import { ForbiddenError, NotFoundError } from '@/lib/errors';
export const dynamic = 'force-dynamic';
export default async function TodosPage({
params,
}: {
params: Promise<{ projectId: string }>;
}) {
const user = await getCurrentUser();
if (!user) redirect('/login');
const { projectId } = await params;
const projectService = createProjectService();
let project: Awaited<ReturnType<typeof projectService.getProject>>;
try {
project = projectService.getProject(user.id, Number(projectId));
} catch (error) {
if (error instanceof ForbiddenError || error instanceof NotFoundError) {
redirect('/dashboard');
}
throw error;
}
const todoService = createTodoService();
const columns = todoService.getColumns(user.id, project.id);
const items = todoService.getItems(user.id, project.id);
return (
<div className="min-h-screen bg-gray-50">
<Header user={toPublicUser(user)} />
<ProjectNav projectId={project.id} active="todos" />
<main className="space-y-4 p-6">
<h1 className="text-2xl font-bold">ToDo / Kanban</h1>
<KanbanBoard
projectId={project.id}
columns={columns}
initialItems={items}
/>
</main>
</div>
);
}