Added functions to fetch requirements from db.
Added functionality to create requirements on page
This commit is contained in:
@@ -1,140 +1,108 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useAuth } from '@/hooks'
|
||||
import { useSearchParams, Link, useNavigate } from 'react-router-dom'
|
||||
import { groupService, tagService, requirementService, priorityService } from '@/services'
|
||||
import type { Group } from '@/services/groupService'
|
||||
import type { Tag } from '@/services/tagService'
|
||||
import type { Priority } from '@/services/priorityService'
|
||||
import type { Requirement, RequirementCreateRequest } from '@/services/requirementService'
|
||||
|
||||
// Types for requirements
|
||||
interface Requirement {
|
||||
id: string
|
||||
tag: string
|
||||
title: string
|
||||
validation: string
|
||||
priority: number
|
||||
progress: number
|
||||
group: RequirementGroup
|
||||
// Helper to lighten a hex color for backgrounds
|
||||
function lightenColor(hex: string, percent: number): string {
|
||||
const num = parseInt(hex.replace('#', ''), 16)
|
||||
const amt = Math.round(2.55 * percent)
|
||||
const R = (num >> 16) + amt
|
||||
const G = (num >> 8 & 0x00FF) + amt
|
||||
const B = (num & 0x0000FF) + amt
|
||||
return '#' + (
|
||||
0x1000000 +
|
||||
(R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
|
||||
(G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
|
||||
(B < 255 ? (B < 1 ? 0 : B) : 255)
|
||||
).toString(16).slice(1)
|
||||
}
|
||||
|
||||
type RequirementGroup =
|
||||
| 'Data Services'
|
||||
| 'Integration'
|
||||
| 'Intelligence'
|
||||
| 'User Experience'
|
||||
| 'Management'
|
||||
| 'Trustworthiness'
|
||||
|
||||
// Color mapping for requirement groups
|
||||
const groupColors: Record<RequirementGroup, { bg: string; border: string }> = {
|
||||
'Data Services': { bg: 'bg-blue-200', border: 'border-blue-300' },
|
||||
'Integration': { bg: 'bg-amber-200', border: 'border-amber-300' },
|
||||
'Intelligence': { bg: 'bg-purple-200', border: 'border-purple-300' },
|
||||
'User Experience': { bg: 'bg-green-200', border: 'border-green-300' },
|
||||
'Management': { bg: 'bg-red-200', border: 'border-red-300' },
|
||||
'Trustworthiness': { bg: 'bg-teal-600', border: 'border-teal-700' },
|
||||
}
|
||||
|
||||
// Static mock data
|
||||
const mockRequirements: Requirement[] = [
|
||||
{
|
||||
id: '1',
|
||||
tag: 'GSR#7',
|
||||
title: 'Controle e monitoramento em right-time',
|
||||
validation: 'Not Validated',
|
||||
priority: 0,
|
||||
progress: 0,
|
||||
group: 'Trustworthiness',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
tag: 'GSR#10',
|
||||
title: 'Interface de software DT-externo bem definida.',
|
||||
validation: 'Not Validated',
|
||||
priority: 1,
|
||||
progress: 0,
|
||||
group: 'Intelligence',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
tag: 'GSR#12',
|
||||
title: 'Visualizacao',
|
||||
validation: 'Not Validated',
|
||||
priority: 1,
|
||||
progress: 0,
|
||||
group: 'User Experience',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
tag: 'GSR#1',
|
||||
title: 'Estado corrente atualizado',
|
||||
validation: 'Not Validated',
|
||||
priority: 1,
|
||||
progress: 0,
|
||||
group: 'Data Services',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
tag: 'GSR#5',
|
||||
title: 'Sincronização de dados em tempo real',
|
||||
validation: 'Not Validated',
|
||||
priority: 2,
|
||||
progress: 25,
|
||||
group: 'Data Services',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
tag: 'GSR#8',
|
||||
title: 'Integração com sistemas legados',
|
||||
validation: 'Not Validated',
|
||||
priority: 1,
|
||||
progress: 50,
|
||||
group: 'Integration',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
tag: 'GSR#15',
|
||||
title: 'Dashboard de gestão',
|
||||
validation: 'Not Validated',
|
||||
priority: 3,
|
||||
progress: 0,
|
||||
group: 'Management',
|
||||
},
|
||||
]
|
||||
|
||||
// Caption types
|
||||
const captionItems = [
|
||||
{ abbr: 'SFR', description: 'Specific Functional Requirement', underline: true },
|
||||
{ abbr: 'SNFR', description: 'Specific Non-Functional Requirement', underline: true },
|
||||
{ abbr: 'GNFR', description: 'Generic Non-Functional Requirement', underline: true },
|
||||
{ abbr: 'GSR', description: 'Generic Service Requirement', underline: true },
|
||||
{ abbr: 'GDR', description: 'Generic Data Requirement', underline: true },
|
||||
{ abbr: 'GCR', description: 'Generic Connection Requirement', underline: true },
|
||||
]
|
||||
|
||||
export default function RequirementsPage() {
|
||||
const { user, logout } = useAuth()
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const navigate = useNavigate()
|
||||
|
||||
// State
|
||||
// Data state
|
||||
const [groups, setGroups] = useState<Group[]>([])
|
||||
const [tags, setTags] = useState<Tag[]>([])
|
||||
const [priorities, setPriorities] = useState<Priority[]>([])
|
||||
const [requirements, setRequirements] = useState<Requirement[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Filter state
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [selectedGroups, setSelectedGroups] = useState<RequirementGroup[]>([])
|
||||
const [selectedGroups, setSelectedGroups] = useState<number[]>([])
|
||||
const [orderBy, setOrderBy] = useState<'Date' | 'Priority' | 'Name'>('Date')
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('list')
|
||||
|
||||
// Modal state
|
||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||
const [createLoading, setCreateLoading] = useState(false)
|
||||
const [createError, setCreateError] = useState<string | null>(null)
|
||||
|
||||
// Form state for new requirement
|
||||
const [newReqName, setNewReqName] = useState('')
|
||||
const [newReqDesc, setNewReqDesc] = useState('')
|
||||
const [newReqTagId, setNewReqTagId] = useState<number | ''>('')
|
||||
const [newReqPriorityId, setNewReqPriorityId] = useState<number | ''>('')
|
||||
const [newReqGroupIds, setNewReqGroupIds] = useState<number[]>([])
|
||||
|
||||
// Fetch data on mount
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
// Fetch groups, tags, priorities, and requirements in parallel
|
||||
const [groupsData, tagsData, prioritiesData, requirementsData] = await Promise.all([
|
||||
groupService.getGroups(),
|
||||
tagService.getTags(),
|
||||
priorityService.getPriorities(),
|
||||
requirementService.getRequirements(),
|
||||
])
|
||||
|
||||
setGroups(groupsData)
|
||||
setTags(tagsData)
|
||||
setPriorities(prioritiesData)
|
||||
setRequirements(requirementsData)
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch data:', err)
|
||||
setError('Failed to load data. Please try again.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
// Initialize filters from URL params
|
||||
useEffect(() => {
|
||||
const groupParam = searchParams.get('group')
|
||||
if (groupParam) {
|
||||
setSelectedGroups([groupParam as RequirementGroup])
|
||||
if (groupParam && groups.length > 0) {
|
||||
const group = groups.find(g => g.group_name === groupParam)
|
||||
if (group) {
|
||||
setSelectedGroups([group.id])
|
||||
}
|
||||
}
|
||||
}, [searchParams])
|
||||
}, [searchParams, groups])
|
||||
|
||||
// Filter requirements based on search and selected groups
|
||||
const filteredRequirements = mockRequirements.filter(req => {
|
||||
const filteredRequirements = requirements.filter(req => {
|
||||
const tagLabel = `${req.tag.tag_code}#${req.id}`
|
||||
const matchesSearch = searchQuery === '' ||
|
||||
req.tag.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
req.title.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
tagLabel.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
req.req_name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
|
||||
const matchesGroup = selectedGroups.length === 0 ||
|
||||
selectedGroups.includes(req.group)
|
||||
req.groups.some(g => selectedGroups.includes(g.id))
|
||||
|
||||
return matchesSearch && matchesGroup
|
||||
})
|
||||
@@ -143,19 +111,24 @@ export default function RequirementsPage() {
|
||||
const sortedRequirements = [...filteredRequirements].sort((a, b) => {
|
||||
switch (orderBy) {
|
||||
case 'Priority':
|
||||
return b.priority - a.priority
|
||||
const priorityA = a.priority?.priority_num ?? 0
|
||||
const priorityB = b.priority?.priority_num ?? 0
|
||||
return priorityB - priorityA
|
||||
case 'Name':
|
||||
return a.title.localeCompare(b.title)
|
||||
return a.req_name.localeCompare(b.req_name)
|
||||
case 'Date':
|
||||
default:
|
||||
return 0
|
||||
const dateA = a.created_at ? new Date(a.created_at).getTime() : 0
|
||||
const dateB = b.created_at ? new Date(b.created_at).getTime() : 0
|
||||
return dateB - dateA
|
||||
}
|
||||
})
|
||||
|
||||
const handleGroupToggle = (group: RequirementGroup) => {
|
||||
const handleGroupToggle = (groupId: number) => {
|
||||
setSelectedGroups(prev =>
|
||||
prev.includes(group)
|
||||
? prev.filter(g => g !== group)
|
||||
: [...prev, group]
|
||||
prev.includes(groupId)
|
||||
? prev.filter(id => id !== groupId)
|
||||
: [...prev, groupId]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -166,18 +139,128 @@ export default function RequirementsPage() {
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
// For now, filtering is automatic - this could trigger an API call later
|
||||
// Filtering is automatic, but this could trigger a fresh API call
|
||||
}
|
||||
|
||||
const handleRemove = (id: string) => {
|
||||
// For now, just a placeholder - will connect to backend later
|
||||
console.log('Remove requirement:', id)
|
||||
const handleRemove = async (id: number) => {
|
||||
if (!confirm('Are you sure you want to delete this requirement?')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await requirementService.deleteRequirement(id)
|
||||
// Remove from local state
|
||||
setRequirements(prev => prev.filter(r => r.id !== id))
|
||||
} catch (err) {
|
||||
console.error('Failed to delete requirement:', err)
|
||||
alert('Failed to delete requirement. Please try again.')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDetails = (id: string) => {
|
||||
const handleDetails = (id: number) => {
|
||||
navigate(`/requirements/${id}`)
|
||||
}
|
||||
|
||||
// Get the primary group color for a requirement (first group or default)
|
||||
const getRequirementColor = (req: Requirement): string => {
|
||||
if (req.groups.length > 0) {
|
||||
return req.groups[0].hex_color
|
||||
}
|
||||
return '#6B7280' // default gray
|
||||
}
|
||||
|
||||
// Modal functions
|
||||
const openCreateModal = () => {
|
||||
setShowCreateModal(true)
|
||||
setCreateError(null)
|
||||
// Reset form
|
||||
setNewReqName('')
|
||||
setNewReqDesc('')
|
||||
setNewReqTagId('')
|
||||
setNewReqPriorityId('')
|
||||
setNewReqGroupIds([])
|
||||
}
|
||||
|
||||
const closeCreateModal = () => {
|
||||
setShowCreateModal(false)
|
||||
setCreateError(null)
|
||||
}
|
||||
|
||||
const handleCreateGroupToggle = (groupId: number) => {
|
||||
setNewReqGroupIds(prev =>
|
||||
prev.includes(groupId)
|
||||
? prev.filter(id => id !== groupId)
|
||||
: [...prev, groupId]
|
||||
)
|
||||
}
|
||||
|
||||
const handleCreateRequirement = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
// Validation
|
||||
if (!newReqName.trim()) {
|
||||
setCreateError('Requirement name is required')
|
||||
return
|
||||
}
|
||||
if (!newReqTagId) {
|
||||
setCreateError('Please select a tag')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setCreateLoading(true)
|
||||
setCreateError(null)
|
||||
|
||||
const data: RequirementCreateRequest = {
|
||||
tag_id: newReqTagId as number,
|
||||
req_name: newReqName.trim(),
|
||||
req_desc: newReqDesc.trim() || undefined,
|
||||
priority_id: newReqPriorityId ? (newReqPriorityId as number) : undefined,
|
||||
group_ids: newReqGroupIds.length > 0 ? newReqGroupIds : undefined,
|
||||
}
|
||||
|
||||
const newRequirement = await requirementService.createRequirement(data)
|
||||
|
||||
// Add to local state
|
||||
setRequirements(prev => [newRequirement, ...prev])
|
||||
|
||||
// Close modal
|
||||
closeCreateModal()
|
||||
} catch (err) {
|
||||
console.error('Failed to create requirement:', err)
|
||||
setCreateError('Failed to create requirement. Please try again.')
|
||||
} finally {
|
||||
setCreateLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<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 requirements...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-red-600 mb-4">{error}</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-4 py-2 bg-teal-600 text-white rounded hover:bg-teal-700"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
{/* Header */}
|
||||
@@ -236,7 +319,10 @@ export default function RequirementsPage() {
|
||||
<div className="flex-1">
|
||||
{/* New Requirement Button */}
|
||||
<div className="mb-6">
|
||||
<button className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-50">
|
||||
<button
|
||||
onClick={openCreateModal}
|
||||
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
New Requirement
|
||||
</button>
|
||||
</div>
|
||||
@@ -268,15 +354,15 @@ export default function RequirementsPage() {
|
||||
<div className="mb-6">
|
||||
<p className="text-sm text-gray-600 mb-3">Filter Group</p>
|
||||
<div className="grid grid-cols-3 gap-x-8 gap-y-2">
|
||||
{(['Data Services', 'Intelligence', 'Management', 'Integration', 'User Experience', 'Trustworthiness'] as RequirementGroup[]).map((group) => (
|
||||
<label key={group} className="flex items-center gap-2 cursor-pointer">
|
||||
{groups.map((group) => (
|
||||
<label key={group.id} className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedGroups.includes(group)}
|
||||
onChange={() => handleGroupToggle(group)}
|
||||
checked={selectedGroups.includes(group.id)}
|
||||
onChange={() => handleGroupToggle(group.id)}
|
||||
className="w-4 h-4 rounded border-gray-300 text-teal-600 focus:ring-teal-500"
|
||||
/>
|
||||
<span className="text-sm text-blue-600 underline">{group}</span>
|
||||
<span className="text-sm text-blue-600 underline">{group.group_name}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
@@ -327,30 +413,39 @@ export default function RequirementsPage() {
|
||||
{/* Requirements List */}
|
||||
<div className="space-y-4">
|
||||
{sortedRequirements.map((req) => {
|
||||
const colors = groupColors[req.group]
|
||||
const primaryColor = getRequirementColor(req)
|
||||
const bgColor = lightenColor(primaryColor, 60)
|
||||
const tagLabel = `${req.tag.tag_code}#${req.id}`
|
||||
const priorityNum = req.priority?.priority_num ?? 0
|
||||
const validationStatus = req.validation_status || 'Not Validated'
|
||||
|
||||
return (
|
||||
<div
|
||||
key={req.id}
|
||||
className={`flex items-center border ${colors.border} rounded overflow-hidden`}
|
||||
className="flex items-center rounded overflow-hidden"
|
||||
style={{ borderColor: primaryColor, borderWidth: '1px', borderStyle: 'solid' }}
|
||||
>
|
||||
{/* Colored tag section */}
|
||||
<div className={`${colors.bg} px-4 py-4 min-w-[320px]`}>
|
||||
<div
|
||||
className="px-4 py-4 min-w-[320px]"
|
||||
style={{ backgroundColor: bgColor }}
|
||||
>
|
||||
<span className="font-bold text-gray-800">
|
||||
{req.tag} - {req.title}
|
||||
{tagLabel} - {req.req_name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Validation status */}
|
||||
<div className="flex-1 px-6 py-4 text-center">
|
||||
<span className="text-sm text-gray-600">
|
||||
Validation: {req.validation}
|
||||
Validation: {validationStatus}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Priority and Progress */}
|
||||
{/* Priority and Version */}
|
||||
<div className="px-6 py-4 text-right">
|
||||
<p className="text-sm text-gray-700">Priority: {req.priority}</p>
|
||||
<p className="text-sm text-gray-600">{req.progress.toFixed(2)}% Done</p>
|
||||
<p className="text-sm text-gray-700">Priority: {priorityNum}</p>
|
||||
<p className="text-sm text-gray-600">Version: {req.version}</p>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
@@ -385,10 +480,10 @@ export default function RequirementsPage() {
|
||||
<div className="border-l border-gray-200 pl-6">
|
||||
<h3 className="font-semibold text-gray-800 mb-4">Caption:</h3>
|
||||
<div className="space-y-2">
|
||||
{captionItems.map((item) => (
|
||||
<p key={item.abbr} className="text-sm">
|
||||
<span className="text-blue-600 underline">{item.abbr}</span>
|
||||
<span className="text-gray-600">: {item.description}</span>
|
||||
{tags.map((tag) => (
|
||||
<p key={tag.id} className="text-sm">
|
||||
<span className="text-blue-600 underline">{tag.tag_code}</span>
|
||||
<span className="text-gray-600">: {tag.tag_description}</span>
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
@@ -397,6 +492,145 @@ export default function RequirementsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Create Requirement Modal */}
|
||||
{showCreateModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-lg mx-4">
|
||||
{/* Modal Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
||||
<h2 className="text-xl font-semibold text-gray-800">New Requirement</h2>
|
||||
<button
|
||||
onClick={closeCreateModal}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Modal Body */}
|
||||
<form onSubmit={handleCreateRequirement}>
|
||||
<div className="px-6 py-4 space-y-4">
|
||||
{/* Error message */}
|
||||
{createError && (
|
||||
<div className="p-3 bg-red-100 border border-red-400 text-red-700 rounded text-sm">
|
||||
{createError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tag Selection */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Tag <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
value={newReqTagId}
|
||||
onChange={(e) => setNewReqTagId(e.target.value ? Number(e.target.value) : '')}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||
required
|
||||
>
|
||||
<option value="">Select a tag...</option>
|
||||
{tags.map((tag) => (
|
||||
<option key={tag.id} value={tag.id}>
|
||||
{tag.tag_code} - {tag.tag_description}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Requirement Name */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Name <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newReqName}
|
||||
onChange={(e) => setNewReqName(e.target.value)}
|
||||
placeholder="Enter requirement name"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
value={newReqDesc}
|
||||
onChange={(e) => setNewReqDesc(e.target.value)}
|
||||
placeholder="Enter requirement description (optional)"
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Priority Selection */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Priority
|
||||
</label>
|
||||
<select
|
||||
value={newReqPriorityId}
|
||||
onChange={(e) => setNewReqPriorityId(e.target.value ? Number(e.target.value) : '')}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||
>
|
||||
<option value="">Select a priority (optional)...</option>
|
||||
{priorities.map((priority) => (
|
||||
<option key={priority.id} value={priority.id}>
|
||||
{priority.priority_name} ({priority.priority_num})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Groups Selection */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Groups
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-2 max-h-40 overflow-y-auto border border-gray-200 rounded p-3">
|
||||
{groups.map((group) => (
|
||||
<label key={group.id} className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newReqGroupIds.includes(group.id)}
|
||||
onChange={() => handleCreateGroupToggle(group.id)}
|
||||
className="w-4 h-4 rounded border-gray-300 text-teal-600 focus:ring-teal-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">{group.group_name}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className="flex justify-end gap-3 px-6 py-4 border-t border-gray-200 bg-gray-50 rounded-b-lg">
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeCreateModal}
|
||||
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-100"
|
||||
disabled={createLoading}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={createLoading}
|
||||
>
|
||||
{createLoading ? 'Creating...' : 'Create Requirement'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
export { authService } from './authService'
|
||||
export { groupService } from './groupService'
|
||||
export type { Group } from './groupService'
|
||||
export { tagService } from './tagService'
|
||||
export type { Tag } from './tagService'
|
||||
export { priorityService } from './priorityService'
|
||||
export type { Priority } from './priorityService'
|
||||
export { requirementService } from './requirementService'
|
||||
export type { Requirement, RequirementCreateRequest, RequirementUpdateRequest } from './requirementService'
|
||||
|
||||
36
frontend/src/services/priorityService.ts
Normal file
36
frontend/src/services/priorityService.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
const API_BASE_URL = '/api'
|
||||
|
||||
export interface Priority {
|
||||
id: number
|
||||
priority_name: string
|
||||
priority_num: number
|
||||
}
|
||||
|
||||
class PriorityService {
|
||||
/**
|
||||
* Get all priorities from the API.
|
||||
*/
|
||||
async getPriorities(): Promise<Priority[]> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/priorities`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const priorities: Priority[] = await response.json()
|
||||
return priorities
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch priorities:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const priorityService = new PriorityService()
|
||||
176
frontend/src/services/requirementService.ts
Normal file
176
frontend/src/services/requirementService.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { Group } from './groupService'
|
||||
import { Tag } from './tagService'
|
||||
import { Priority } from './priorityService'
|
||||
|
||||
const API_BASE_URL = '/api'
|
||||
|
||||
export interface Requirement {
|
||||
id: number
|
||||
req_name: string
|
||||
req_desc: string | null
|
||||
version: number
|
||||
created_at: string | null
|
||||
updated_at: string | null
|
||||
tag: Tag
|
||||
priority: Priority | null
|
||||
groups: Group[]
|
||||
validation_status: string | null
|
||||
}
|
||||
|
||||
export interface RequirementCreateRequest {
|
||||
tag_id: number
|
||||
req_name: string
|
||||
req_desc?: string
|
||||
priority_id?: number
|
||||
group_ids?: number[]
|
||||
}
|
||||
|
||||
export interface RequirementUpdateRequest {
|
||||
req_name?: string
|
||||
req_desc?: string
|
||||
tag_id?: number
|
||||
priority_id?: number
|
||||
group_ids?: number[]
|
||||
}
|
||||
|
||||
class RequirementService {
|
||||
/**
|
||||
* Get all requirements from the API.
|
||||
* Optionally filter by group_id or tag_id.
|
||||
*/
|
||||
async getRequirements(params?: { group_id?: number; tag_id?: number }): Promise<Requirement[]> {
|
||||
try {
|
||||
let url = `${API_BASE_URL}/requirements`
|
||||
|
||||
if (params) {
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params.group_id) queryParams.append('group_id', params.group_id.toString())
|
||||
if (params.tag_id) queryParams.append('tag_id', params.tag_id.toString())
|
||||
|
||||
const queryString = queryParams.toString()
|
||||
if (queryString) {
|
||||
url += `?${queryString}`
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const requirements: Requirement[] = await response.json()
|
||||
return requirements
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch requirements:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific requirement by ID.
|
||||
*/
|
||||
async getRequirement(requirementId: number): Promise<Requirement> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/requirements/${requirementId}`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const requirement: Requirement = await response.json()
|
||||
return requirement
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch requirement:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new requirement.
|
||||
*/
|
||||
async createRequirement(data: RequirementCreateRequest): Promise<Requirement> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/requirements`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const requirement: Requirement = await response.json()
|
||||
return requirement
|
||||
} catch (error) {
|
||||
console.error('Failed to create requirement:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing requirement.
|
||||
*/
|
||||
async updateRequirement(requirementId: number, data: RequirementUpdateRequest): Promise<Requirement> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/requirements/${requirementId}`, {
|
||||
method: 'PUT',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const requirement: Requirement = await response.json()
|
||||
return requirement
|
||||
} catch (error) {
|
||||
console.error('Failed to update requirement:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a requirement.
|
||||
*/
|
||||
async deleteRequirement(requirementId: number): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/requirements/${requirementId}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete requirement:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const requirementService = new RequirementService()
|
||||
61
frontend/src/services/tagService.ts
Normal file
61
frontend/src/services/tagService.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
const API_BASE_URL = '/api'
|
||||
|
||||
export interface Tag {
|
||||
id: number
|
||||
tag_code: string
|
||||
tag_description: string
|
||||
}
|
||||
|
||||
class TagService {
|
||||
/**
|
||||
* Get all tags from the API.
|
||||
*/
|
||||
async getTags(): Promise<Tag[]> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/tags`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const tags: Tag[] = await response.json()
|
||||
return tags
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch tags:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific tag by ID.
|
||||
*/
|
||||
async getTag(tagId: number): Promise<Tag> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/tags/${tagId}`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const tag: Tag = await response.json()
|
||||
return tag
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch tag:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const tagService = new TagService()
|
||||
Reference in New Issue
Block a user