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 */}