import React, { useState, useEffect } from 'react' import { useAuth, useProject } 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' // Get validation status color const getValidationStatusStyle = (status: string): { bgColor: string; textColor: string } => { switch (status) { case 'Approved': return { bgColor: 'bg-green-100', textColor: 'text-green-800' } case 'Denied': return { bgColor: 'bg-red-100', textColor: 'text-red-800' } case 'Partial': return { bgColor: 'bg-yellow-100', textColor: 'text-yellow-800' } case 'Not Validated': default: return { bgColor: 'bg-gray-100', textColor: 'text-gray-600' } } } export default function RequirementsPage() { const { user, logout, isAuditor } = useAuth() const { currentProject, isLoading: projectLoading } = useProject() const [searchParams, setSearchParams] = useSearchParams() const navigate = useNavigate() // Data state const [groups, setGroups] = useState([]) const [tags, setTags] = useState([]) const [priorities, setPriorities] = useState([]) const [requirements, setRequirements] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) // Filter state const [searchQuery, setSearchQuery] = useState('') const [selectedGroups, setSelectedGroups] = useState([]) const [orderBy, setOrderBy] = useState<'Date' | 'Priority' | 'Name'>('Date') // Modal state const [showCreateModal, setShowCreateModal] = useState(false) const [createLoading, setCreateLoading] = useState(false) const [createError, setCreateError] = useState(null) // Form state for new requirement const [newReqName, setNewReqName] = useState('') const [newReqDesc, setNewReqDesc] = useState('') const [newReqTagId, setNewReqTagId] = useState('') const [newReqPriorityId, setNewReqPriorityId] = useState('') const [newReqGroupIds, setNewReqGroupIds] = useState([]) // Fetch data when project changes useEffect(() => { const fetchData = async () => { // Don't fetch if no project is selected if (!currentProject) { setRequirements([]) setLoading(false) return } try { setLoading(true) setError(null) // Fetch groups, tags, and priorities in parallel const [groupsData, tagsData, prioritiesData] = await Promise.all([ groupService.getGroups(), tagService.getTags(), priorityService.getPriorities(), ]) setGroups(groupsData) setTags(tagsData) setPriorities(prioritiesData) // Fetch requirements for the current project const requirementsData = await requirementService.getRequirements({ project_id: currentProject.id, }) setRequirements(requirementsData) } catch (err) { console.error('Failed to fetch data:', err) setError('Failed to load data. Please try again.') } finally { setLoading(false) } } if (!projectLoading) { fetchData() } }, [currentProject, projectLoading]) // Initialize filters from URL params useEffect(() => { const groupParam = searchParams.get('group') if (groupParam && groups.length > 0) { const group = groups.find(g => g.group_name === groupParam) if (group) { setSelectedGroups([group.id]) } } }, [searchParams, groups]) // Filter requirements based on search and selected groups const filteredRequirements = requirements.filter(req => { const matchesSearch = searchQuery === '' || req.tag.tag_code.toLowerCase().includes(searchQuery.toLowerCase()) || req.req_name.toLowerCase().includes(searchQuery.toLowerCase()) const matchesGroup = selectedGroups.length === 0 || req.groups.some(g => selectedGroups.includes(g.id)) return matchesSearch && matchesGroup }) // Sort requirements const sortedRequirements = [...filteredRequirements].sort((a, b) => { switch (orderBy) { case 'Priority': const priorityA = a.priority?.priority_num ?? 0 const priorityB = b.priority?.priority_num ?? 0 return priorityB - priorityA case 'Name': return a.req_name.localeCompare(b.req_name) case 'Date': default: 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 = (groupId: number) => { setSelectedGroups(prev => prev.includes(groupId) ? prev.filter(id => id !== groupId) : [...prev, groupId] ) } const handleClear = () => { setSearchQuery('') setSelectedGroups([]) setSearchParams({}) } const handleSearch = () => { // Filtering is automatic, but this could trigger a fresh API call } 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: number) => { navigate(`/requirements/${id}`) } // 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 } if (!currentProject) { setCreateError('No project selected') return } try { setCreateLoading(true) setCreateError(null) const data: RequirementCreateRequest = { project_id: currentProject.id, 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 || projectLoading) { return (

Loading requirements...

) } if (!currentProject) { return (

No Project Selected

Please select a project from the dashboard to view requirements.

) } if (error) { return (

{error}

) } return (
{/* Header */}

Digital Twin Requirements Tool

{/* Top Bar */}
{/* Breadcrumb */}
Projects » {currentProject.project_name} » Search Requirements
{/* Language Toggle */}
English
Portuguese
{/* User Info */}
{user?.full_name || user?.preferred_username || 'User'}{' '} ({user?.role || 'user'})
{/* Main Content */}
{/* Main Panel */}
{/* New Requirement Button - Hidden for auditors */} {!isAuditor && (
)} {/* Search Bar */}
setSearchQuery(e.target.value)} className="flex-1 px-4 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent" />
{/* Filter Group */}

Filter Group

{groups.map((group) => ( ))}
{/* Order By */}
Order by:
{/* Requirements List */}
{sortedRequirements.map((req) => { const tagLabel = req.tag.tag_code const priorityName = req.priority?.priority_name ?? 'None' const validationStatus = req.validation_status || 'Not Validated' const validationStyle = getValidationStatusStyle(validationStatus) const isStale = req.validation_version !== null && req.validation_version !== req.version return (
{/* Tag and name section */}
{tagLabel} - {req.req_name}
{/* Group chips */}
{req.groups.length > 0 ? ( <> {req.groups.slice(0, 2).map(group => ( {group.group_name} ))} {req.groups.length > 2 && ( g.group_name).join(', ')} > +{req.groups.length - 2} more )} ) : ( No groups )}
{/* Validation status */}
{validationStatus} {isStale && ( ⚠ Stale )}
{req.validated_by && (

by @{req.validated_by}

)}
{/* Priority and Version */}

Priority: {priorityName}

Version: {req.version}

{/* Action buttons */}
{!isAuditor && ( )}
) })} {sortedRequirements.length === 0 && (
No requirements found matching your criteria.
)}
{/* Right Sidebar - Caption */}

Caption:

{tags.map((tag) => (

{tag.tag_code} : {tag.tag_description}

))}

{/* Create Requirement Modal */} {showCreateModal && (
{/* Modal Header */}

New Requirement

{/* Modal Body */}
{/* Error message */} {createError && (
{createError}
)} {/* Tag Selection */}
{/* Requirement Name */}
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 />
{/* Description */}