diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx
index 917694e..fdfe163 100644
--- a/frontend/src/pages/DashboardPage.tsx
+++ b/frontend/src/pages/DashboardPage.tsx
@@ -111,6 +111,31 @@ export default function DashboardPage() {
return { total, validated, notValidated: total - validated }
}
+ // Get requirements needing attention (Denied or Partial/Partially Approved)
+ const getRequirementsNeedingAttention = () => {
+ return requirements.filter(req => {
+ const status = req.validation_status?.toLowerCase() || ''
+ return status === 'denied' || status.includes('partial')
+ })
+ }
+
+ // Get requirements validated in a previous version (for auditors)
+ // These are requirements where the current version is greater than the version that was validated
+ const getRequirementsNeedingRevalidation = () => {
+ return requirements.filter(req => {
+ // Must have been validated at least once
+ if (!req.validation_version) return false
+ // Current version must be greater than the version that was validated
+ return req.version > req.validation_version
+ })
+ }
+
+ const requirementsNeedingAttention = getRequirementsNeedingAttention()
+ const requirementsNeedingRevalidation = getRequirementsNeedingRevalidation()
+
+ // Determine if user is an auditor (role_id=2)
+ const isAuditor = user?.role_id === 2
+
useEffect(() => {
const fetchData = async () => {
try {
@@ -150,6 +175,20 @@ export default function DashboardPage() {
navigate('/requirements', { state: { openCreateModal: true } })
}
+ const handleNeedsAttentionClick = () => {
+ // Filter will be handled case-insensitively on RequirementsPage
+ navigate('/requirements', { state: { validationFilter: ['Denied', 'Partial'], needsAttention: true } })
+ }
+
+ const handleNeedsRevalidationClick = () => {
+ // Navigate to requirements page with a special filter for outdated validations
+ navigate('/requirements', { state: { needsRevalidation: true } })
+ }
+
+ const handleRequirementClick = (reqId: number) => {
+ navigate(`/requirements/${reqId}`)
+ }
+
const handleProjectSelect = (project: typeof currentProject) => {
if (project) {
setCurrentProject(project)
@@ -534,6 +573,192 @@ export default function DashboardPage() {
)}
+
+ {/* Needs Attention Section */}
+ {!loading && !error && currentProject && requirementsNeedingAttention.length > 0 && (
+
+
+
+
+
+
+ Needs Attention
+
+
+ Requirements with denied or partial validation
+
+
+
+
+
+
+ {/* Requirements Cards */}
+
+ {requirementsNeedingAttention.slice(0, 6).map((req) => {
+ const status = req.validation_status?.toLowerCase() || ''
+ const isDenied = status === 'denied'
+
+ return (
+
handleRequirementClick(req.id)}
+ className={`p-4 bg-white border-l-4 rounded-lg shadow-sm hover:shadow-md cursor-pointer transition-all ${
+ isDenied ? 'border-l-red-500' : 'border-l-yellow-500'
+ }`}
+ >
+
+
+ {req.tag.tag_code}
+
+
+ {req.validation_status}
+
+
+
+ {req.req_name}
+
+
+ v{req.version}
+ {req.validated_by && (
+
+ by {req.validated_by}
+
+ )}
+
+
+ )
+ })}
+
+
+ {/* Show more indicator */}
+ {requirementsNeedingAttention.length > 6 && (
+
+
+
+ )}
+
+ )}
+
+ {/* Needs Revalidation Section - Only for Auditors */}
+ {!loading && !error && currentProject && isAuditor && requirementsNeedingRevalidation.length > 0 && (
+
+
+
+
+
+
+ Needs Revalidation
+
+
+ Requirements updated since last validation
+
+
+
+
+
+
+ {/* Requirements Cards */}
+
+ {requirementsNeedingRevalidation.slice(0, 6).map((req) => {
+ const versionDiff = req.version - (req.validation_version || 0)
+
+ return (
+
handleRequirementClick(req.id)}
+ className="p-4 bg-white border-l-4 border-l-blue-500 rounded-lg shadow-sm hover:shadow-md cursor-pointer transition-all"
+ >
+
+
+ {req.tag.tag_code}
+
+
+ {versionDiff} version{versionDiff > 1 ? 's' : ''} behind
+
+
+
+ {req.req_name}
+
+
+
+ v{req.validation_version} → v{req.version}
+
+ {req.validated_by && (
+
+ by {req.validated_by}
+
+ )}
+
+
+ )
+ })}
+
+
+ {/* Show more indicator */}
+ {requirementsNeedingRevalidation.length > 6 && (
+
+
+
+ )}
+
+ )}
+
+ {/* Empty Attention State - Show positive message when no issues */}
+ {!loading && !error && currentProject && requirements.length > 0 && requirementsNeedingAttention.length === 0 && (
+
+
+
+
+
All Clear!
+
+ No requirements need attention. All validations are either approved or pending review.
+
+
+
+
+ )}
diff --git a/frontend/src/pages/RequirementsPage.tsx b/frontend/src/pages/RequirementsPage.tsx
index dcd26c1..ca4a511 100644
--- a/frontend/src/pages/RequirementsPage.tsx
+++ b/frontend/src/pages/RequirementsPage.tsx
@@ -10,17 +10,18 @@ import type { DeletedRequirement } from '@/types'
// 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' }
+ const statusLower = status.toLowerCase()
+ if (statusLower === 'approved') {
+ return { bgColor: 'bg-green-100', textColor: 'text-green-800' }
}
+ if (statusLower === 'denied') {
+ return { bgColor: 'bg-red-100', textColor: 'text-red-800' }
+ }
+ if (statusLower.includes('partial')) {
+ return { bgColor: 'bg-yellow-100', textColor: 'text-yellow-800' }
+ }
+ // 'Not Validated' or default
+ return { bgColor: 'bg-gray-100', textColor: 'text-gray-600' }
}
// Check if requirement is in draft status
@@ -46,6 +47,8 @@ export default function RequirementsPage() {
// Filter state
const [searchQuery, setSearchQuery] = useState('')
const [selectedGroups, setSelectedGroups] = useState([])
+ const [selectedValidationStatuses, setSelectedValidationStatuses] = useState([])
+ const [needsRevalidationFilter, setNeedsRevalidationFilter] = useState(false)
const [orderBy, setOrderBy] = useState<'Date' | 'Priority' | 'Name'>('Date')
// Modal state
@@ -134,6 +137,21 @@ export default function RequirementsPage() {
// Clear the state so it doesn't reopen on refresh
navigate(location.pathname, { replace: true, state: {} })
}
+ // Handle needs revalidation filter from navigation state
+ if (location.state?.needsRevalidation) {
+ setNeedsRevalidationFilter(true)
+ navigate(location.pathname, { replace: true, state: {} })
+ }
+ // Handle validation status filter from navigation state (needsAttention mode)
+ else if (location.state?.needsAttention) {
+ // Special handling for "needs attention" - will filter Denied and Partial case-insensitively
+ setSelectedValidationStatuses(['__NEEDS_ATTENTION__'])
+ navigate(location.pathname, { replace: true, state: {} })
+ } else if (location.state?.validationFilter && Array.isArray(location.state.validationFilter)) {
+ setSelectedValidationStatuses(location.state.validationFilter)
+ // Clear the state so it doesn't persist on refresh
+ navigate(location.pathname, { replace: true, state: {} })
+ }
}, [location.state, isAuditor, projectLoading, currentProject, navigate, location.pathname])
// Fetch deleted requirements when panel is opened
@@ -159,7 +177,7 @@ export default function RequirementsPage() {
}
}
- // Filter requirements based on search and selected groups
+ // Filter requirements based on search, selected groups, validation status, and revalidation needs
const filteredRequirements = requirements.filter(req => {
const matchesSearch = searchQuery === '' ||
req.tag.tag_code.toLowerCase().includes(searchQuery.toLowerCase()) ||
@@ -168,7 +186,29 @@ export default function RequirementsPage() {
const matchesGroup = selectedGroups.length === 0 ||
req.groups.some(g => selectedGroups.includes(g.id))
- return matchesSearch && matchesGroup
+ // Handle needs revalidation filter (requirements with outdated validation)
+ const matchesRevalidation = !needsRevalidationFilter ||
+ (req.validation_version !== null && req.version > req.validation_version)
+
+ // Handle validation status filtering
+ let matchesValidationStatus = true
+ if (selectedValidationStatuses.length > 0) {
+ const reqStatus = req.validation_status || 'Not Validated'
+ const reqStatusLower = reqStatus.toLowerCase()
+
+ // Special case: filter for "needs attention" (Denied or Partial variants)
+ if (selectedValidationStatuses.includes('__NEEDS_ATTENTION__')) {
+ matchesValidationStatus = reqStatusLower === 'denied' || reqStatusLower.includes('partial')
+ } else {
+ // Normal case-insensitive matching
+ matchesValidationStatus = selectedValidationStatuses.some(
+ status => status.toLowerCase() === reqStatusLower ||
+ (status.toLowerCase() === 'partial' && reqStatusLower.includes('partial'))
+ )
+ }
+ }
+
+ return matchesSearch && matchesGroup && matchesValidationStatus && matchesRevalidation
})
// Sort requirements
@@ -199,9 +239,19 @@ export default function RequirementsPage() {
const handleClear = () => {
setSearchQuery('')
setSelectedGroups([])
+ setSelectedValidationStatuses([])
+ setNeedsRevalidationFilter(false)
setSearchParams({})
}
+ const handleValidationStatusToggle = (status: string) => {
+ setSelectedValidationStatuses(prev =>
+ prev.includes(status)
+ ? prev.filter(s => s !== status)
+ : [...prev, status]
+ )
+ }
+
const handleSearch = () => {
// Filtering is automatic, but this could trigger a fresh API call
}
@@ -506,6 +556,72 @@ export default function RequirementsPage() {
+ {/* Filter Validation Status */}
+
+
+
Filter Validation Status
+
+ {needsRevalidationFilter && (
+
+ Showing: Needs Revalidation
+
+ )}
+ {selectedValidationStatuses.includes('__NEEDS_ATTENTION__') && (
+
+ Showing: Needs Attention
+
+ )}
+
+
+
+ {/* Needs Revalidation button - only for auditors */}
+ {isAuditor && (
+
+ )}
+ {['Approved', 'Denied', 'Partially Approved', 'Not Validated'].map((status) => {
+ // Check if this status is selected (direct match or via needs attention mode)
+ const isNeedsAttentionMode = selectedValidationStatuses.includes('__NEEDS_ATTENTION__')
+ const statusLower = status.toLowerCase()
+ const isSelected = selectedValidationStatuses.some(s => s.toLowerCase() === statusLower) ||
+ (isNeedsAttentionMode && (status === 'Denied' || statusLower.includes('partial')))
+ const statusStyles: Record
= {
+ 'Approved': isSelected ? 'bg-green-100 border-green-500 text-green-800' : 'bg-white border-gray-300 text-gray-600 hover:border-green-400',
+ 'Denied': isSelected ? 'bg-red-100 border-red-500 text-red-800' : 'bg-white border-gray-300 text-gray-600 hover:border-red-400',
+ 'Partially Approved': isSelected ? 'bg-yellow-100 border-yellow-500 text-yellow-800' : 'bg-white border-gray-300 text-gray-600 hover:border-yellow-400',
+ 'Not Validated': isSelected ? 'bg-gray-200 border-gray-500 text-gray-800' : 'bg-white border-gray-300 text-gray-600 hover:border-gray-400',
+ }
+ return (
+
+ )
+ })}
+
+
+
{/* Order By */}