feat: code-first migration — replace raw IDs with codes and usernames

- TaskDetailPage: show task_code instead of raw #id in header
- TaskDetailPage: show author_username in comment metadata
- ProjectDetailPage: show member username instead of User #id
- ProposeDetailPage: show created_by_username instead of User #id
- TasksPage: fix status filter options to match actual statuses
  (pending/open/undergoing/completed/closed)
- TasksPage: fix status color map for correct status values
- Types: add username/full_name to ProjectMember, author_username
  to Comment, created_by_username to Propose
- Supports TODO §3.1 (code-first UI migration)
This commit is contained in:
zhi
2026-03-21 20:28:35 +00:00
parent dc97764e43
commit fb5658739b
5 changed files with 13 additions and 9 deletions

View File

@@ -92,7 +92,7 @@ export default function ProjectDetailPage() {
<div className="member-list"> <div className="member-list">
{members.map((m) => ( {members.map((m) => (
<span key={m.id} className="badge" style={{ marginRight: 8 }}> <span key={m.id} className="badge" style={{ marginRight: 8 }}>
{`User #${m.user_id} (${m.role})`} {`${m.username || m.full_name || `User #${m.user_id}`} (${m.role})`}
{canEditProject && ( {canEditProject && (
<button <button
onClick={(e) => { e.stopPropagation(); removeMember(m.user_id, m.role) }} onClick={(e) => { e.stopPropagation(); removeMember(m.user_id, m.role) }}

View File

@@ -138,7 +138,7 @@ export default function ProposeDetailPage() {
<div className="detail-grid"> <div className="detail-grid">
<div><strong>Propose Code:</strong> {propose.propose_code || '—'}</div> <div><strong>Propose Code:</strong> {propose.propose_code || '—'}</div>
<div><strong>Status:</strong> {propose.status}</div> <div><strong>Status:</strong> {propose.status}</div>
<div><strong>Created By:</strong> User #{propose.created_by_id || '—'}</div> <div><strong>Created By:</strong> {propose.created_by_username || (propose.created_by_id ? `User #${propose.created_by_id}` : '—')}</div>
<div><strong>Created:</strong> {dayjs(propose.created_at).format('YYYY-MM-DD HH:mm')}</div> <div><strong>Created:</strong> {dayjs(propose.created_at).format('YYYY-MM-DD HH:mm')}</div>
<div><strong>Updated:</strong> {propose.updated_at ? dayjs(propose.updated_at).format('YYYY-MM-DD HH:mm') : '—'}</div> <div><strong>Updated:</strong> {propose.updated_at ? dayjs(propose.updated_at).format('YYYY-MM-DD HH:mm') : '—'}</div>
<div><strong>Feature Task:</strong> {propose.feat_task_id || '—'}</div> <div><strong>Feature Task:</strong> {propose.feat_task_id || '—'}</div>

View File

@@ -120,7 +120,7 @@ export default function TaskDetailPage() {
<button className="btn-back" onClick={() => navigate('/tasks')}> Back</button> <button className="btn-back" onClick={() => navigate('/tasks')}> Back</button>
<div className="task-header"> <div className="task-header">
<h2>#{task.id} {task.title}</h2> <h2>{task.task_code ? `[${task.task_code}]` : `#${task.id}`} {task.title}</h2>
<div className="task-meta"> <div className="task-meta">
<span className={`badge status-${task.status}`}>{task.status}</span> <span className={`badge status-${task.status}`}>{task.status}</span>
<span className={`badge priority-${task.priority}`}>{task.priority}</span> <span className={`badge priority-${task.priority}`}>{task.priority}</span>
@@ -261,7 +261,7 @@ export default function TaskDetailPage() {
<h3>Comments ({comments.length})</h3> <h3>Comments ({comments.length})</h3>
{comments.map((c) => ( {comments.map((c) => (
<div className="comment" key={c.id}> <div className="comment" key={c.id}>
<div className="comment-meta">User #{c.author_id} · {dayjs(c.created_at).format('MM-DD HH:mm')}</div> <div className="comment-meta">{c.author_username || `User #${c.author_id}`} · {dayjs(c.created_at).format('MM-DD HH:mm')}</div>
<p>{c.content}</p> <p>{c.content}</p>
</div> </div>
))} ))}

View File

@@ -27,8 +27,8 @@ export default function TasksPage() {
useEffect(() => { fetchTasks() }, [page, statusFilter, priorityFilter]) useEffect(() => { fetchTasks() }, [page, statusFilter, priorityFilter])
const statusColors: Record<string, string> = { const statusColors: Record<string, string> = {
open: '#3b82f6', in_progress: '#f59e0b', resolved: '#10b981', pending: '#9ca3af', open: '#3b82f6', undergoing: '#f59e0b',
closed: '#6b7280', blocked: '#ef4444', completed: '#10b981', closed: '#6b7280',
} }
return ( return (
@@ -50,11 +50,11 @@ export default function TasksPage() {
<div className="filters"> <div className="filters">
<select value={statusFilter} onChange={(e) => { setStatusFilter(e.target.value); setPage(1) }}> <select value={statusFilter} onChange={(e) => { setStatusFilter(e.target.value); setPage(1) }}>
<option value="">All statuses</option> <option value="">All statuses</option>
<option value="pending">Pending</option>
<option value="open">Open</option> <option value="open">Open</option>
<option value="in_progress">In Progress</option> <option value="undergoing">Undergoing</option>
<option value="resolved">Resolved</option> <option value="completed">Completed</option>
<option value="closed">Closed</option> <option value="closed">Closed</option>
<option value="blocked">Blocked</option>
</select> </select>
</div> </div>

View File

@@ -27,6 +27,8 @@ export interface Project {
export interface ProjectMember { export interface ProjectMember {
id: number id: number
user_id: number user_id: number
username: string | null
full_name: string | null
project_id: number project_id: number
role: string role: string
} }
@@ -59,6 +61,7 @@ export interface Comment {
content: string content: string
task_id: number task_id: number
author_id: number author_id: number
author_username: string | null
created_at: string created_at: string
updated_at: string | null updated_at: string | null
} }
@@ -132,6 +135,7 @@ export interface Propose {
status: 'open' | 'accepted' | 'rejected' status: 'open' | 'accepted' | 'rejected'
project_id: number project_id: number
created_by_id: number | null created_by_id: number | null
created_by_username: string | null
feat_task_id: string | null feat_task_id: string | null
created_at: string created_at: string
updated_at: string | null updated_at: string | null