From e60763b128564d2ea7a1175604ae9092980a2b32 Mon Sep 17 00:00:00 2001 From: zhi Date: Tue, 17 Mar 2026 01:04:51 +0000 Subject: [PATCH 01/11] feat: update frontend type definitions and status enums to match new backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - types/index.ts: Task status 'progressing' → 'undergoing' + 'completed'; Milestone status updated to open/freeze/undergoing/completed/closed + started_at field - MilestoneFormModal: dropdown options updated - MilestoneDetailPage: isProgressing → isUndergoing, badge class simplified - MilestonesPage: badge class simplified - TaskDetailPage: status transition map updated for new state machine --- src/components/MilestoneFormModal.tsx | 6 +++--- src/pages/MilestoneDetailPage.tsx | 8 ++++---- src/pages/MilestonesPage.tsx | 2 +- src/pages/TaskDetailPage.tsx | 7 ++++--- src/types/index.ts | 5 +++-- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/components/MilestoneFormModal.tsx b/src/components/MilestoneFormModal.tsx index 00e6ecb..7147dc1 100644 --- a/src/components/MilestoneFormModal.tsx +++ b/src/components/MilestoneFormModal.tsx @@ -125,9 +125,9 @@ export default function MilestoneFormModal({ isOpen, onClose, onSaved, milestone Status diff --git a/src/pages/MilestoneDetailPage.tsx b/src/pages/MilestoneDetailPage.tsx index fe85746..37db6cf 100644 --- a/src/pages/MilestoneDetailPage.tsx +++ b/src/pages/MilestoneDetailPage.tsx @@ -96,7 +96,7 @@ export default function MilestoneDetailPage() { currentMemberRole === 'admin' )) - const isProgressing = milestone?.status === 'progressing' + const isUndergoing = milestone?.status === 'undergoing' if (!milestone) return
Loading...
@@ -117,7 +117,7 @@ export default function MilestoneDetailPage() {

🏁 {milestone.title}

- {milestone.status} + {milestone.status} {milestone.due_date && Due {dayjs(milestone.due_date).format('YYYY-MM-DD')}} {milestone.planned_release_date && Planned Release: {dayjs(milestone.planned_release_date).format('YYYY-MM-DD')}}
@@ -154,14 +154,14 @@ export default function MilestoneDetailPage() {
- {!isProgressing && canEditMilestone && ( + {!isUndergoing && canEditMilestone && ( <> )} - {isProgressing && Milestone is in progress - cannot add new items} + {isUndergoing && Milestone is undergoing - cannot add new items}
(
navigate(`/milestones/${ms.id}`)}>
- {ms.status} + {ms.status}

{ms.title}

{ms.description || 'No description'}

diff --git a/src/pages/TaskDetailPage.tsx b/src/pages/TaskDetailPage.tsx index a3194f3..0c5f91d 100644 --- a/src/pages/TaskDetailPage.tsx +++ b/src/pages/TaskDetailPage.tsx @@ -63,9 +63,10 @@ export default function TaskDetailPage() { if (!task) return
Loading...
const statusActions: Record = { - open: ['progressing', 'closed'], - pending: ['progressing', 'closed'], - progressing: ['pending', 'closed'], + open: ['undergoing', 'closed'], + pending: ['open', 'closed'], + undergoing: ['completed', 'closed'], + completed: ['open'], closed: ['open'], } diff --git a/src/types/index.ts b/src/types/index.ts index fa48069..73ff651 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -35,7 +35,7 @@ export interface Task { description: string | null task_type: 'issue' | 'maintenance' | 'research' | 'review' | 'story' | 'test' | 'resolution' | 'task' task_subtype: string | null - status: 'open' | 'pending' | 'progressing' | 'closed' + status: 'open' | 'pending' | 'undergoing' | 'completed' | 'closed' priority: 'low' | 'medium' | 'high' | 'critical' project_id: number milestone_id: number | null @@ -64,7 +64,8 @@ export interface Milestone { id: number title: string description: string | null - status: 'open' | 'pending' | 'deferred' | 'progressing' | 'closed' + status: 'open' | 'freeze' | 'undergoing' | 'completed' | 'closed' + started_at: string | null project_id: number created_by_id: number | null due_date: string | null -- 2.49.1 From 35e7d3a141b69b03beaee1760b268d62cad01553 Mon Sep 17 00:00:00 2001 From: zhi Date: Tue, 17 Mar 2026 05:03:49 +0000 Subject: [PATCH 02/11] feat(P10): add Propose type, list page, detail page with accept/reject/reopen + sidebar link --- src/App.tsx | 4 + src/components/Sidebar.tsx | 1 + src/pages/ProposeDetailPage.tsx | 190 ++++++++++++++++++++++++++++++++ src/pages/ProposesPage.tsx | 110 ++++++++++++++++++ src/types/index.ts | 13 +++ 5 files changed, 318 insertions(+) create mode 100644 src/pages/ProposeDetailPage.tsx create mode 100644 src/pages/ProposesPage.tsx diff --git a/src/App.tsx b/src/App.tsx index 6d38b45..4aab74c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,6 +14,8 @@ import MilestoneDetailPage from '@/pages/MilestoneDetailPage' import NotificationsPage from '@/pages/NotificationsPage' import RoleEditorPage from '@/pages/RoleEditorPage' import MonitorPage from '@/pages/MonitorPage' +import ProposesPage from '@/pages/ProposesPage' +import ProposeDetailPage from '@/pages/ProposeDetailPage' import axios from 'axios' const WIZARD_PORT = Number(import.meta.env.VITE_WIZARD_PORT) || 18080 @@ -90,6 +92,8 @@ export default function App() { } /> } /> } /> + } /> + } /> } /> } /> } /> diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 3395448..7868417 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -32,6 +32,7 @@ export default function Sidebar({ user, onLogout }: Props) { const links = user ? [ { to: '/', icon: '📊', label: 'Dashboard' }, { to: '/projects', icon: '📁', label: 'Projects' }, + { to: '/proposes', icon: '💡', label: 'Proposes' }, { to: '/notifications', icon: '🔔', label: 'Notifications' + (unreadCount > 0 ? ' (' + unreadCount + ')' : '') }, { to: '/monitor', icon: '📡', label: 'Monitor' }, ...(user.is_admin ? [{ to: '/roles', icon: '🔐', label: 'Roles' }] : []), diff --git a/src/pages/ProposeDetailPage.tsx b/src/pages/ProposeDetailPage.tsx new file mode 100644 index 0000000..f9ad52e --- /dev/null +++ b/src/pages/ProposeDetailPage.tsx @@ -0,0 +1,190 @@ +import { useState, useEffect } from 'react' +import { useParams, useNavigate, useSearchParams } from 'react-router-dom' +import api from '@/services/api' +import type { Propose, Milestone } from '@/types' +import dayjs from 'dayjs' + +export default function ProposeDetailPage() { + const { id } = useParams() + const [searchParams] = useSearchParams() + const projectId = searchParams.get('project_id') + const navigate = useNavigate() + + const [propose, setPropose] = useState(null) + const [milestones, setMilestones] = useState([]) + const [showAccept, setShowAccept] = useState(false) + const [selectedMilestone, setSelectedMilestone] = useState('') + const [actionLoading, setActionLoading] = useState(false) + const [error, setError] = useState('') + + const fetchPropose = () => { + if (!projectId) return + api.get(`/projects/${projectId}/proposes/${id}`).then(({ data }) => setPropose(data)) + } + + useEffect(() => { + fetchPropose() + }, [id, projectId]) + + const loadMilestones = () => { + if (!projectId) return + api.get(`/milestones?project_id=${projectId}`) + .then(({ data }) => setMilestones(data.filter((m) => m.status === 'open'))) + } + + const handleAccept = async () => { + if (!selectedMilestone || !projectId) return + setActionLoading(true) + setError('') + try { + await api.post(`/projects/${projectId}/proposes/${id}/accept`, { milestone_id: selectedMilestone }) + setShowAccept(false) + fetchPropose() + } catch (err: any) { + setError(err.response?.data?.detail || 'Accept failed') + } finally { + setActionLoading(false) + } + } + + const handleReject = async () => { + if (!projectId) return + const reason = prompt('Reject reason (optional):') + setActionLoading(true) + setError('') + try { + await api.post(`/projects/${projectId}/proposes/${id}/reject`, { reason: reason || undefined }) + fetchPropose() + } catch (err: any) { + setError(err.response?.data?.detail || 'Reject failed') + } finally { + setActionLoading(false) + } + } + + const handleReopen = async () => { + if (!projectId) return + setActionLoading(true) + setError('') + try { + await api.post(`/projects/${projectId}/proposes/${id}/reopen`) + fetchPropose() + } catch (err: any) { + setError(err.response?.data?.detail || 'Reopen failed') + } finally { + setActionLoading(false) + } + } + + if (!propose) return
Loading...
+ + const statusBadgeClass = (s: string) => { + if (s === 'open') return 'status-open' + if (s === 'accepted') return 'status-completed' + if (s === 'rejected') return 'status-closed' + return '' + } + + return ( +
+ + +
+

+ 💡 {propose.title} + {propose.propose_code && {propose.propose_code}} +

+ + {propose.status} + +
+ + {error &&
{error}
} + +
+

Details

+
+
Propose Code: {propose.propose_code || '—'}
+
Status: {propose.status}
+
Created By: User #{propose.created_by_id || '—'}
+
Created: {dayjs(propose.created_at).format('YYYY-MM-DD HH:mm')}
+
Updated: {propose.updated_at ? dayjs(propose.updated_at).format('YYYY-MM-DD HH:mm') : '—'}
+
Feature Task: {propose.feat_task_id || '—'}
+
+
+ +
+

Description

+

{propose.description || 'No description'}

+
+ + {/* Action buttons */} +
+ {propose.status === 'open' && ( + <> + + + + )} + {propose.status === 'accepted' && propose.feat_task_id && ( + + )} + {propose.status === 'rejected' && ( + + )} +
+ + {/* Accept modal with milestone selector */} + {showAccept && ( +
setShowAccept(false)}> +
e.stopPropagation()}> +

Accept Propose

+

Select an open milestone to create a feature story task in:

+ + {milestones.length === 0 && ( +

No open milestones available.

+ )} +
+ + +
+
+
+ )} +
+ ) +} diff --git a/src/pages/ProposesPage.tsx b/src/pages/ProposesPage.tsx new file mode 100644 index 0000000..c9f16ea --- /dev/null +++ b/src/pages/ProposesPage.tsx @@ -0,0 +1,110 @@ +import { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import api from '@/services/api' +import type { Propose, Project } from '@/types' +import dayjs from 'dayjs' + +export default function ProposesPage() { + const [proposes, setProposes] = useState([]) + const [projects, setProjects] = useState([]) + const [projectFilter, setProjectFilter] = useState('') + const [showCreate, setShowCreate] = useState(false) + const [newTitle, setNewTitle] = useState('') + const [newDesc, setNewDesc] = useState('') + const [creating, setCreating] = useState(false) + const navigate = useNavigate() + + const fetchProposes = () => { + if (!projectFilter) { + setProposes([]) + return + } + api.get(`/projects/${projectFilter}/proposes`).then(({ data }) => setProposes(data)) + } + + useEffect(() => { + api.get('/projects').then(({ data }) => setProjects(data)) + }, []) + + useEffect(() => { fetchProposes() }, [projectFilter]) + + const handleCreate = async () => { + if (!newTitle.trim() || !projectFilter) return + setCreating(true) + try { + await api.post(`/projects/${projectFilter}/proposes`, { + title: newTitle.trim(), + description: newDesc.trim() || null, + }) + setNewTitle('') + setNewDesc('') + setShowCreate(false) + fetchProposes() + } finally { + setCreating(false) + } + } + + const statusBadgeClass = (s: string) => { + if (s === 'open') return 'status-open' + if (s === 'accepted') return 'status-completed' + if (s === 'rejected') return 'status-closed' + return '' + } + + return ( +
+
+

💡 Proposes ({proposes.length})

+ +
+ +
+ +
+ + {!projectFilter &&

Please select a project to view proposes.

} + +
+ {proposes.map((pr) => ( +
navigate(`/proposes/${pr.id}?project_id=${pr.project_id}`)}> +
+ {pr.status} + {pr.propose_code && {pr.propose_code}} +

{pr.title}

+
+

{pr.description || 'No description'}

+
+ {pr.feat_task_id && Task: {pr.feat_task_id}} + Created {dayjs(pr.created_at).format('YYYY-MM-DD')} +
+
+ ))} + {projectFilter && proposes.length === 0 &&

No proposes

} +
+ + {showCreate && ( +
setShowCreate(false)}> +
e.stopPropagation()}> +

New Propose

+ + setNewTitle(e.target.value)} placeholder="Propose title" /> + +