Compare commits
1 Commits
9880cfc41e
...
01affdb020
| Author | SHA1 | Date | |
|---|---|---|---|
| 01affdb020 |
12
src/App.tsx
12
src/App.tsx
@@ -5,9 +5,9 @@ import Sidebar from '@/components/Sidebar'
|
|||||||
import LoginPage from '@/pages/LoginPage'
|
import LoginPage from '@/pages/LoginPage'
|
||||||
import SetupWizardPage from '@/pages/SetupWizardPage'
|
import SetupWizardPage from '@/pages/SetupWizardPage'
|
||||||
import DashboardPage from '@/pages/DashboardPage'
|
import DashboardPage from '@/pages/DashboardPage'
|
||||||
import IssuesPage from '@/pages/IssuesPage'
|
import TasksPage from '@/pages/TasksPage'
|
||||||
import IssueDetailPage from '@/pages/IssueDetailPage'
|
import TaskDetailPage from '@/pages/TaskDetailPage'
|
||||||
import CreateIssuePage from '@/pages/CreateIssuePage'
|
import CreateTaskPage from '@/pages/CreateTaskPage'
|
||||||
import ProjectsPage from '@/pages/ProjectsPage'
|
import ProjectsPage from '@/pages/ProjectsPage'
|
||||||
import ProjectDetailPage from '@/pages/ProjectDetailPage'
|
import ProjectDetailPage from '@/pages/ProjectDetailPage'
|
||||||
import MilestonesPage from '@/pages/MilestonesPage'
|
import MilestonesPage from '@/pages/MilestonesPage'
|
||||||
@@ -85,9 +85,9 @@ export default function App() {
|
|||||||
<main className="main-content">
|
<main className="main-content">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<DashboardPage />} />
|
<Route path="/" element={<DashboardPage />} />
|
||||||
<Route path="/issues" element={<IssuesPage />} />
|
<Route path="/tasks" element={<TasksPage />} />
|
||||||
<Route path="/issues/new" element={<CreateIssuePage />} />
|
<Route path="/tasks/new" element={<CreateTaskPage />} />
|
||||||
<Route path="/issues/:id" element={<IssueDetailPage />} />
|
<Route path="/tasks/:id" element={<TaskDetailPage />} />
|
||||||
<Route path="/projects" element={<ProjectsPage />} />
|
<Route path="/projects" element={<ProjectsPage />} />
|
||||||
<Route path="/projects/:id" element={<ProjectDetailPage />} />
|
<Route path="/projects/:id" element={<ProjectDetailPage />} />
|
||||||
<Route path="/milestones" element={<MilestonesPage />} />
|
<Route path="/milestones" element={<MilestonesPage />} />
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ thead th { text-align: left; padding: 10px 12px; border-bottom: 2px solid var(--
|
|||||||
tbody td { padding: 10px 12px; border-bottom: 1px solid var(--border); }
|
tbody td { padding: 10px 12px; border-bottom: 1px solid var(--border); }
|
||||||
tr.clickable { cursor: pointer; }
|
tr.clickable { cursor: pointer; }
|
||||||
tr.clickable:hover { background: var(--bg-hover); }
|
tr.clickable:hover { background: var(--bg-hover); }
|
||||||
.issue-title { font-weight: 500; max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.task-title { font-weight: 500; max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
|
||||||
/* Badges */
|
/* Badges */
|
||||||
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: .75rem; font-weight: 600; text-transform: capitalize; color: #fff; background: var(--text-dim); }
|
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: .75rem; font-weight: 600; text-transform: capitalize; color: #fff; background: var(--text-dim); }
|
||||||
@@ -92,10 +92,10 @@ tr.clickable:hover { background: var(--bg-hover); }
|
|||||||
.pagination button { padding: 6px 14px; border: 1px solid var(--border); background: var(--bg-card); color: var(--text); border-radius: 6px; cursor: pointer; }
|
.pagination button { padding: 6px 14px; border: 1px solid var(--border); background: var(--bg-card); color: var(--text); border-radius: 6px; cursor: pointer; }
|
||||||
.pagination button:disabled { opacity: .4; cursor: default; }
|
.pagination button:disabled { opacity: .4; cursor: default; }
|
||||||
|
|
||||||
/* Issue detail */
|
/* Task detail */
|
||||||
.issue-header { margin-bottom: 20px; }
|
.task-header { margin-bottom: 20px; }
|
||||||
.issue-header h2 { margin-bottom: 8px; }
|
.task-header h2 { margin-bottom: 8px; }
|
||||||
.issue-meta { display: flex; gap: 8px; flex-wrap: wrap; }
|
.task-meta { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||||
.tags { color: var(--accent); font-size: .85rem; }
|
.tags { color: var(--accent); font-size: .85rem; }
|
||||||
.section { margin: 20px 0; }
|
.section { margin: 20px 0; }
|
||||||
.section h3 { margin-bottom: 8px; color: var(--text-dim); font-size: .9rem; text-transform: uppercase; }
|
.section h3 { margin-bottom: 8px; color: var(--text-dim); font-size: .9rem; text-transform: uppercase; }
|
||||||
@@ -113,11 +113,11 @@ dd { font-size: .9rem; }
|
|||||||
.comment-form textarea { width: 100%; min-height: 80px; padding: 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text); resize: vertical; margin-bottom: 8px; }
|
.comment-form textarea { width: 100%; min-height: 80px; padding: 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text); resize: vertical; margin-bottom: 8px; }
|
||||||
.comment-form button { padding: 8px 16px; background: var(--accent); color: #fff; border: none; border-radius: 6px; cursor: pointer; }
|
.comment-form button { padding: 8px 16px; background: var(--accent); color: #fff; border: none; border-radius: 6px; cursor: pointer; }
|
||||||
|
|
||||||
/* Create Issue form */
|
/* Create Task form */
|
||||||
.create-issue form { max-width: 600px; display: flex; flex-direction: column; gap: 14px; }
|
.create-task form { max-width: 600px; display: flex; flex-direction: column; gap: 14px; }
|
||||||
.create-issue label { display: flex; flex-direction: column; gap: 4px; font-size: .85rem; color: var(--text-dim); }
|
.create-task label { display: flex; flex-direction: column; gap: 4px; font-size: .85rem; color: var(--text-dim); }
|
||||||
.create-issue input, .create-issue textarea, .create-issue select { padding: 8px 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text); font-size: .95rem; }
|
.create-task input, .create-task textarea, .create-task select { padding: 8px 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text); font-size: .95rem; }
|
||||||
.create-issue textarea { min-height: 100px; resize: vertical; }
|
.create-task textarea { min-height: 100px; resize: vertical; }
|
||||||
|
|
||||||
/* Project grid */
|
/* Project grid */
|
||||||
.project-grid, .milestone-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; margin-top: 16px; }
|
.project-grid, .milestone-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; margin-top: 16px; }
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'
|
|||||||
import api from '@/services/api'
|
import api from '@/services/api'
|
||||||
import type { Project } from '@/types'
|
import type { Project } from '@/types'
|
||||||
|
|
||||||
const ISSUE_TYPES = [
|
const TASK_TYPES = [
|
||||||
{ value: 'story', label: 'Story', subtypes: ['feature', 'improvement', 'refactor'] },
|
{ value: 'story', label: 'Story', subtypes: ['feature', 'improvement', 'refactor'] },
|
||||||
{ value: 'issue', label: 'Issue', subtypes: ['infrastructure', 'performance', 'regression', 'security', 'user_experience', 'defect'] },
|
{ value: 'issue', label: 'Issue', subtypes: ['infrastructure', 'performance', 'regression', 'security', 'user_experience', 'defect'] },
|
||||||
{ value: 'task', label: 'Task', subtypes: ['defect'] },
|
{ value: 'task', label: 'Task', subtypes: ['defect'] },
|
||||||
@@ -16,11 +16,11 @@ const ISSUE_TYPES = [
|
|||||||
{ value: 'resolution', label: 'Resolution', subtypes: [] },
|
{ value: 'resolution', label: 'Resolution', subtypes: [] },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function CreateIssuePage() {
|
export default function CreateTaskPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [projects, setProjects] = useState<Project[]>([])
|
const [projects, setProjects] = useState<Project[]>([])
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
title: '', description: '', project_id: 0, issue_type: 'issue',
|
title: '', description: '', project_id: 0, issue_type: 'task',
|
||||||
issue_subtype: '', priority: 'medium', tags: '', reporter_id: 1,
|
issue_subtype: '', priority: 'medium', tags: '', reporter_id: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ export default function CreateIssuePage() {
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const currentType = ISSUE_TYPES.find(t => t.value === form.issue_type) || ISSUE_TYPES[1]
|
const currentType = TASK_TYPES.find(t => t.value === form.issue_type) || TASK_TYPES[2]
|
||||||
const subtypes = currentType.subtypes || []
|
const subtypes = currentType.subtypes || []
|
||||||
|
|
||||||
const handleTypeChange = (newType: string) => {
|
const handleTypeChange = (newType: string) => {
|
||||||
@@ -43,12 +43,12 @@ export default function CreateIssuePage() {
|
|||||||
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.issue_subtype) delete payload.issue_subtype
|
||||||
await api.post('/issues', payload)
|
await api.post('/issues', payload)
|
||||||
navigate('/issues')
|
navigate('/tasks')
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="create-issue">
|
<div className="create-task">
|
||||||
<h2>Create Issue</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 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 value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} /></label>
|
||||||
@@ -59,7 +59,7 @@ export default function CreateIssuePage() {
|
|||||||
</label>
|
</label>
|
||||||
<label>Type
|
<label>Type
|
||||||
<select value={form.issue_type} onChange={(e) => handleTypeChange(e.target.value)}>
|
<select value={form.issue_type} onChange={(e) => handleTypeChange(e.target.value)}>
|
||||||
{ISSUE_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 && (
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import api from '@/services/api'
|
import api from '@/services/api'
|
||||||
import type { DashboardStats } from '@/types'
|
import type { DashboardStats, Task } from '@/types'
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const [stats, setStats] = useState<DashboardStats | null>(null)
|
const [stats, setStats] = useState<DashboardStats | null>(null)
|
||||||
@@ -26,7 +26,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_issues}</span>
|
||||||
<span className="stat-label">Total Issues</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]) => (
|
||||||
<div className="stat-card" key={k} style={{ borderLeftColor: statusColors[k] || '#ccc' }}>
|
<div className="stat-card" key={k} style={{ borderLeftColor: statusColors[k] || '#ccc' }}>
|
||||||
@@ -52,7 +52,7 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h3>Recent Issues</h3>
|
<h3>Recent Tasks</h3>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<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>
|
||||||
@@ -61,7 +61,7 @@ export default function DashboardPage() {
|
|||||||
{(stats.recent_issues || []).map((i) => (
|
{(stats.recent_issues || []).map((i) => (
|
||||||
<tr key={i.id}>
|
<tr key={i.id}>
|
||||||
<td>#{i.id}</td>
|
<td>#{i.id}</td>
|
||||||
<td><a href={`/issues/${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.issue_type}</td><td>{i.issue_subtype || "-"}</td>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useParams, useNavigate } from 'react-router-dom'
|
import { useParams, useNavigate } from 'react-router-dom'
|
||||||
import api from '@/services/api'
|
import api from '@/services/api'
|
||||||
import type { Milestone, MilestoneProgress, Issue } from '@/types'
|
import type { Milestone, MilestoneProgress, Task } from '@/types'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
interface Task {
|
interface MilestoneTask {
|
||||||
id: number
|
id: number
|
||||||
title: string
|
title: string
|
||||||
description?: string
|
description?: string
|
||||||
@@ -24,9 +24,9 @@ export default function MilestoneDetailPage() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [milestone, setMilestone] = useState<Milestone | null>(null)
|
const [milestone, setMilestone] = useState<Milestone | null>(null)
|
||||||
const [progress, setProgress] = useState<MilestoneProgress | null>(null)
|
const [progress, setProgress] = useState<MilestoneProgress | null>(null)
|
||||||
const [tasks, setTasks] = useState<Task[]>([])
|
const [tasks, setTasks] = useState<MilestoneTask[]>([])
|
||||||
const [supports, setSupports] = useState<Issue[]>([])
|
const [supports, setSupports] = useState<Task[]>([])
|
||||||
const [meetings, setMeetings] = useState<Issue[]>([])
|
const [meetings, setMeetings] = useState<Task[]>([])
|
||||||
const [activeTab, setActiveTab] = useState<'tasks' | 'supports' | 'meetings'>('tasks')
|
const [activeTab, setActiveTab] = useState<'tasks' | 'supports' | 'meetings'>('tasks')
|
||||||
const [showCreateTask, setShowCreateTask] = useState(false)
|
const [showCreateTask, setShowCreateTask] = useState(false)
|
||||||
const [showCreateSupport, setShowCreateSupport] = useState(false)
|
const [showCreateSupport, setShowCreateSupport] = useState(false)
|
||||||
@@ -52,9 +52,9 @@ export default function MilestoneDetailPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!projectCode || !id) return
|
if (!projectCode || !id) return
|
||||||
api.get<Task[]>(`/tasks/${projectCode}/${id}`).then(({ data }) => setTasks(data)).catch(() => {})
|
api.get<MilestoneTask[]>(`/tasks/${projectCode}/${id}`).then(({ data }) => setTasks(data)).catch(() => {})
|
||||||
api.get<Issue[]>(`/supports/${projectCode}/${id}`).then(({ data }) => setSupports(data)).catch(() => {})
|
api.get<Task[]>(`/supports/${projectCode}/${id}`).then(({ data }) => setSupports(data)).catch(() => {})
|
||||||
api.get<Issue[]>(`/meetings/${projectCode}/${id}`).then(({ data }) => setMeetings(data)).catch(() => {})
|
api.get<Task[]>(`/meetings/${projectCode}/${id}`).then(({ data }) => setMeetings(data)).catch(() => {})
|
||||||
}, [projectCode, id])
|
}, [projectCode, id])
|
||||||
|
|
||||||
const createItem = async (type: 'tasks' | 'supports' | 'meetings') => {
|
const createItem = async (type: 'tasks' | 'supports' | 'meetings') => {
|
||||||
@@ -74,19 +74,19 @@ export default function MilestoneDetailPage() {
|
|||||||
setShowCreateSupport(false)
|
setShowCreateSupport(false)
|
||||||
setShowCreateMeeting(false)
|
setShowCreateMeeting(false)
|
||||||
// Refresh
|
// Refresh
|
||||||
api.get<Task[]>(`/tasks/${projectCode}/${id}`).then(({ data }) => setTasks(data))
|
api.get<MilestoneTask[]>(`/tasks/${projectCode}/${id}`).then(({ data }) => setTasks(data))
|
||||||
api.get<Issue[]>(`/supports/${projectCode}/${id}`).then(({ data }) => setSupports(data))
|
api.get<Task[]>(`/supports/${projectCode}/${id}`).then(({ data }) => setSupports(data))
|
||||||
api.get<Issue[]>(`/meetings/${projectCode}/${id}`).then(({ data }) => setMeetings(data))
|
api.get<Task[]>(`/meetings/${projectCode}/${id}`).then(({ data }) => setMeetings(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
const isProgressing = milestone?.status === 'progressing'
|
const isProgressing = milestone?.status === 'progressing'
|
||||||
|
|
||||||
if (!milestone) return <div className="loading">Loading...</div>
|
if (!milestone) return <div className="loading">Loading...</div>
|
||||||
|
|
||||||
const renderTaskRow = (t: Task) => (
|
const renderTaskRow = (t: MilestoneTask) => (
|
||||||
<tr key={t.id} className="clickable" onClick={() => navigate(`/milestones/${id}`)}>
|
<tr key={t.id} className="clickable" onClick={() => navigate(`/milestones/${id}`)}>
|
||||||
<td>{t.task_code || t.id}</td>
|
<td>{t.task_code || t.id}</td>
|
||||||
<td className="issue-title">{t.title}</td>
|
<td className="task-title">{t.title}</td>
|
||||||
<td><span className={`badge status-${t.task_status || t.status}`}>{t.task_status || t.status}</span></td>
|
<td><span className={`badge status-${t.task_status || t.status}`}>{t.task_status || t.status}</span></td>
|
||||||
<td>{t.estimated_effort || '-'}</td>
|
<td>{t.estimated_effort || '-'}</td>
|
||||||
<td>{t.estimated_working_time || '-'}</td>
|
<td>{t.estimated_working_time || '-'}</td>
|
||||||
@@ -97,9 +97,9 @@ export default function MilestoneDetailPage() {
|
|||||||
<div className="milestone-detail">
|
<div className="milestone-detail">
|
||||||
<button className="btn-back" onClick={() => navigate('/milestones')}>← Back to Milestones</button>
|
<button className="btn-back" onClick={() => navigate('/milestones')}>← Back to Milestones</button>
|
||||||
|
|
||||||
<div className="issue-header">
|
<div className="task-header">
|
||||||
<h2>🏁 {milestone.title}</h2>
|
<h2>🏁 {milestone.title}</h2>
|
||||||
<div className="issue-meta">
|
<div className="task-meta">
|
||||||
<span className={`badge status-${milestone.status === 'progressing' ? 'in_progress' : milestone.status}`}>{milestone.status}</span>
|
<span className={`badge status-${milestone.status === 'progressing' ? 'in_progress' : milestone.status}`}>{milestone.status}</span>
|
||||||
{milestone.due_date && <span className="text-dim">Due {dayjs(milestone.due_date).format('YYYY-MM-DD')}</span>}
|
{milestone.due_date && <span className="text-dim">Due {dayjs(milestone.due_date).format('YYYY-MM-DD')}</span>}
|
||||||
{milestone.planned_release_date && <span className="text-dim">Planned Release: {dayjs(milestone.planned_release_date).format('YYYY-MM-DD')}</span>}
|
{milestone.planned_release_date && <span className="text-dim">Planned Release: {dayjs(milestone.planned_release_date).format('YYYY-MM-DD')}</span>}
|
||||||
@@ -207,7 +207,7 @@ export default function MilestoneDetailPage() {
|
|||||||
{supports.map((i) => (
|
{supports.map((i) => (
|
||||||
<tr key={i.id} className="clickable" onClick={() => navigate(`/supports/${projectCode}/${id}/${i.id}`)}>
|
<tr key={i.id} className="clickable" onClick={() => navigate(`/supports/${projectCode}/${id}/${i.id}`)}>
|
||||||
<td>{i.id}</td>
|
<td>{i.id}</td>
|
||||||
<td className="issue-title">{i.title}</td>
|
<td className="task-title">{i.title}</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>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -224,7 +224,7 @@ export default function MilestoneDetailPage() {
|
|||||||
{meetings.map((i) => (
|
{meetings.map((i) => (
|
||||||
<tr key={i.id} className="clickable" onClick={() => navigate(`/meetings/${projectCode}/${id}/${i.id}`)}>
|
<tr key={i.id} className="clickable" onClick={() => navigate(`/meetings/${projectCode}/${id}/${i.id}`)}>
|
||||||
<td>{i.id}</td>
|
<td>{i.id}</td>
|
||||||
<td className="issue-title">{i.title}</td>
|
<td className="task-title">{i.title}</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>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ 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.issues.total_issues}</span>
|
||||||
<span className="stat-label">Total Issues</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.issues.new_issues_24h}</span>
|
||||||
|
|||||||
@@ -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(`/issues/${n.issue_id}`)
|
if (n.issue_id) navigate(`/tasks/${n.issue_id}`)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="notification-dot">{n.is_read ? '' : '●'}</div>
|
<div className="notification-dot">{n.is_read ? '' : '●'}</div>
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export default function ProjectDetailPage() {
|
|||||||
<div className="project-detail">
|
<div className="project-detail">
|
||||||
<button className="btn-back" onClick={() => navigate('/projects')}>← Back to projects</button>
|
<button className="btn-back" onClick={() => navigate('/projects')}>← Back to projects</button>
|
||||||
|
|
||||||
<div className="issue-header">
|
<div className="task-header">
|
||||||
{editing ? (
|
{editing ? (
|
||||||
<form className="inline-form" onSubmit={updateProject}>
|
<form className="inline-form" onSubmit={updateProject}>
|
||||||
<div style={{ fontWeight: 600 }}>{project.name}</div>
|
<div style={{ fontWeight: 600 }}>{project.name}</div>
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useParams, useNavigate } from 'react-router-dom'
|
import { useParams, useNavigate } from 'react-router-dom'
|
||||||
import api from '@/services/api'
|
import api from '@/services/api'
|
||||||
import type { Issue, Comment } from '@/types'
|
import type { Task, Comment } from '@/types'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
export default function IssueDetailPage() {
|
export default function TaskDetailPage() {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [issue, setIssue] = useState<Issue | null>(null)
|
const [task, setTask] = useState<Task | null>(null)
|
||||||
const [comments, setComments] = useState<Comment[]>([])
|
const [comments, setComments] = useState<Comment[]>([])
|
||||||
const [newComment, setNewComment] = useState('')
|
const [newComment, setNewComment] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.get<Issue>(`/issues/${id}`).then(({ data }) => setIssue(data))
|
api.get<Task>(`/issues/${id}`).then(({ data }) => setTask(data))
|
||||||
api.get<Comment[]>(`/issues/${id}/comments`).then(({ data }) => setComments(data))
|
api.get<Comment[]>(`/issues/${id}/comments`).then(({ data }) => setComments(data))
|
||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
const addComment = async () => {
|
const addComment = async () => {
|
||||||
if (!newComment.trim() || !issue) return
|
if (!newComment.trim() || !task) return
|
||||||
await api.post('/comments', { content: newComment, issue_id: issue.id, author_id: 1 })
|
await api.post('/comments', { content: newComment, issue_id: task.id, author_id: 1 })
|
||||||
setNewComment('')
|
setNewComment('')
|
||||||
const { data } = await api.get<Comment[]>(`/issues/${id}/comments`)
|
const { data } = await api.get<Comment[]>(`/issues/${id}/comments`)
|
||||||
setComments(data)
|
setComments(data)
|
||||||
@@ -26,11 +26,11 @@ export default function IssueDetailPage() {
|
|||||||
|
|
||||||
const transition = async (newStatus: string) => {
|
const transition = async (newStatus: string) => {
|
||||||
await api.post(`/issues/${id}/transition?new_status=${newStatus}`)
|
await api.post(`/issues/${id}/transition?new_status=${newStatus}`)
|
||||||
const { data } = await api.get<Issue>(`/issues/${id}`)
|
const { data } = await api.get<Task>(`/issues/${id}`)
|
||||||
setIssue(data)
|
setTask(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!issue) return <div className="loading">Loading...</div>
|
if (!task) return <div className="loading">Loading...</div>
|
||||||
|
|
||||||
const statusActions: Record<string, string[]> = {
|
const statusActions: Record<string, string[]> = {
|
||||||
open: ['in_progress', 'blocked'],
|
open: ['in_progress', 'blocked'],
|
||||||
@@ -41,38 +41,38 @@ export default function IssueDetailPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="issue-detail">
|
<div className="task-detail">
|
||||||
<button className="btn-back" onClick={() => navigate('/issues')}>← Back</button>
|
<button className="btn-back" onClick={() => navigate('/tasks')}>← Back</button>
|
||||||
|
|
||||||
<div className="issue-header">
|
<div className="task-header">
|
||||||
<h2>#{issue.id} {issue.title}</h2>
|
<h2>#{task.id} {task.title}</h2>
|
||||||
<div className="issue-meta">
|
<div className="task-meta">
|
||||||
<span className={`badge status-${issue.status}`}>{issue.status}</span>
|
<span className={`badge status-${task.status}`}>{task.status}</span>
|
||||||
<span className={`badge priority-${issue.priority}`}>{issue.priority}</span>
|
<span className={`badge priority-${task.priority}`}>{task.priority}</span>
|
||||||
<span className="badge">{issue.issue_type}</span>{issue.issue_subtype && <span className="badge">{issue.issue_subtype}</span>}
|
<span className="badge">{task.issue_type}</span>{task.issue_subtype && <span className="badge">{task.issue_subtype}</span>}
|
||||||
{issue.tags && <span className="tags">{issue.tags}</span>}
|
{task.tags && <span className="tags">{task.tags}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="issue-body">
|
<div className="task-body">
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h3>Description</h3>
|
<h3>Description</h3>
|
||||||
<p>{issue.description || 'No description'}</p>
|
<p>{task.description || 'No description'}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h3>Details</h3>
|
<h3>Details</h3>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Created</dt><dd>{dayjs(issue.created_at).format('YYYY-MM-DD HH:mm')}</dd>
|
<dt>Created</dt><dd>{dayjs(task.created_at).format('YYYY-MM-DD HH:mm')}</dd>
|
||||||
{issue.due_date && <><dt>Due date</dt><dd>{dayjs(issue.due_date).format('YYYY-MM-DD')}</dd></>}
|
{task.due_date && <><dt>Due date</dt><dd>{dayjs(task.due_date).format('YYYY-MM-DD')}</dd></>}
|
||||||
{issue.updated_at && <><dt>Updated</dt><dd>{dayjs(issue.updated_at).format('YYYY-MM-DD HH:mm')}</dd></>}
|
{task.updated_at && <><dt>Updated</dt><dd>{dayjs(task.updated_at).format('YYYY-MM-DD HH:mm')}</dd></>}
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h3>Status changes</h3>
|
<h3>Status changes</h3>
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
{(statusActions[issue.status] || []).map((s) => (
|
{(statusActions[task.status] || []).map((s) => (
|
||||||
<button key={s} className="btn-transition" onClick={() => transition(s)}>{s}</button>
|
<button key={s} className="btn-transition" onClick={() => transition(s)}>{s}</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
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 { Issue, PaginatedResponse } from '@/types'
|
import type { Task, PaginatedResponse } from '@/types'
|
||||||
|
|
||||||
export default function IssuesPage() {
|
export default function TasksPage() {
|
||||||
const [issues, setIssues] = useState<Issue[]>([])
|
const [tasks, setTasks] = useState<Task[]>([])
|
||||||
const [total, setTotal] = useState(0)
|
const [total, setTotal] = useState(0)
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [totalPages, setTotalPages] = useState(1)
|
const [totalPages, setTotalPages] = useState(1)
|
||||||
@@ -12,17 +12,17 @@ export default function IssuesPage() {
|
|||||||
const [priorityFilter, setPriorityFilter] = useState('')
|
const [priorityFilter, setPriorityFilter] = useState('')
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const fetchIssues = () => {
|
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('issue_status', statusFilter)
|
||||||
api.get<PaginatedResponse<Issue>>(`/issues?${params}`).then(({ data }) => {
|
api.get<PaginatedResponse<Task>>(`/issues?${params}`).then(({ data }) => {
|
||||||
setIssues(data.items)
|
setTasks(data.items)
|
||||||
setTotal(data.total)
|
setTotal(data.total)
|
||||||
setTotalPages(data.total_pages)
|
setTotalPages(data.total_pages)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => { fetchIssues() }, [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',
|
open: '#3b82f6', in_progress: '#f59e0b', resolved: '#10b981',
|
||||||
@@ -30,10 +30,10 @@ export default function IssuesPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="issues-page">
|
<div className="tasks-page">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<h2>📋 Issues ({total})</h2>
|
<h2>📋 Tasks ({total})</h2>
|
||||||
<button className="btn-primary" onClick={() => navigate('/issues/new')}>+ Create Issue</button>
|
<button className="btn-primary" onClick={() => navigate('/tasks/new')}>+ Create Task</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="filters">
|
<div className="filters">
|
||||||
@@ -47,20 +47,20 @@ export default function IssuesPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table className="issues-table">
|
<table className="tasks-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>#</th><th>Title</th><th>Status</th><th>Priority</th><th>Type</th><th>Subtype</th><th>Tags</th><th>Created</th></tr>
|
<tr><th>#</th><th>Title</th><th>Status</th><th>Priority</th><th>Type</th><th>Subtype</th><th>Tags</th><th>Created</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{issues.map((i) => (
|
{tasks.map((t) => (
|
||||||
<tr key={i.id} onClick={() => navigate(`/issues/${i.id}`)} className="clickable">
|
<tr key={t.id} onClick={() => navigate(`/tasks/${t.id}`)} className="clickable">
|
||||||
<td>{i.id}</td>
|
<td>{t.id}</td>
|
||||||
<td className="issue-title">{i.title}</td>
|
<td className="task-title">{t.title}</td>
|
||||||
<td><span className="badge" style={{ backgroundColor: statusColors[i.status] || '#ccc' }}>{i.status}</span></td>
|
<td><span className="badge" style={{ backgroundColor: statusColors[t.status] || '#ccc' }}>{t.status}</span></td>
|
||||||
<td><span className={`badge priority-${i.priority}`}>{i.priority}</span></td>
|
<td><span className={`badge priority-${t.priority}`}>{t.priority}</span></td>
|
||||||
<td>{i.issue_type}</td><td>{i.issue_subtype || "-"}</td>
|
<td>{t.issue_type}</td><td>{t.issue_subtype || "-"}</td>
|
||||||
<td>{i.tags || '-'}</td>
|
<td>{t.tags || '-'}</td>
|
||||||
<td>{new Date(i.created_at).toLocaleDateString()}</td>
|
<td>{new Date(t.created_at).toLocaleDateString()}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -29,7 +29,7 @@ export interface ProjectMember {
|
|||||||
role: string
|
role: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Issue {
|
export interface Task {
|
||||||
id: number
|
id: number
|
||||||
title: string
|
title: string
|
||||||
description: string | null
|
description: string | null
|
||||||
@@ -114,7 +114,7 @@ export interface DashboardStats {
|
|||||||
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: Issue[]
|
recent_issues: Task[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
|
|||||||
Reference in New Issue
Block a user