From dc97764e431fb213b451160c45fbf52ea4eeeef5 Mon Sep 17 00:00:00 2001 From: zhi Date: Sat, 21 Mar 2026 16:06:41 +0000 Subject: [PATCH 1/7] Surface canonical codes in list views --- src/pages/MilestonesPage.tsx | 4 ++-- src/pages/ProjectsPage.tsx | 2 +- src/pages/ProposesPage.tsx | 2 +- src/pages/TasksPage.tsx | 2 +- src/types/index.ts | 2 ++ 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/pages/MilestonesPage.tsx b/src/pages/MilestonesPage.tsx index f0ffab9..7bd59f5 100644 --- a/src/pages/MilestonesPage.tsx +++ b/src/pages/MilestonesPage.tsx @@ -49,10 +49,10 @@ export default function MilestonesPage() {
{milestones.map((ms) => ( -
navigate(`/milestones/${ms.id}`)}> +
navigate(`/milestones/${ms.milestone_code || ms.id}`)}>
{ms.status} -

{ms.title}

+

{ms.title}

{ms.milestone_code && {ms.milestone_code}}

{ms.description || 'No description'}

diff --git a/src/pages/ProjectsPage.tsx b/src/pages/ProjectsPage.tsx index 37473f9..014fcf5 100644 --- a/src/pages/ProjectsPage.tsx +++ b/src/pages/ProjectsPage.tsx @@ -31,7 +31,7 @@ export default function ProjectsPage() {
{projects.map((p) => ( -
navigate(`/projects/${p.id}`)}> +
navigate(`/projects/${p.project_code || p.id}`)}>

{p.name}

{p.project_code && {p.project_code}}

{p.description || 'No description'}

diff --git a/src/pages/ProposesPage.tsx b/src/pages/ProposesPage.tsx index c9f16ea..1e84e5d 100644 --- a/src/pages/ProposesPage.tsx +++ b/src/pages/ProposesPage.tsx @@ -72,7 +72,7 @@ export default function ProposesPage() {
{proposes.map((pr) => ( -
navigate(`/proposes/${pr.id}?project_id=${pr.project_id}`)}> +
navigate(`/proposes/${pr.propose_code || pr.id}?project_id=${pr.project_id}`)}>
{pr.status} {pr.propose_code && {pr.propose_code}} diff --git a/src/pages/TasksPage.tsx b/src/pages/TasksPage.tsx index 8f02aee..7bbecec 100644 --- a/src/pages/TasksPage.tsx +++ b/src/pages/TasksPage.tsx @@ -65,7 +65,7 @@ export default function TasksPage() { {tasks.map((t) => ( navigate(`/tasks/${t.id}`)} className="clickable"> - {t.id} + {t.task_code || t.id} {t.title} {t.status} {t.priority} diff --git a/src/types/index.ts b/src/types/index.ts index d7f3e7a..b647963 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -33,6 +33,7 @@ export interface ProjectMember { export interface Task { id: number + task_code: string | null title: string description: string | null task_type: 'issue' | 'maintenance' | 'research' | 'review' | 'story' | 'test' | 'resolution' // P7.1: 'task' removed @@ -64,6 +65,7 @@ export interface Comment { export interface Milestone { id: number + milestone_code: string | null title: string description: string | null status: 'open' | 'freeze' | 'undergoing' | 'completed' | 'closed' From fb5658739b5bb8c1c62a3b6464e258e203556ec3 Mon Sep 17 00:00:00 2001 From: zhi Date: Sat, 21 Mar 2026 20:28:35 +0000 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20code-first=20migration=20=E2=80=94?= =?UTF-8?q?=20replace=20raw=20IDs=20with=20codes=20and=20usernames?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- src/pages/ProjectDetailPage.tsx | 2 +- src/pages/ProposeDetailPage.tsx | 2 +- src/pages/TaskDetailPage.tsx | 4 ++-- src/pages/TasksPage.tsx | 10 +++++----- src/types/index.ts | 4 ++++ 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/pages/ProjectDetailPage.tsx b/src/pages/ProjectDetailPage.tsx index 9db0a4b..6ba7028 100644 --- a/src/pages/ProjectDetailPage.tsx +++ b/src/pages/ProjectDetailPage.tsx @@ -92,7 +92,7 @@ export default function ProjectDetailPage() {
{members.map((m) => ( - {`User #${m.user_id} (${m.role})`} + {`${m.username || m.full_name || `User #${m.user_id}`} (${m.role})`} {canEditProject && (
-

#{task.id} {task.title}

+

{task.task_code ? `[${task.task_code}]` : `#${task.id}`} {task.title}

{task.status} {task.priority} @@ -261,7 +261,7 @@ export default function TaskDetailPage() {

Comments ({comments.length})

{comments.map((c) => (
-
User #{c.author_id} · {dayjs(c.created_at).format('MM-DD HH:mm')}
+
{c.author_username || `User #${c.author_id}`} · {dayjs(c.created_at).format('MM-DD HH:mm')}

{c.content}

))} diff --git a/src/pages/TasksPage.tsx b/src/pages/TasksPage.tsx index 7bbecec..737e04c 100644 --- a/src/pages/TasksPage.tsx +++ b/src/pages/TasksPage.tsx @@ -27,8 +27,8 @@ export default function TasksPage() { useEffect(() => { fetchTasks() }, [page, statusFilter, priorityFilter]) const statusColors: Record = { - open: '#3b82f6', in_progress: '#f59e0b', resolved: '#10b981', - closed: '#6b7280', blocked: '#ef4444', + pending: '#9ca3af', open: '#3b82f6', undergoing: '#f59e0b', + completed: '#10b981', closed: '#6b7280', } return ( @@ -50,11 +50,11 @@ export default function TasksPage() {
diff --git a/src/types/index.ts b/src/types/index.ts index b647963..523a122 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -27,6 +27,8 @@ export interface Project { export interface ProjectMember { id: number user_id: number + username: string | null + full_name: string | null project_id: number role: string } @@ -59,6 +61,7 @@ export interface Comment { content: string task_id: number author_id: number + author_username: string | null created_at: string updated_at: string | null } @@ -132,6 +135,7 @@ export interface Propose { status: 'open' | 'accepted' | 'rejected' project_id: number created_by_id: number | null + created_by_username: string | null feat_task_id: string | null created_at: string updated_at: string | null From 41ebd3621829280b85c5702bce891a3a5fd7b8e1 Mon Sep 17 00:00:00 2001 From: zhi Date: Sat, 21 Mar 2026 21:38:14 +0000 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20code-first=20navigation=20=E2=80=94?= =?UTF-8?q?=20use=20resource=20codes=20in=20links=20and=20displays?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DashboardPage: show task_code instead of #id, link via code - TasksPage: navigate to tasks via task_code - MilestoneDetailPage: navigate to tasks/supports/meetings via codes - MilestoneDetailPage: display codes in support/meeting tables - Fix support/meeting state types to any[] for code property access --- src/pages/DashboardPage.tsx | 6 +++--- src/pages/MilestoneDetailPage.tsx | 18 +++++++++--------- src/pages/TasksPage.tsx | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx index cbf9bab..0727349 100644 --- a/src/pages/DashboardPage.tsx +++ b/src/pages/DashboardPage.tsx @@ -55,13 +55,13 @@ export default function DashboardPage() {

Recent Tasks

- + {(stats.recent_tasks || []).map((i) => ( - - + + diff --git a/src/pages/MilestoneDetailPage.tsx b/src/pages/MilestoneDetailPage.tsx index f5e27c5..87c3a94 100644 --- a/src/pages/MilestoneDetailPage.tsx +++ b/src/pages/MilestoneDetailPage.tsx @@ -31,8 +31,8 @@ export default function MilestoneDetailPage() { const [members, setMembers] = useState([]) const [progress, setProgress] = useState(null) const [tasks, setTasks] = useState([]) - const [supports, setSupports] = useState([]) - const [meetings, setMeetings] = useState([]) + const [supports, setSupports] = useState([]) + const [meetings, setMeetings] = useState([]) const [activeTab, setActiveTab] = useState<'tasks' | 'supports' | 'meetings'>('tasks') const [showCreateTask, setShowCreateTask] = useState(false) const [showEditMilestone, setShowEditMilestone] = useState(false) @@ -141,7 +141,7 @@ export default function MilestoneDetailPage() { if (!milestone) return
Loading...
const renderTaskRow = (t: MilestoneTask) => ( -
navigate(`/tasks/${t.id}`)}> + navigate(`/tasks/${t.task_code || t.id}`)}> @@ -352,11 +352,11 @@ export default function MilestoneDetailPage() { {activeTab === 'supports' && (
IDTitleStatusPriorityTypeSubtype
CodeTitleStatusPriorityTypeSubtype
#{i.id}{i.title}{i.task_code || `#${i.id}`}{i.title} {i.status} {i.priority} {i.task_type}{i.task_subtype || "-"}
{t.task_code || t.id} {t.title} {t.task_status || t.status}
- + {supports.map((i) => ( - navigate(`/supports/${projectCode}/${id}/${i.id}`)}> - + navigate(`/supports/${projectCode}/${id}/${i.support_code || i.id}`)}> + @@ -369,11 +369,11 @@ export default function MilestoneDetailPage() { {activeTab === 'meetings' && (
#TitleStatusPriority
CodeTitleStatusPriority
{i.id}
{i.support_code || i.id} {i.title} {i.status} {i.priority}
- + {meetings.map((i) => ( - navigate(`/meetings/${projectCode}/${id}/${i.id}`)}> - + navigate(`/meetings/${projectCode}/${id}/${i.meeting_code || i.id}`)}> + diff --git a/src/pages/TasksPage.tsx b/src/pages/TasksPage.tsx index 737e04c..165ff6e 100644 --- a/src/pages/TasksPage.tsx +++ b/src/pages/TasksPage.tsx @@ -64,7 +64,7 @@ export default function TasksPage() { {tasks.map((t) => ( - navigate(`/tasks/${t.id}`)} className="clickable"> + navigate(`/tasks/${t.task_code || t.id}`)} className="clickable"> From a65429250a61ee54aac37a984fe67169121c0f4a Mon Sep 17 00:00:00 2001 From: zhi Date: Sun, 22 Mar 2026 00:16:35 +0000 Subject: [PATCH 4/7] feat: wire meeting/support detail routes and fix code-first navigation - Add routes for /meetings/:meetingId and /supports/:supportId in App.tsx - Fix MilestoneDetailPage to navigate to code-first detail URLs - Update table headers from '#' to 'Code' for supports/meetings lists - Fix TypeScript types for supports/meetings (use any[] instead of Task[]) - MeetingDetailPage: full detail view with attend, transition, edit, delete - SupportDetailPage: full detail view with take, transition, edit, delete --- src/App.tsx | 4 + src/pages/MeetingDetailPage.tsx | 321 ++++++++++++++++++++++++++++++ src/pages/MilestoneDetailPage.tsx | 8 +- src/pages/SupportDetailPage.tsx | 298 +++++++++++++++++++++++++++ 4 files changed, 627 insertions(+), 4 deletions(-) create mode 100644 src/pages/MeetingDetailPage.tsx create mode 100644 src/pages/SupportDetailPage.tsx diff --git a/src/App.tsx b/src/App.tsx index ae7218e..c2b78a7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,8 @@ import MonitorPage from '@/pages/MonitorPage' import ProposesPage from '@/pages/ProposesPage' import ProposeDetailPage from '@/pages/ProposeDetailPage' import UsersPage from '@/pages/UsersPage' +import SupportDetailPage from '@/pages/SupportDetailPage' +import MeetingDetailPage from '@/pages/MeetingDetailPage' import axios from 'axios' const WIZARD_PORT = Number(import.meta.env.VITE_WIZARD_PORT) || 18080 @@ -96,6 +98,8 @@ export default function App() { } /> } /> } /> + } /> + } /> } /> } /> } /> diff --git a/src/pages/MeetingDetailPage.tsx b/src/pages/MeetingDetailPage.tsx new file mode 100644 index 0000000..08df180 --- /dev/null +++ b/src/pages/MeetingDetailPage.tsx @@ -0,0 +1,321 @@ +import { useState, useEffect } from 'react' +import { useParams, useNavigate } from 'react-router-dom' +import api from '@/services/api' +import { useAuth } from '@/hooks/useAuth' +import dayjs from 'dayjs' + +interface MeetingItem { + id: number + code: string | null + meeting_code: string | null + title: string + description: string | null + status: string + priority: string + project_id: number + project_code: string | null + milestone_id: number + milestone_code: string | null + reporter_id: number + meeting_time: string | null + scheduled_at: string | null + duration_minutes: number | null + participants: string[] + created_at: string + updated_at: string | null +} + +const STATUS_OPTIONS = ['scheduled', 'in_progress', 'completed', 'cancelled'] + +export default function MeetingDetailPage() { + const { meetingId } = useParams() + const navigate = useNavigate() + const { user } = useAuth() + const [meeting, setMeeting] = useState(null) + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + const [message, setMessage] = useState('') + const [editMode, setEditMode] = useState(false) + const [editForm, setEditForm] = useState({ + title: '', + description: '', + meeting_time: '', + duration_minutes: '', + }) + + const fetchMeeting = async () => { + try { + const { data } = await api.get(`/meetings/${meetingId}`) + setMeeting(data) + setEditForm({ + title: data.title, + description: data.description || '', + meeting_time: data.meeting_time || data.scheduled_at || '', + duration_minutes: data.duration_minutes ? String(data.duration_minutes) : '', + }) + } catch (err: any) { + setMessage(err.response?.data?.detail || 'Failed to load meeting') + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchMeeting() + }, [meetingId]) + + const handleAttend = async () => { + if (!meeting) return + setSaving(true) + setMessage('') + try { + const { data } = await api.post(`/meetings/${meetingId}/attend`) + setMeeting(data) + setMessage('You have joined this meeting') + } catch (err: any) { + setMessage(err.response?.data?.detail || 'Failed to attend meeting') + } finally { + setSaving(false) + } + } + + const handleTransition = async (newStatus: string) => { + if (!meeting) return + setSaving(true) + setMessage('') + try { + const { data } = await api.patch(`/meetings/${meetingId}`, { + status: newStatus, + }) + setMeeting(data) + setMessage(`Status changed to ${newStatus}`) + } catch (err: any) { + setMessage(err.response?.data?.detail || 'Failed to update meeting status') + } finally { + setSaving(false) + } + } + + const handleSave = async () => { + if (!meeting) return + setSaving(true) + setMessage('') + try { + const payload: Record = {} + if (editForm.title.trim() !== meeting.title) payload.title = editForm.title.trim() + if ((editForm.description || '') !== (meeting.description || '')) payload.description = editForm.description || null + const currentTime = meeting.meeting_time || meeting.scheduled_at || '' + if (editForm.meeting_time !== currentTime) payload.meeting_time = editForm.meeting_time || null + const currentDuration = meeting.duration_minutes ? String(meeting.duration_minutes) : '' + if (editForm.duration_minutes !== currentDuration) { + payload.duration_minutes = editForm.duration_minutes ? Number(editForm.duration_minutes) : null + } + + if (Object.keys(payload).length === 0) { + setEditMode(false) + return + } + + const { data } = await api.patch(`/meetings/${meetingId}`, payload) + setMeeting(data) + setEditMode(false) + setMessage('Meeting updated') + } catch (err: any) { + setMessage(err.response?.data?.detail || 'Failed to update meeting') + } finally { + setSaving(false) + } + } + + const handleDelete = async () => { + if (!meeting) return + if (!confirm(`Delete meeting ${meeting.meeting_code || meeting.id}? This cannot be undone.`)) return + setSaving(true) + try { + await api.delete(`/meetings/${meetingId}`) + navigate(-1) + } catch (err: any) { + setMessage(err.response?.data?.detail || 'Failed to delete meeting') + setSaving(false) + } + } + + if (loading) return
Loading meeting...
+ if (!meeting) return
{message || 'Meeting not found'}
+ + const isParticipant = user && meeting.participants.includes(user.username) + const canAttend = user && !isParticipant && meeting.status !== 'completed' && meeting.status !== 'cancelled' + const availableTransitions = STATUS_OPTIONS.filter((s) => s !== meeting.status) + const scheduledTime = meeting.meeting_time || meeting.scheduled_at + + return ( +
+ + +
+

📅 {meeting.meeting_code || `#${meeting.id}`}

+
+ {meeting.status} + {meeting.project_code && Project: {meeting.project_code}} + {meeting.milestone_code && Milestone: {meeting.milestone_code}} +
+
+ + {message && ( +
+ {message} +
+ )} + +
+ {/* Main content */} +
+ {editMode ? ( +
+ +
#TitleStatusPriority
CodeTitleStatusPriority
{i.id}
{i.meeting_code || i.id} {i.title} {i.status} {i.priority}
{t.task_code || t.id} {t.title} {t.status}