diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts index 1b97f86..8a8444d 100644 --- a/src/app/api/tasks/route.ts +++ b/src/app/api/tasks/route.ts @@ -67,7 +67,6 @@ export async function POST(request: Request) { assigneeId: task.assigneeId || user.id, assigneeName: task.assigneeName || user.name, assigneeEmail: task.assigneeEmail || user.email, - assigneeAvatarUrl: task.assigneeAvatarUrl || user.avatarUrl, }); } } @@ -84,7 +83,7 @@ export async function POST(request: Request) { assigneeId: entry.assigneeId || undefined, assigneeName: entry.assigneeName || undefined, assigneeEmail: entry.assigneeEmail || undefined, - assigneeAvatarUrl: entry.assigneeAvatarUrl || undefined, + assigneeAvatarUrl: undefined, })); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 1229de6..4d568bf 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -219,11 +219,13 @@ function KanbanDropColumn({ function KanbanTaskCard({ task, taskTags, + assigneeAvatarUrl, onOpen, onDelete, }: { task: Task taskTags: string[] + assigneeAvatarUrl?: string onOpen: () => void onDelete: () => void }) { @@ -307,7 +309,7 @@ function KanbanTaskCard({
@@ -1002,6 +1004,7 @@ export default function Home() { key={task.id} task={task} taskTags={getTags(task)} + assigneeAvatarUrl={resolveAssignee(task.assigneeId)?.avatarUrl || task.assigneeAvatarUrl} onOpen={() => router.push(`/tasks/${encodeURIComponent(task.id)}`)} onDelete={() => deleteTask(task.id)} /> diff --git a/src/app/tasks/[taskId]/page.tsx b/src/app/tasks/[taskId]/page.tsx index ecbf187..57a2c02 100644 --- a/src/app/tasks/[taskId]/page.tsx +++ b/src/app/tasks/[taskId]/page.tsx @@ -684,7 +684,7 @@ export default function TaskDetailPage() { diff --git a/src/components/BacklogView.tsx b/src/components/BacklogView.tsx index 449c366..4b7242a 100644 --- a/src/components/BacklogView.tsx +++ b/src/components/BacklogView.tsx @@ -1,6 +1,6 @@ "use client" -import { useState, type ReactNode } from "react" +import { useEffect, useState, type ReactNode } from "react" import { DndContext, DragEndEvent, @@ -42,6 +42,13 @@ const typeLabels: Record = { plan: "📐", } +interface AssignableUser { + id: string + name: string + email?: string + avatarUrl?: string +} + function AssigneeAvatar({ name, avatarUrl, seed }: { name?: string; avatarUrl?: string; seed?: string }) { const displayUrl = avatarUrl || generateAvatarDataUrl(seed || name || "unassigned", name || "Unassigned") return ( @@ -57,9 +64,11 @@ function AssigneeAvatar({ name, avatarUrl, seed }: { name?: string; avatarUrl?: // Sortable Task Row function SortableTaskRow({ task, + assigneeAvatarUrl, onClick, }: { task: Task + assigneeAvatarUrl?: string onClick: () => void }) { const { @@ -103,13 +112,13 @@ function SortableTaskRow({ {task.comments && task.comments.length > 0 && ( 💬 {task.comments.length} )} - +
) } // Drag Overlay Item -function DragOverlayItem({ task }: { task: Task }) { +function DragOverlayItem({ task, assigneeAvatarUrl }: { task: Task; assigneeAvatarUrl?: string }) { return (
@@ -120,7 +129,7 @@ function DragOverlayItem({ task }: { task: Task }) { {task.priority} - +
) } @@ -145,6 +154,7 @@ function TaskSection({ isOpen, onToggle, onTaskClick, + resolveAssigneeAvatar, sprintInfo, }: { title: string @@ -152,6 +162,7 @@ function TaskSection({ isOpen: boolean onToggle: () => void onTaskClick: (task: Task) => void + resolveAssigneeAvatar: (task: Task) => string | undefined sprintInfo?: { name: string; date: string; status: string } }) { return ( @@ -196,6 +207,7 @@ function TaskSection({ onTaskClick(task)} /> )) @@ -210,6 +222,7 @@ function TaskSection({ export function BacklogView() { const router = useRouter() + const [assignableUsers, setAssignableUsers] = useState([]) const { tasks, sprints, @@ -218,6 +231,37 @@ export function BacklogView() { addSprint, } = useTaskStore() + useEffect(() => { + let active = true + const loadUsers = async () => { + try { + const response = await fetch("/api/auth/users", { cache: "no-store" }) + if (!response.ok) return + const data = await response.json() + if (!active || !Array.isArray(data?.users)) return + setAssignableUsers( + data.users.map((entry: { id: string; name: string; email?: string; avatarUrl?: string }) => ({ + id: entry.id, + name: entry.name, + email: entry.email, + avatarUrl: entry.avatarUrl, + })), + ) + } catch { + // Keep backlog usable if users lookup fails. + } + } + void loadUsers() + return () => { + active = false + } + }, []) + + const resolveAssigneeAvatar = (task: Task) => { + if (!task.assigneeId) return task.assigneeAvatarUrl + return assignableUsers.find((user) => user.id === task.assigneeId)?.avatarUrl || task.assigneeAvatarUrl + } + const [activeId, setActiveId] = useState(null) const [openSections, setOpenSections] = useState>({ current: true, @@ -331,6 +375,7 @@ export function BacklogView() { isOpen={openSections.current} onToggle={() => toggleSection("current")} onTaskClick={(task) => router.push(`/tasks/${encodeURIComponent(task.id)}`)} + resolveAssigneeAvatar={resolveAssigneeAvatar} sprintInfo={ currentSprint ? { @@ -362,6 +407,7 @@ export function BacklogView() { isOpen={openSections[sprint.id] ?? false} onToggle={() => toggleSection(sprint.id)} onTaskClick={(task) => router.push(`/tasks/${encodeURIComponent(task.id)}`)} + resolveAssigneeAvatar={resolveAssigneeAvatar} sprintInfo={{ name: sprint.name, date: (() => { @@ -439,12 +485,13 @@ export function BacklogView() { isOpen={openSections.backlog} onToggle={() => toggleSection("backlog")} onTaskClick={(task) => router.push(`/tasks/${encodeURIComponent(task.id)}`)} + resolveAssigneeAvatar={resolveAssigneeAvatar} /> - {activeTask ? : null} + {activeTask ? : null} ) diff --git a/src/lib/server/taskDb.ts b/src/lib/server/taskDb.ts index 9d9e5ec..75fb04e 100644 --- a/src/lib/server/taskDb.ts +++ b/src/lib/server/taskDb.ts @@ -98,6 +98,13 @@ type SqliteDb = InstanceType; let db: SqliteDb | null = null; +interface UserProfileLookup { + id: string; + name: string; + email?: string; + avatarUrl?: string; +} + function ensureTaskSchema(database: SqliteDb) { const taskColumns = database.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>; if (!taskColumns.some((column) => column.name === "attachments")) { @@ -145,6 +152,32 @@ function safeParseArray(value: string | null, fallback: T[]): T[] { } } +function getUserLookup(database: SqliteDb): Map { + const hasUsersTable = database + .prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'users' LIMIT 1") + .get() as { 1: number } | undefined; + if (!hasUsersTable) return new Map(); + + try { + const rows = database + .prepare("SELECT id, name, email, avatarUrl FROM users") + .all() as Array<{ id: string; name: string; email: string | null; avatarUrl: string | null }>; + + const lookup = new Map(); + for (const row of rows) { + lookup.set(row.id, { + id: row.id, + name: row.name, + email: row.email ?? undefined, + avatarUrl: row.avatarUrl ?? undefined, + }); + } + return lookup; + } catch { + return new Map(); + } +} + function normalizeAttachments(attachments: unknown): TaskAttachment[] { if (!Array.isArray(attachments)) return []; @@ -421,6 +454,7 @@ function getDb(): SqliteDb { export function getData(): DataStore { const database = getDb(); + const usersById = getUserLookup(database); const projects = database.prepare("SELECT * FROM projects ORDER BY createdAt ASC").all() as Array<{ id: string; @@ -486,32 +520,38 @@ export function getData(): DataStore { projectId: sprint.projectId, createdAt: sprint.createdAt, })), - tasks: tasks.map((task) => ({ - id: task.id, - title: task.title, - description: task.description ?? undefined, - type: task.type, - status: task.status, - priority: task.priority, - projectId: task.projectId, - sprintId: task.sprintId ?? undefined, - createdAt: task.createdAt, - updatedAt: task.updatedAt, - createdById: task.createdById ?? undefined, - createdByName: task.createdByName ?? undefined, - createdByAvatarUrl: task.createdByAvatarUrl ?? undefined, - updatedById: task.updatedById ?? undefined, - updatedByName: task.updatedByName ?? undefined, - updatedByAvatarUrl: task.updatedByAvatarUrl ?? undefined, - assigneeId: task.assigneeId ?? undefined, - assigneeName: task.assigneeName ?? undefined, - assigneeEmail: task.assigneeEmail ?? undefined, - assigneeAvatarUrl: task.assigneeAvatarUrl ?? undefined, - dueDate: task.dueDate ?? undefined, - comments: normalizeComments(safeParseArray(task.comments, [])), - tags: safeParseArray(task.tags, []), - attachments: normalizeAttachments(safeParseArray(task.attachments, [])), - })), + tasks: tasks.map((task) => { + const createdByUser = task.createdById ? usersById.get(task.createdById) : undefined; + const updatedByUser = task.updatedById ? usersById.get(task.updatedById) : undefined; + const assigneeUser = task.assigneeId ? usersById.get(task.assigneeId) : undefined; + + return { + id: task.id, + title: task.title, + description: task.description ?? undefined, + type: task.type, + status: task.status, + priority: task.priority, + projectId: task.projectId, + sprintId: task.sprintId ?? undefined, + createdAt: task.createdAt, + updatedAt: task.updatedAt, + createdById: task.createdById ?? undefined, + createdByName: task.createdByName ?? createdByUser?.name ?? undefined, + createdByAvatarUrl: createdByUser?.avatarUrl ?? task.createdByAvatarUrl ?? undefined, + updatedById: task.updatedById ?? undefined, + updatedByName: task.updatedByName ?? updatedByUser?.name ?? undefined, + updatedByAvatarUrl: updatedByUser?.avatarUrl ?? task.updatedByAvatarUrl ?? undefined, + assigneeId: task.assigneeId ?? undefined, + assigneeName: assigneeUser?.name ?? task.assigneeName ?? undefined, + assigneeEmail: assigneeUser?.email ?? task.assigneeEmail ?? undefined, + assigneeAvatarUrl: assigneeUser?.avatarUrl ?? undefined, + dueDate: task.dueDate ?? undefined, + comments: normalizeComments(safeParseArray(task.comments, [])), + tags: safeParseArray(task.tags, []), + attachments: normalizeAttachments(safeParseArray(task.attachments, [])), + }; + }), lastUpdated: getLastUpdated(database), }; }