Refactor RequirementDetailPage to fetch requirement data from API and remove mock data
This commit is contained in:
@@ -1,176 +1,63 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useAuth } from '@/hooks'
|
import { useAuth } from '@/hooks'
|
||||||
import { useParams, Link } from 'react-router-dom'
|
import { useParams, Link } from 'react-router-dom'
|
||||||
|
import { requirementService } from '@/services'
|
||||||
// Types for requirement details
|
import type { Requirement } from '@/services/requirementService'
|
||||||
interface RequirementDetail {
|
|
||||||
id: string
|
|
||||||
tag: string
|
|
||||||
title: string
|
|
||||||
priority: number
|
|
||||||
group: string
|
|
||||||
description: string
|
|
||||||
authors: string[]
|
|
||||||
subRequirements: SubRequirement[]
|
|
||||||
coRequirements: CoRequirement[]
|
|
||||||
acceptanceCriteria: AcceptanceCriterion[]
|
|
||||||
sharedComments: Comment[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SubRequirement {
|
|
||||||
id: string
|
|
||||||
tag: string
|
|
||||||
title: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CoRequirement {
|
|
||||||
id: string
|
|
||||||
tag: string
|
|
||||||
title: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AcceptanceCriterion {
|
|
||||||
id: string
|
|
||||||
description: string
|
|
||||||
validated: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Comment {
|
|
||||||
id: string
|
|
||||||
author: string
|
|
||||||
date: string
|
|
||||||
content: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tab types
|
// Tab types
|
||||||
type TabType = 'description' | 'sub-requirements' | 'co-requirements' | 'acceptance-criteria' | 'shared-comments' | 'validate'
|
type TabType = 'description' | 'sub-requirements' | 'co-requirements' | 'acceptance-criteria' | 'shared-comments' | 'validate'
|
||||||
|
|
||||||
// Mock data for requirement details
|
|
||||||
const mockRequirementDetails: Record<string, RequirementDetail> = {
|
|
||||||
'1': {
|
|
||||||
id: '1',
|
|
||||||
tag: 'GSR#7',
|
|
||||||
title: 'Controle e monitoramento em right-time',
|
|
||||||
priority: 0,
|
|
||||||
group: 'userExperience',
|
|
||||||
description: '',
|
|
||||||
authors: ['Ricardo Belo'],
|
|
||||||
subRequirements: [],
|
|
||||||
coRequirements: [],
|
|
||||||
acceptanceCriteria: [],
|
|
||||||
sharedComments: [],
|
|
||||||
},
|
|
||||||
'2': {
|
|
||||||
id: '2',
|
|
||||||
tag: 'GSR#10',
|
|
||||||
title: 'Interface de software DT-externo bem definida.',
|
|
||||||
priority: 1,
|
|
||||||
group: 'Intelligence',
|
|
||||||
description: 'This requirement defines the need for a well-defined software interface between the Digital Twin and external systems.',
|
|
||||||
authors: ['Ricardo Belo', 'Maria Silva'],
|
|
||||||
subRequirements: [
|
|
||||||
{ id: 'sub1', tag: 'SFR#1', title: 'API Documentation' },
|
|
||||||
{ id: 'sub2', tag: 'SFR#2', title: 'Authentication Protocol' },
|
|
||||||
],
|
|
||||||
coRequirements: [
|
|
||||||
{ id: 'co1', tag: 'GSR#5', title: 'Sincronização de dados em tempo real' },
|
|
||||||
],
|
|
||||||
acceptanceCriteria: [
|
|
||||||
{ id: 'ac1', description: 'API endpoints documented with OpenAPI spec', validated: true },
|
|
||||||
{ id: 'ac2', description: 'Authentication tested with external systems', validated: false },
|
|
||||||
],
|
|
||||||
sharedComments: [
|
|
||||||
{ id: 'c1', author: 'Ricardo Belo', date: '2025-11-28', content: 'Initial draft created' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'3': {
|
|
||||||
id: '3',
|
|
||||||
tag: 'GSR#12',
|
|
||||||
title: 'Visualizacao',
|
|
||||||
priority: 1,
|
|
||||||
group: 'User Experience',
|
|
||||||
description: 'Visualization requirements for the Digital Twin interface.',
|
|
||||||
authors: ['Ricardo Belo'],
|
|
||||||
subRequirements: [],
|
|
||||||
coRequirements: [],
|
|
||||||
acceptanceCriteria: [],
|
|
||||||
sharedComments: [],
|
|
||||||
},
|
|
||||||
'4': {
|
|
||||||
id: '4',
|
|
||||||
tag: 'GSR#1',
|
|
||||||
title: 'Estado corrente atualizado',
|
|
||||||
priority: 1,
|
|
||||||
group: 'Data Services',
|
|
||||||
description: 'The system must maintain an updated current state of all monitored entities.',
|
|
||||||
authors: ['Ricardo Belo'],
|
|
||||||
subRequirements: [],
|
|
||||||
coRequirements: [],
|
|
||||||
acceptanceCriteria: [],
|
|
||||||
sharedComments: [],
|
|
||||||
},
|
|
||||||
'5': {
|
|
||||||
id: '5',
|
|
||||||
tag: 'GSR#5',
|
|
||||||
title: 'Sincronização de dados em tempo real',
|
|
||||||
priority: 2,
|
|
||||||
group: 'Data Services',
|
|
||||||
description: 'Real-time data synchronization between physical and digital twin.',
|
|
||||||
authors: ['Ricardo Belo'],
|
|
||||||
subRequirements: [],
|
|
||||||
coRequirements: [],
|
|
||||||
acceptanceCriteria: [],
|
|
||||||
sharedComments: [],
|
|
||||||
},
|
|
||||||
'6': {
|
|
||||||
id: '6',
|
|
||||||
tag: 'GSR#8',
|
|
||||||
title: 'Integração com sistemas legados',
|
|
||||||
priority: 1,
|
|
||||||
group: 'Integration',
|
|
||||||
description: 'Integration capabilities with legacy systems.',
|
|
||||||
authors: ['Ricardo Belo'],
|
|
||||||
subRequirements: [],
|
|
||||||
coRequirements: [],
|
|
||||||
acceptanceCriteria: [],
|
|
||||||
sharedComments: [],
|
|
||||||
},
|
|
||||||
'7': {
|
|
||||||
id: '7',
|
|
||||||
tag: 'GSR#15',
|
|
||||||
title: 'Dashboard de gestão',
|
|
||||||
priority: 3,
|
|
||||||
group: 'Management',
|
|
||||||
description: 'Management dashboard for system monitoring and control.',
|
|
||||||
authors: ['Ricardo Belo'],
|
|
||||||
subRequirements: [],
|
|
||||||
coRequirements: [],
|
|
||||||
acceptanceCriteria: [],
|
|
||||||
sharedComments: [],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function RequirementDetailPage() {
|
export default function RequirementDetailPage() {
|
||||||
const { user, logout } = useAuth()
|
const { user, logout } = useAuth()
|
||||||
const { id } = useParams<{ id: string }>()
|
const { id } = useParams<{ id: string }>()
|
||||||
const [activeTab, setActiveTab] = useState<TabType>('description')
|
const [activeTab, setActiveTab] = useState<TabType>('description')
|
||||||
const [priority, setPriority] = useState<number>(0)
|
const [requirement, setRequirement] = useState<Requirement | null>(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
// Get requirement details from mock data
|
// Fetch requirement data on mount
|
||||||
const requirement = id ? mockRequirementDetails[id] : null
|
useEffect(() => {
|
||||||
|
const fetchRequirement = async () => {
|
||||||
// Initialize priority when requirement loads
|
if (!id) {
|
||||||
useState(() => {
|
setError('No requirement ID provided')
|
||||||
if (requirement) {
|
setLoading(false)
|
||||||
setPriority(requirement.priority)
|
return
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
if (!requirement) {
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
const data = await requirementService.getRequirement(parseInt(id, 10))
|
||||||
|
setRequirement(data)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch requirement:', err)
|
||||||
|
setError('Failed to load requirement. Please try again.')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchRequirement()
|
||||||
|
}, [id])
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white flex items-center justify-center">
|
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className="text-2xl font-semibold text-gray-700 mb-4">Requirement not found</h2>
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-teal-600 mx-auto"></div>
|
||||||
|
<p className="mt-4 text-gray-600">Loading requirement...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !requirement) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-700 mb-4">
|
||||||
|
{error || 'Requirement not found'}
|
||||||
|
</h2>
|
||||||
<Link to="/requirements" className="text-teal-600 hover:underline">
|
<Link to="/requirements" className="text-teal-600 hover:underline">
|
||||||
Back to Requirements
|
Back to Requirements
|
||||||
</Link>
|
</Link>
|
||||||
@@ -188,9 +75,10 @@ export default function RequirementDetailPage() {
|
|||||||
{ id: 'validate', label: 'Validate' },
|
{ id: 'validate', label: 'Validate' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const handlePriorityChange = (delta: number) => {
|
// Get display values from the requirement data
|
||||||
setPriority(prev => Math.max(0, prev + delta))
|
const tagCode = requirement.tag.tag_code
|
||||||
}
|
const priorityName = requirement.priority?.priority_name ?? 'None'
|
||||||
|
const validationStatus = requirement.validation_status || 'Not Validated'
|
||||||
|
|
||||||
const renderTabContent = () => {
|
const renderTabContent = () => {
|
||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
@@ -198,11 +86,24 @@ export default function RequirementDetailPage() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-bold text-gray-800 mb-2">Description</h3>
|
<h3 className="text-xl font-bold text-gray-800 mb-2">Description</h3>
|
||||||
<p className="text-sm text-gray-700 mb-4">
|
<p className="text-sm text-gray-700 mb-2">
|
||||||
<span className="font-semibold">Author(s):</span> {requirement.authors.join(', ')}
|
<span className="font-semibold">Version:</span> {requirement.version}
|
||||||
</p>
|
</p>
|
||||||
|
<p className="text-sm text-gray-700 mb-2">
|
||||||
|
<span className="font-semibold">Validation Status:</span> {validationStatus}
|
||||||
|
</p>
|
||||||
|
{requirement.created_at && (
|
||||||
|
<p className="text-sm text-gray-700 mb-2">
|
||||||
|
<span className="font-semibold">Created:</span> {new Date(requirement.created_at).toLocaleDateString()}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{requirement.updated_at && (
|
||||||
|
<p className="text-sm text-gray-700 mb-4">
|
||||||
|
<span className="font-semibold">Last Updated:</span> {new Date(requirement.updated_at).toLocaleDateString()}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<div className="bg-teal-50 border border-gray-300 rounded min-h-[300px] p-4">
|
<div className="bg-teal-50 border border-gray-300 rounded min-h-[300px] p-4">
|
||||||
<p className="text-gray-700">{requirement.description || ''}</p>
|
<p className="text-gray-700">{requirement.req_desc || 'No description provided.'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<button className="px-4 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50">
|
<button className="px-4 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50">
|
||||||
@@ -216,17 +117,7 @@ export default function RequirementDetailPage() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-bold text-gray-800 mb-4">Sub-Requirements</h3>
|
<h3 className="text-xl font-bold text-gray-800 mb-4">Sub-Requirements</h3>
|
||||||
{requirement.subRequirements.length > 0 ? (
|
<p className="text-gray-500">No sub-requirements defined yet.</p>
|
||||||
<div className="space-y-2">
|
|
||||||
{requirement.subRequirements.map(sub => (
|
|
||||||
<div key={sub.id} className="p-3 border border-gray-300 rounded">
|
|
||||||
<span className="font-semibold">{sub.tag}</span> - {sub.title}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-gray-500">No sub-requirements defined.</p>
|
|
||||||
)}
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<button className="px-4 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50">
|
<button className="px-4 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50">
|
||||||
Add Sub-Requirement
|
Add Sub-Requirement
|
||||||
@@ -239,17 +130,7 @@ export default function RequirementDetailPage() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-bold text-gray-800 mb-4">Co-Requirements</h3>
|
<h3 className="text-xl font-bold text-gray-800 mb-4">Co-Requirements</h3>
|
||||||
{requirement.coRequirements.length > 0 ? (
|
<p className="text-gray-500">No co-requirements defined yet.</p>
|
||||||
<div className="space-y-2">
|
|
||||||
{requirement.coRequirements.map(co => (
|
|
||||||
<div key={co.id} className="p-3 border border-gray-300 rounded">
|
|
||||||
<span className="font-semibold">{co.tag}</span> - {co.title}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-gray-500">No co-requirements defined.</p>
|
|
||||||
)}
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<button className="px-4 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50">
|
<button className="px-4 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50">
|
||||||
Add Co-Requirement
|
Add Co-Requirement
|
||||||
@@ -262,25 +143,7 @@ export default function RequirementDetailPage() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-bold text-gray-800 mb-4">Acceptance Criteria</h3>
|
<h3 className="text-xl font-bold text-gray-800 mb-4">Acceptance Criteria</h3>
|
||||||
{requirement.acceptanceCriteria.length > 0 ? (
|
<p className="text-gray-500">No acceptance criteria defined yet.</p>
|
||||||
<div className="space-y-2">
|
|
||||||
{requirement.acceptanceCriteria.map(ac => (
|
|
||||||
<div key={ac.id} className="p-3 border border-gray-300 rounded flex items-center gap-3">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={ac.validated}
|
|
||||||
readOnly
|
|
||||||
className="w-4 h-4 rounded border-gray-300 text-teal-600"
|
|
||||||
/>
|
|
||||||
<span className={ac.validated ? 'line-through text-gray-400' : ''}>
|
|
||||||
{ac.description}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-gray-500">No acceptance criteria defined.</p>
|
|
||||||
)}
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<button className="px-4 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50">
|
<button className="px-4 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50">
|
||||||
Add Criterion
|
Add Criterion
|
||||||
@@ -293,21 +156,7 @@ export default function RequirementDetailPage() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-bold text-gray-800 mb-4">Shared Comments</h3>
|
<h3 className="text-xl font-bold text-gray-800 mb-4">Shared Comments</h3>
|
||||||
{requirement.sharedComments.length > 0 ? (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{requirement.sharedComments.map(comment => (
|
|
||||||
<div key={comment.id} className="p-3 border border-gray-300 rounded">
|
|
||||||
<div className="flex justify-between text-sm text-gray-500 mb-1">
|
|
||||||
<span className="font-semibold">{comment.author}</span>
|
|
||||||
<span>{comment.date}</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-gray-700">{comment.content}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-gray-500">No comments yet.</p>
|
<p className="text-gray-500">No comments yet.</p>
|
||||||
)}
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<textarea
|
<textarea
|
||||||
placeholder="Add a comment..."
|
placeholder="Add a comment..."
|
||||||
@@ -375,7 +224,7 @@ export default function RequirementDetailPage() {
|
|||||||
<span className="mx-2 text-gray-400">»</span>
|
<span className="mx-2 text-gray-400">»</span>
|
||||||
<Link to="/requirements" className="text-gray-600 hover:underline">Search</Link>
|
<Link to="/requirements" className="text-gray-600 hover:underline">Search</Link>
|
||||||
<span className="mx-2 text-gray-400">»</span>
|
<span className="mx-2 text-gray-400">»</span>
|
||||||
<span className="font-semibold text-gray-900">Details {requirement.tag}</span>
|
<span className="font-semibold text-gray-900">Details {tagCode}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Language Toggle */}
|
{/* Language Toggle */}
|
||||||
@@ -413,33 +262,35 @@ export default function RequirementDetailPage() {
|
|||||||
{/* Requirement Header */}
|
{/* Requirement Header */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
{/* Tag */}
|
{/* Tag */}
|
||||||
<h2 className="text-2xl font-bold text-gray-800 mb-2">{requirement.tag}</h2>
|
<h2 className="text-2xl font-bold text-gray-800 mb-2">{tagCode}</h2>
|
||||||
|
|
||||||
{/* Priority */}
|
{/* Priority */}
|
||||||
<div className="flex items-center justify-center gap-2 mb-3">
|
<div className="flex items-center justify-center gap-2 mb-3">
|
||||||
<span className="text-gray-700 font-medium">Priority:</span>
|
<span className="text-gray-700 font-medium">Priority:</span>
|
||||||
<button
|
<span className="text-lg font-semibold text-gray-800">{priorityName}</span>
|
||||||
onClick={() => handlePriorityChange(-1)}
|
|
||||||
className="w-6 h-6 rounded-full border border-gray-400 text-gray-600 hover:bg-gray-100 flex items-center justify-center text-sm"
|
|
||||||
>
|
|
||||||
−
|
|
||||||
</button>
|
|
||||||
<span className="text-lg font-semibold text-gray-800 min-w-[20px] text-center">{priority}</span>
|
|
||||||
<button
|
|
||||||
onClick={() => handlePriorityChange(1)}
|
|
||||||
className="w-6 h-6 rounded-full border border-gray-400 text-gray-600 hover:bg-gray-100 flex items-center justify-center text-sm"
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<h3 className="text-xl text-gray-700 mb-3">{requirement.title}</h3>
|
<h3 className="text-xl text-gray-700 mb-3">{requirement.req_name}</h3>
|
||||||
|
|
||||||
{/* Group Badge */}
|
{/* Group Badges */}
|
||||||
<span className="inline-block px-3 py-1 border border-gray-400 rounded text-sm text-gray-700">
|
<div className="flex items-center justify-center gap-2 flex-wrap">
|
||||||
{requirement.group}
|
{requirement.groups.length > 0 ? (
|
||||||
|
requirement.groups.map(group => (
|
||||||
|
<span
|
||||||
|
key={group.id}
|
||||||
|
className="inline-block px-3 py-1 border rounded text-sm text-gray-700"
|
||||||
|
style={{ borderColor: group.hex_color, backgroundColor: `${group.hex_color}20` }}
|
||||||
|
>
|
||||||
|
{group.group_name}
|
||||||
</span>
|
</span>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className="inline-block px-3 py-1 border border-gray-400 rounded text-sm text-gray-700">
|
||||||
|
No groups
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content Area */}
|
{/* Content Area */}
|
||||||
|
|||||||
Reference in New Issue
Block a user