feat(m4): project & member management with tests and e2e

- ProjectRepository, ProjectMemberRepository, minimal NotificationRepository
- ProjectService: create/update/archive/delete project, add/remove member,
  permission checks (isMember/admin), member-added notification, getDashboard
  skeleton, getMyProjects, getMemberRole
- projectValidator, API services factory
- API routes: projects CRUD, members list/add/remove
- Screens: dashboard (project list + create form), project overview, members,
  settings; layout (Header/Sidebar/ProjectNav) and project components
- Unit tests (Project/ProjectMember repositories, ProjectService),
  integration project-member-permission, e2e project-management
This commit is contained in:
Ken Yasue
2026-06-25 01:02:10 +02:00
parent 8a67523c0e
commit 9eb391ea8d
30 changed files with 2230 additions and 14 deletions

View File

@ -0,0 +1,37 @@
'use client';
import { useRouter } from 'next/navigation';
import type { PublicUser } from '@/lib/auth/getCurrentUser';
export function Header({ user }: { user: PublicUser }) {
const router = useRouter();
async function handleLogout() {
await fetch('/api/auth/logout', { method: 'POST' });
router.push('/login');
router.refresh();
}
return (
<header className="flex items-center justify-between border-b bg-white px-6 py-3">
<a href="/dashboard" className="font-bold text-gray-800">
</a>
<nav className="flex items-center gap-4 text-sm">
<a href="/dashboard" className="text-gray-600 hover:underline">
</a>
<a href="/profile" className="text-gray-600 hover:underline">
{user.name}
</a>
<button
type="button"
onClick={handleLogout}
className="text-blue-600 hover:underline"
>
</button>
</nav>
</header>
);
}

View File

@ -0,0 +1,45 @@
const NAV_ITEMS = [
{ href: '', label: '概要' },
{ href: '/members', label: 'メンバー' },
{ href: '/settings', label: '設定' },
] as const;
/**
* プロジェクト内サブナビゲーション。
* href は `/projects/{projectId}` を起点とする相対パス。
*/
export function ProjectNav({
projectId,
active,
}: {
projectId: number;
active: 'overview' | 'members' | 'settings';
}) {
const activeMap: Record<string, boolean> = {
overview: active === 'overview',
members: active === 'members',
settings: active === 'settings',
};
return (
<nav className="flex gap-4 border-b bg-white px-6 py-2 text-sm">
{NAV_ITEMS.map((item) => {
const key = item.href === '' ? 'overview' : item.href.slice(1);
const href = `/projects/${projectId}${item.href}`;
return (
<a
key={item.href}
href={href}
className={
activeMap[key]
? 'font-medium text-blue-600'
: 'text-gray-600 hover:underline'
}
>
{item.label}
</a>
);
})}
</nav>
);
}

View File

@ -0,0 +1,9 @@
/**
* プロジェクトページ共通のサイドバー。
* プロジェクト内ナビゲーションは ProjectNav に委譲する。
*/
export function Sidebar({ children }: { children: React.ReactNode }) {
return (
<aside className="w-48 shrink-0 border-r bg-gray-50 p-4">{children}</aside>
);
}