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,45 @@
import type { SqliteDatabase } from '@/lib/db/sqlite';
import type { Notification, NotificationType } from '@/lib/types';
export interface CreateNotificationInput {
userId: number;
projectId: number | null;
type: NotificationType;
title: string;
body: string | null;
}
/**
* notificationsテーブルへのデータアクセスを担うRepository。
* M4ではメンバー追加通知のために create のみ実装する。
* 一覧取得・既読化は M5 で拡張する。
*/
export class NotificationRepository {
constructor(private readonly db: SqliteDatabase) {}
create(input: CreateNotificationInput): Notification {
const now = new Date().toISOString();
const result = this.db.execute(
`INSERT INTO notifications (user_id, project_id, type, title, body, read_at, created_at)
VALUES (@userId, @projectId, @type, @title, @body, NULL, @createdAt)`,
{
userId: input.userId,
projectId: input.projectId,
type: input.type,
title: input.title,
body: input.body,
createdAt: now,
}
);
return {
id: Number(result.lastInsertRowid),
userId: input.userId,
projectId: input.projectId,
type: input.type,
title: input.title,
body: input.body,
readAt: null,
createdAt: now,
};
}
}