fix: align task pages with backend task api
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import api from '@/services/api'
|
import api from '@/services/api'
|
||||||
import type { Project } from '@/types'
|
import type { Project, Milestone } from '@/types'
|
||||||
|
|
||||||
const TASK_TYPES = [
|
const TASK_TYPES = [
|
||||||
{ value: 'story', label: 'Story', subtypes: ['feature', 'improvement', 'refactor'] },
|
{ value: 'story', label: 'Story', subtypes: ['feature', 'improvement', 'refactor'] },
|
||||||
@@ -11,38 +11,56 @@ const TASK_TYPES = [
|
|||||||
{ value: 'maintenance', label: 'Maintenance', subtypes: ['deploy', 'release'] },
|
{ value: 'maintenance', label: 'Maintenance', subtypes: ['deploy', 'release'] },
|
||||||
{ value: 'research', label: 'Research', subtypes: [] },
|
{ value: 'research', label: 'Research', subtypes: [] },
|
||||||
{ value: 'review', label: 'Review', subtypes: ['code_review', 'decision_review', 'function_review'] },
|
{ value: 'review', label: 'Review', subtypes: ['code_review', 'decision_review', 'function_review'] },
|
||||||
{ value: 'support', label: 'Support', subtypes: ['access', 'information'] },
|
|
||||||
{ value: 'meeting', label: 'Meeting', subtypes: ['conference', 'handover', 'recap'] },
|
|
||||||
{ value: 'resolution', label: 'Resolution', subtypes: [] },
|
{ value: 'resolution', label: 'Resolution', subtypes: [] },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function CreateTaskPage() {
|
export default function CreateTaskPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [projects, setProjects] = useState<Project[]>([])
|
const [projects, setProjects] = useState<Project[]>([])
|
||||||
|
const [milestones, setMilestones] = useState<Milestone[]>([])
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
title: '', description: '', project_id: 0, issue_type: 'task',
|
title: '', description: '', project_id: 0, milestone_id: 0, task_type: 'task',
|
||||||
issue_subtype: '', priority: 'medium', tags: '', reporter_id: 1,
|
task_subtype: '', priority: 'medium', tags: '', reporter_id: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.get<Project[]>('/projects').then(({ data }) => {
|
api.get<Project[]>('/projects').then(({ data }) => {
|
||||||
setProjects(data)
|
setProjects(data)
|
||||||
if (data.length) setForm((f) => ({ ...f, project_id: data[0].id }))
|
if (data.length) {
|
||||||
|
setForm((f) => ({ ...f, project_id: data[0].id }))
|
||||||
|
// Load milestones for the first project
|
||||||
|
api.get<Milestone[]>(`/milestones?project_id=${data[0].id}`).then(({ data: ms }) => {
|
||||||
|
setMilestones(ms)
|
||||||
|
if (ms.length) setForm((f) => ({ ...f, milestone_id: ms[0].id }))
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const currentType = TASK_TYPES.find(t => t.value === form.issue_type) || TASK_TYPES[2]
|
const handleProjectChange = (projectId: number) => {
|
||||||
|
setForm(f => ({ ...f, project_id: projectId, milestone_id: 0 }))
|
||||||
|
api.get<Milestone[]>(`/milestones?project_id=${projectId}`).then(({ data: ms }) => {
|
||||||
|
setMilestones(ms)
|
||||||
|
if (ms.length) setForm((f) => ({ ...f, milestone_id: ms[0].id }))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentType = TASK_TYPES.find(t => t.value === form.task_type) || TASK_TYPES[2]
|
||||||
const subtypes = currentType.subtypes || []
|
const subtypes = currentType.subtypes || []
|
||||||
|
|
||||||
const handleTypeChange = (newType: string) => {
|
const handleTypeChange = (newType: string) => {
|
||||||
setForm(f => ({ ...f, issue_type: newType, issue_subtype: '' }))
|
setForm(f => ({ ...f, task_type: newType, task_subtype: '' }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const submit = async (e: React.FormEvent) => {
|
const submit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
if (!form.milestone_id) {
|
||||||
|
alert('Please select a milestone')
|
||||||
|
return
|
||||||
|
}
|
||||||
const payload: any = { ...form, tags: form.tags || null }
|
const payload: any = { ...form, tags: form.tags || null }
|
||||||
if (!form.issue_subtype) delete payload.issue_subtype
|
if (!form.task_subtype) delete payload.task_subtype
|
||||||
await api.post('/issues', payload)
|
await api.post('/tasks', payload)
|
||||||
navigate('/tasks')
|
navigate('/tasks')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,21 +68,27 @@ export default function CreateTaskPage() {
|
|||||||
<div className="create-task">
|
<div className="create-task">
|
||||||
<h2>Create Task</h2>
|
<h2>Create Task</h2>
|
||||||
<form onSubmit={submit}>
|
<form onSubmit={submit}>
|
||||||
<label>Title <input required value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} /></label>
|
<label>Title <input data-testid="task-title-input" required value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} /></label>
|
||||||
<label>Description <textarea value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} /></label>
|
<label>Description <textarea data-testid="task-description-input" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} /></label>
|
||||||
<label>Projects
|
<label>Projects
|
||||||
<select value={form.project_id} onChange={(e) => setForm({ ...form, project_id: Number(e.target.value) })}>
|
<select data-testid="project-select" value={form.project_id} onChange={(e) => handleProjectChange(Number(e.target.value))}>
|
||||||
{projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
|
{projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<label>Milestone
|
||||||
|
<select data-testid="milestone-select" value={form.milestone_id} onChange={(e) => setForm({ ...form, milestone_id: Number(e.target.value) })}>
|
||||||
|
{milestones.length === 0 && <option value={0}>No milestones available</option>}
|
||||||
|
{milestones.map((m) => <option key={m.id} value={m.id}>{m.title}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label>Type
|
<label>Type
|
||||||
<select value={form.issue_type} onChange={(e) => handleTypeChange(e.target.value)}>
|
<select data-testid="task-type-select" value={form.task_type} onChange={(e) => handleTypeChange(e.target.value)}>
|
||||||
{TASK_TYPES.map((t) => <option key={t.value} value={t.value}>{t.label}</option>)}
|
{TASK_TYPES.map((t) => <option key={t.value} value={t.value}>{t.label}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
{subtypes.length > 0 && (
|
{subtypes.length > 0 && (
|
||||||
<label>Subtype
|
<label>Subtype
|
||||||
<select value={form.issue_subtype} onChange={(e) => setForm({ ...form, issue_subtype: e.target.value })}>
|
<select value={form.task_subtype} onChange={(e) => setForm({ ...form, task_subtype: e.target.value })}>
|
||||||
<option value="">Select subtype</option>
|
<option value="">Select subtype</option>
|
||||||
{subtypes.map((s) => <option key={s} value={s}>{s.replace('_', ' ')}</option>)}
|
{subtypes.map((s) => <option key={s} value={s}>{s.replace('_', ' ')}</option>)}
|
||||||
</select>
|
</select>
|
||||||
@@ -79,7 +103,7 @@ export default function CreateTaskPage() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>Tags <input value={form.tags} onChange={(e) => setForm({ ...form, tags: e.target.value })} placeholder="Comma separated" /></label>
|
<label>Tags <input value={form.tags} onChange={(e) => setForm({ ...form, tags: e.target.value })} placeholder="Comma separated" /></label>
|
||||||
<button type="submit" className="btn-primary">Create</button>
|
<button data-testid="create-task-button" type="submit" className="btn-primary">Create</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
<div className="stats-grid">
|
<div className="stats-grid">
|
||||||
<div className="stat-card total">
|
<div className="stat-card total">
|
||||||
<span className="stat-number">{stats.total_issues}</span>
|
<span className="stat-number">{stats.total_tasks}</span>
|
||||||
<span className="stat-label">Total Tasks</span>
|
<span className="stat-label">Total Tasks</span>
|
||||||
</div>
|
</div>
|
||||||
{Object.entries(stats.by_status || {}).map(([k, v]) => (
|
{Object.entries(stats.by_status || {}).map(([k, v]) => (
|
||||||
@@ -43,7 +43,7 @@ export default function DashboardPage() {
|
|||||||
<div className="bar-row" key={k}>
|
<div className="bar-row" key={k}>
|
||||||
<span className="bar-label">{k}</span>
|
<span className="bar-label">{k}</span>
|
||||||
<div className="bar" style={{
|
<div className="bar" style={{
|
||||||
width: `${Math.max((v / stats.total_issues) * 100, 5)}%`,
|
width: `${Math.max((v / stats.total_tasks) * 100, 5)}%`,
|
||||||
backgroundColor: priorityColors[k] || '#ccc',
|
backgroundColor: priorityColors[k] || '#ccc',
|
||||||
}}>{v}</div>
|
}}>{v}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,13 +58,13 @@ export default function DashboardPage() {
|
|||||||
<tr><th>ID</th><th>Title</th><th>Status</th><th>Priority</th><th>Type</th><th>Subtype</th></tr>
|
<tr><th>ID</th><th>Title</th><th>Status</th><th>Priority</th><th>Type</th><th>Subtype</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{(stats.recent_issues || []).map((i) => (
|
{(stats.recent_tasks || []).map((i) => (
|
||||||
<tr key={i.id}>
|
<tr key={i.id}>
|
||||||
<td>#{i.id}</td>
|
<td>#{i.id}</td>
|
||||||
<td><a href={`/tasks/${i.id}`}>{i.title}</a></td>
|
<td><a href={`/tasks/${i.id}`}>{i.title}</a></td>
|
||||||
<td><span className={`badge status-${i.status}`}>{i.status}</span></td>
|
<td><span className={`badge status-${i.status}`}>{i.status}</span></td>
|
||||||
<td><span className={`badge priority-${i.priority}`}>{i.priority}</span></td>
|
<td><span className={`badge priority-${i.priority}`}>{i.priority}</span></td>
|
||||||
<td>{i.issue_type}</td><td>{i.issue_subtype || "-"}</td>
|
<td>{i.task_type}</td><td>{i.task_subtype || "-"}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ interface ServerRow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface OverviewData {
|
interface OverviewData {
|
||||||
issues: {
|
tasks: {
|
||||||
total_issues: number
|
total_tasks: number
|
||||||
new_issues_24h: number
|
new_tasks_24h: number
|
||||||
processed_issues_24h: number
|
processed_tasks_24h: number
|
||||||
computed_at: string
|
computed_at: string
|
||||||
}
|
}
|
||||||
servers: ServerRow[]
|
servers: ServerRow[]
|
||||||
@@ -109,15 +109,15 @@ export default function MonitorPage() {
|
|||||||
|
|
||||||
<div className="stats-grid">
|
<div className="stats-grid">
|
||||||
<div className="stat-card total">
|
<div className="stat-card total">
|
||||||
<span className="stat-number">{data.issues.total_issues}</span>
|
<span className="stat-number">{data.tasks.total_tasks}</span>
|
||||||
<span className="stat-label">Total Tasks</span>
|
<span className="stat-label">Total Tasks</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-card" style={{ borderLeftColor: 'var(--accent)' }}>
|
<div className="stat-card" style={{ borderLeftColor: 'var(--accent)' }}>
|
||||||
<span className="stat-number">{data.issues.new_issues_24h}</span>
|
<span className="stat-number">{data.tasks.new_tasks_24h}</span>
|
||||||
<span className="stat-label">New (24h)</span>
|
<span className="stat-label">New (24h)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-card" style={{ borderLeftColor: 'var(--success)' }}>
|
<div className="stat-card" style={{ borderLeftColor: 'var(--success)' }}>
|
||||||
<span className="stat-number">{data.issues.processed_issues_24h}</span>
|
<span className="stat-number">{data.tasks.processed_tasks_24h}</span>
|
||||||
<span className="stat-label">Processed (24h)</span>
|
<span className="stat-label">Processed (24h)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export default function NotificationsPage() {
|
|||||||
className={`notification-item ${n.is_read ? 'read' : 'unread'}`}
|
className={`notification-item ${n.is_read ? 'read' : 'unread'}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!n.is_read) markRead(n.id)
|
if (!n.is_read) markRead(n.id)
|
||||||
if (n.issue_id) navigate(`/tasks/${n.issue_id}`)
|
if (n.task_id) navigate(`/tasks/${n.task_id}`)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="notification-dot">{n.is_read ? '' : '●'}</div>
|
<div className="notification-dot">{n.is_read ? '' : '●'}</div>
|
||||||
|
|||||||
@@ -12,21 +12,21 @@ export default function TaskDetailPage() {
|
|||||||
const [newComment, setNewComment] = useState('')
|
const [newComment, setNewComment] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.get<Task>(`/issues/${id}`).then(({ data }) => setTask(data))
|
api.get<Task>(`/tasks/${id}`).then(({ data }) => setTask(data))
|
||||||
api.get<Comment[]>(`/issues/${id}/comments`).then(({ data }) => setComments(data))
|
api.get<Comment[]>(`/tasks/${id}/comments`).then(({ data }) => setComments(data))
|
||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
const addComment = async () => {
|
const addComment = async () => {
|
||||||
if (!newComment.trim() || !task) return
|
if (!newComment.trim() || !task) return
|
||||||
await api.post('/comments', { content: newComment, issue_id: task.id, author_id: 1 })
|
await api.post('/comments', { content: newComment, task_id: task.id, author_id: 1 })
|
||||||
setNewComment('')
|
setNewComment('')
|
||||||
const { data } = await api.get<Comment[]>(`/issues/${id}/comments`)
|
const { data } = await api.get<Comment[]>(`/tasks/${id}/comments`)
|
||||||
setComments(data)
|
setComments(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
const transition = async (newStatus: string) => {
|
const transition = async (newStatus: string) => {
|
||||||
await api.post(`/issues/${id}/transition?new_status=${newStatus}`)
|
await api.post(`/tasks/${id}/transition?new_status=${newStatus}`)
|
||||||
const { data } = await api.get<Task>(`/issues/${id}`)
|
const { data } = await api.get<Task>(`/tasks/${id}`)
|
||||||
setTask(data)
|
setTask(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ export default function TaskDetailPage() {
|
|||||||
<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>
|
||||||
<span className="badge">{task.issue_type}</span>{task.issue_subtype && <span className="badge">{task.issue_subtype}</span>}
|
<span className="badge">{task.task_type}</span>{task.task_subtype && <span className="badge">{task.task_subtype}</span>}
|
||||||
{task.tags && <span className="tags">{task.tags}</span>}
|
{task.tags && <span className="tags">{task.tags}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ export default function TasksPage() {
|
|||||||
|
|
||||||
const fetchTasks = () => {
|
const fetchTasks = () => {
|
||||||
const params = new URLSearchParams({ page: String(page), page_size: '20' })
|
const params = new URLSearchParams({ page: String(page), page_size: '20' })
|
||||||
if (statusFilter) params.set('issue_status', statusFilter)
|
if (statusFilter) params.set('task_status', statusFilter)
|
||||||
api.get<PaginatedResponse<Task>>(`/issues?${params}`).then(({ data }) => {
|
api.get<PaginatedResponse<Task>>(`/tasks?${params}`).then(({ data }) => {
|
||||||
setTasks(data.items)
|
setTasks(data.items)
|
||||||
setTotal(data.total)
|
setTotal(data.total)
|
||||||
setTotalPages(data.total_pages)
|
setTotalPages(data.total_pages)
|
||||||
@@ -58,7 +58,7 @@ export default function TasksPage() {
|
|||||||
<td className="task-title">{t.title}</td>
|
<td className="task-title">{t.title}</td>
|
||||||
<td><span className="badge" style={{ backgroundColor: statusColors[t.status] || '#ccc' }}>{t.status}</span></td>
|
<td><span className="badge" style={{ backgroundColor: statusColors[t.status] || '#ccc' }}>{t.status}</span></td>
|
||||||
<td><span className={`badge priority-${t.priority}`}>{t.priority}</span></td>
|
<td><span className={`badge priority-${t.priority}`}>{t.priority}</span></td>
|
||||||
<td>{t.issue_type}</td><td>{t.issue_subtype || "-"}</td>
|
<td>{t.task_type}</td><td>{t.task_subtype || "-"}</td>
|
||||||
<td>{t.tags || '-'}</td>
|
<td>{t.tags || '-'}</td>
|
||||||
<td>{new Date(t.created_at).toLocaleDateString()}</td>
|
<td>{new Date(t.created_at).toLocaleDateString()}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ export interface Task {
|
|||||||
id: number
|
id: number
|
||||||
title: string
|
title: string
|
||||||
description: string | null
|
description: string | null
|
||||||
issue_type: 'meeting' | 'support' | 'issue' | 'maintenance' | 'research' | 'review' | 'story' | 'test' | 'resolution' | 'task'
|
task_type: 'issue' | 'maintenance' | 'research' | 'review' | 'story' | 'test' | 'resolution' | 'task'
|
||||||
issue_subtype: string | null
|
task_subtype: string | null
|
||||||
status: 'open' | 'in_progress' | 'resolved' | 'closed' | 'blocked'
|
status: 'open' | 'in_progress' | 'resolved' | 'closed' | 'blocked'
|
||||||
priority: 'low' | 'medium' | 'high' | 'critical'
|
priority: 'low' | 'medium' | 'high' | 'critical'
|
||||||
project_id: number
|
project_id: number
|
||||||
@@ -53,7 +53,7 @@ export interface Task {
|
|||||||
export interface Comment {
|
export interface Comment {
|
||||||
id: number
|
id: number
|
||||||
content: string
|
content: string
|
||||||
issue_id: number
|
task_id: number
|
||||||
author_id: number
|
author_id: number
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string | null
|
updated_at: string | null
|
||||||
@@ -76,7 +76,7 @@ export interface Milestone {
|
|||||||
export interface MilestoneProgress {
|
export interface MilestoneProgress {
|
||||||
milestone_id: number
|
milestone_id: number
|
||||||
title: string
|
title: string
|
||||||
total_issues: number
|
total_tasks: number
|
||||||
total: number
|
total: number
|
||||||
completed: number
|
completed: number
|
||||||
progress_pct: number
|
progress_pct: number
|
||||||
@@ -95,13 +95,13 @@ export interface Notification {
|
|||||||
user_id: number
|
user_id: number
|
||||||
message: string
|
message: string
|
||||||
is_read: boolean
|
is_read: boolean
|
||||||
issue_id: number | null
|
task_id: number | null
|
||||||
created_at: string
|
created_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaginatedResponse<T> {
|
export interface PaginatedResponse<T> {
|
||||||
items: T[]
|
items: T[]
|
||||||
total_issues: number
|
total_tasks: number
|
||||||
total: number
|
total: number
|
||||||
page: number
|
page: number
|
||||||
page_size: number
|
page_size: number
|
||||||
@@ -109,12 +109,12 @@ export interface PaginatedResponse<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardStats {
|
export interface DashboardStats {
|
||||||
total_issues: number
|
total_tasks: number
|
||||||
total: number
|
total: number
|
||||||
by_status: Record<string, number>
|
by_status: Record<string, number>
|
||||||
by_priority: Record<string, number>
|
by_priority: Record<string, number>
|
||||||
by_type: Record<string, number>
|
by_type: Record<string, number>
|
||||||
recent_issues: Task[]
|
recent_tasks: Task[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
|
|||||||
Reference in New Issue
Block a user