Loading...
+ const selectableProjects = allProjects.filter((p) => p.id !== project.id && p.project_code)
+
return (
@@ -40,26 +99,49 @@ export default function ProjectDetailPage() {
{editing ? (
) : (
<>
-
๐ {project.name}
+
๐ {project.name} {project.project_code && {project.project_code}}
{project.description || 'No description'}
+
Owner: {project.owner_name || project.owner || "Unknown"}
+
>
)}
-
Members ({members.length})
+
Members ({members.length})
{members.length > 0 ? (
{members.map((m) => (
- {`User #${m.user_id} (${m.role})`}
+
+ {`User #${m.user_id} (${m.role})`}
+
+
))}
) : (
@@ -68,7 +150,7 @@ export default function ProjectDetailPage() {
-
Milestones ({milestones.length})
+
Milestones ({milestones.length})
{milestones.map((ms) => (
navigate(`/milestones/${ms.id}`)}>
{ms.status}
@@ -79,27 +161,36 @@ export default function ProjectDetailPage() {
{milestones.length === 0 &&
No milestones
}
-
-
-
Recent Issues
-
+ {showAddMember && (
+
setShowAddMember(false)}>
+
e.stopPropagation()}>
+
Add Member
+
+
+
+
+
+
+
-
-
- | # | Title | Status | Priority |
-
-
- {issues.map((i) => (
- navigate(`/issues/${i.id}`)}>
- | {i.id} |
- {i.title} |
- {i.status} |
- {i.priority} |
-
- ))}
-
-
-
+ )}
+
+ {showAddMilestone && (
+
setShowAddMilestone(false)}>
+
e.stopPropagation()}>
+
New Milestone
+
setNewMilestoneTitle(e.target.value)} placeholder="Milestone title" />
+
+
+
+
+
+
+ )}
)
}
diff --git a/src/pages/ProjectsPage.tsx b/src/pages/ProjectsPage.tsx
index 7374b66..14212f5 100644
--- a/src/pages/ProjectsPage.tsx
+++ b/src/pages/ProjectsPage.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from 'react'
+import { useState, useEffect, useMemo } from 'react'
import { useNavigate } from 'react-router-dom'
import api from '@/services/api'
import type { Project } from '@/types'
@@ -6,8 +6,9 @@ import dayjs from 'dayjs'
export default function ProjectsPage() {
const [projects, setProjects] = useState
([])
+ const [users, setUsers] = useState([])
const [showCreate, setShowCreate] = useState(false)
- const [form, setForm] = useState({ name: '', description: '', owner_id: 1 })
+ const [form, setForm] = useState({ name: '', description: '', owner_id: 1, repo: '', sub_projects: [] as string[], related_projects: [] as string[] })
const navigate = useNavigate()
const fetchProjects = () => {
@@ -15,11 +16,21 @@ export default function ProjectsPage() {
}
useEffect(() => { fetchProjects() }, [])
+ useEffect(() => {
+ api.get('/users').then(({ data }) => setUsers(data)).catch(console.error)
+ }, [])
+
+ const projectOptions = useMemo(() => projects.filter(p => p.project_code), [projects])
+
+ const handleMulti = (e: React.ChangeEvent, field: 'sub_projects' | 'related_projects') => {
+ const values = Array.from(e.target.selectedOptions).map((o) => o.value)
+ setForm({ ...form, [field]: values })
+ }
const createProject = async (e: React.FormEvent) => {
e.preventDefault()
await api.post('/projects', form)
- setForm({ name: '', description: '', owner_id: 1 })
+ setForm({ name: '', description: '', owner_id: 1, repo: '', sub_projects: [], related_projects: [] })
setShowCreate(false)
fetchProjects()
}
@@ -29,7 +40,7 @@ export default function ProjectsPage() {
๐ Projects ({projects.length})
@@ -39,10 +50,33 @@ export default function ProjectsPage() {
required placeholder="Project name" value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
/>
+
setForm({ ...form, description: e.target.value })}
/>
+ setForm({ ...form, repo: e.target.value })}
+ />
+
+
+
+
)}
@@ -50,10 +84,11 @@ export default function ProjectsPage() {
{projects.map((p) => (
navigate(`/projects/${p.id}`)}>
-
{p.name}
+
{p.name}
{p.project_code &&
{p.project_code}}
{p.description || 'No description'}
- Created {dayjs(p.created_at).format('YYYY-MM-DD')}
+ ๐ค {p.owner_name || 'Unknown'}
+ ยท Created {dayjs(p.created_at).format('YYYY-MM-DD')}
))}
diff --git a/src/pages/RoleEditorPage.tsx b/src/pages/RoleEditorPage.tsx
new file mode 100644
index 0000000..ba483f8
--- /dev/null
+++ b/src/pages/RoleEditorPage.tsx
@@ -0,0 +1,184 @@
+import { useState, useEffect } from 'react'
+import api from '@/services/api'
+
+interface Permission {
+ id: number
+ name: string
+ description: string
+ category: string
+}
+
+interface Role {
+ id: number
+ name: string
+ description: string
+ is_global: boolean
+ permission_ids: number[]
+}
+
+export default function RoleEditorPage() {
+ const [roles, setRoles] = useState
([])
+ const [permissions, setPermissions] = useState([])
+ const [selectedRole, setSelectedRole] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [saving, setSaving] = useState(false)
+ const [message, setMessage] = useState('')
+
+ useEffect(() => {
+ fetchData()
+ }, [])
+
+ const fetchData = async () => {
+ try {
+ const [rolesRes, permsRes] = await Promise.all([
+ api.get('/roles'),
+ api.get('/roles/permissions')
+ ])
+ setRoles(rolesRes.data)
+ setPermissions(permsRes.data)
+ } catch (err) {
+ console.error('Failed to fetch data:', err)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handlePermissionToggle = (permId: number) => {
+ if (!selectedRole) return
+ const newPermIds = selectedRole.permission_ids.includes(permId)
+ ? selectedRole.permission_ids.filter(id => id !== permId)
+ : [...selectedRole.permission_ids, permId]
+ setSelectedRole({ ...selectedRole, permission_ids: newPermIds })
+ }
+
+ const handleSave = async () => {
+ if (!selectedRole) return
+ setSaving(true)
+ setMessage('')
+ try {
+ await api.post(`/roles/${selectedRole.id}/permissions`, {
+ permission_ids: selectedRole.permission_ids
+ })
+ setMessage('Saved successfully!')
+ fetchData()
+ } catch (err: any) {
+ setMessage(err.response?.data?.detail || 'Failed to save')
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const groupedPermissions = permissions.reduce((acc, p) => {
+ if (!acc[p.category]) acc[p.category] = []
+ acc[p.category].push(p)
+ return acc
+ }, {} as Record)
+
+ if (loading) return Loading...
+
+ return (
+
+
๐ Role Editor
+
+ Configure permissions for each role. Only admins can edit roles.
+
+
+ {message && (
+
+ {message}
+
+ )}
+
+
+ {/* Role List */}
+
+
Roles
+
+ {roles.map(role => (
+
setSelectedRole({ ...role })}
+ style={{
+ padding: '12px',
+ border: selectedRole?.id === role.id ? '2px solid #007bff' : '1px solid #ddd',
+ borderRadius: '6px',
+ cursor: 'pointer',
+ backgroundColor: selectedRole?.id === role.id ? '#f0f8ff' : 'white'
+ }}
+ >
+
{role.name}
+ {role.is_global &&
๐}
+
{role.description}
+
+ {role.permission_ids.length} permissions
+
+
+ ))}
+
+
+
+ {/* Permission Editor */}
+
+ {selectedRole ? (
+ <>
+
Permissions for: {selectedRole.name}
+
+ {Object.entries(groupedPermissions).map(([category, perms]) => (
+
+
+ {category}
+
+
+ {perms.map(perm => (
+
+ ))}
+
+
+ ))}
+
+
+ >
+ ) : (
+
+ Select a role to edit its permissions
+
+ )}
+
+
+
+ )
+}
diff --git a/src/types/index.ts b/src/types/index.ts
index 5cfb782..5a787eb 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -11,8 +11,14 @@ export interface User {
export interface Project {
id: number
name: string
+ owner: string
description: string | null
owner_id: number
+ owner_name: string | null
+ project_code: string | null
+ repo: string | null
+ sub_projects: string[] | null
+ related_projects: string[] | null
created_at: string
}
@@ -27,7 +33,8 @@ export interface Issue {
id: number
title: string
description: string | null
- issue_type: 'task' | 'story' | 'test' | 'resolution'
+ issue_type: 'meeting' | 'support' | 'issue' | 'maintenance' | 'research' | 'review' | 'story' | 'test' | 'resolution' | 'task'
+ issue_subtype: string | null
status: 'open' | 'in_progress' | 'resolved' | 'closed' | 'blocked'
priority: 'low' | 'medium' | 'high' | 'critical'
project_id: number