Added auditor logic
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useMemo,
|
||||
type ReactNode,
|
||||
} from 'react'
|
||||
import type { User, AuthContextType } from '@/types'
|
||||
@@ -51,10 +52,14 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
||||
refreshUser()
|
||||
}, [refreshUser])
|
||||
|
||||
// Determine if user is an auditor (role_id = 2)
|
||||
const isAuditor = useMemo(() => user?.role_id === 2, [user?.role_id])
|
||||
|
||||
const value: AuthContextType = {
|
||||
user,
|
||||
isLoading,
|
||||
isAuthenticated: !!user,
|
||||
isAuditor,
|
||||
login,
|
||||
logout,
|
||||
refreshUser,
|
||||
|
||||
@@ -246,7 +246,7 @@ export default function DashboardPage() {
|
||||
</svg>
|
||||
<span className="text-sm text-gray-700">
|
||||
{user?.full_name || user?.preferred_username || 'User'}{' '}
|
||||
<span className="text-gray-500">(admin)</span>
|
||||
<span className="text-gray-500">({user?.role || 'user'})</span>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useAuth, useProject } from '@/hooks'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { requirementService } from '@/services'
|
||||
import { requirementService, validationService } from '@/services'
|
||||
import type { Requirement } from '@/services/requirementService'
|
||||
import type { ValidationStatus, ValidationHistory } from '@/types'
|
||||
|
||||
// Tab types
|
||||
type TabType = 'description' | 'sub-requirements' | 'co-requirements' | 'acceptance-criteria' | 'shared-comments' | 'validate'
|
||||
|
||||
export default function RequirementDetailPage() {
|
||||
const { user, logout } = useAuth()
|
||||
const { user, logout, isAuditor } = useAuth()
|
||||
const { currentProject } = useProject()
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const [activeTab, setActiveTab] = useState<TabType>('description')
|
||||
const [requirement, setRequirement] = useState<Requirement | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Validation state
|
||||
const [validationStatuses, setValidationStatuses] = useState<ValidationStatus[]>([])
|
||||
const [validationHistory, setValidationHistory] = useState<ValidationHistory[]>([])
|
||||
const [selectedStatusId, setSelectedStatusId] = useState<number | ''>('')
|
||||
const [validationComment, setValidationComment] = useState('')
|
||||
const [validationLoading, setValidationLoading] = useState(false)
|
||||
const [validationError, setValidationError] = useState<string | null>(null)
|
||||
const [historyLoading, setHistoryLoading] = useState(false)
|
||||
|
||||
// Fetch requirement data on mount
|
||||
useEffect(() => {
|
||||
@@ -41,6 +51,78 @@ export default function RequirementDetailPage() {
|
||||
fetchRequirement()
|
||||
}, [id])
|
||||
|
||||
// Fetch validation statuses and history when validate tab is active
|
||||
useEffect(() => {
|
||||
const fetchValidationData = async () => {
|
||||
if (activeTab !== 'validate' || !id) return
|
||||
|
||||
try {
|
||||
setHistoryLoading(true)
|
||||
const [statuses, history] = await Promise.all([
|
||||
validationService.getStatuses(),
|
||||
validationService.getValidationHistory(parseInt(id, 10))
|
||||
])
|
||||
setValidationStatuses(statuses.filter(s => s.id !== 4)) // Exclude "Not Validated" as option
|
||||
setValidationHistory(history)
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch validation data:', err)
|
||||
} finally {
|
||||
setHistoryLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchValidationData()
|
||||
}, [activeTab, id])
|
||||
|
||||
// Handle validation submission
|
||||
const handleSubmitValidation = async () => {
|
||||
if (!selectedStatusId || !id) {
|
||||
setValidationError('Please select a validation status')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setValidationLoading(true)
|
||||
setValidationError(null)
|
||||
|
||||
const newValidation = await validationService.createValidation(parseInt(id, 10), {
|
||||
status_id: selectedStatusId as number,
|
||||
comment: validationComment.trim() || undefined
|
||||
})
|
||||
|
||||
// Add to history and update requirement
|
||||
setValidationHistory(prev => [newValidation, ...prev])
|
||||
|
||||
// Refresh requirement to get updated validation status
|
||||
const updatedRequirement = await requirementService.getRequirement(parseInt(id, 10))
|
||||
setRequirement(updatedRequirement)
|
||||
|
||||
// Reset form
|
||||
setSelectedStatusId('')
|
||||
setValidationComment('')
|
||||
} catch (err) {
|
||||
console.error('Failed to submit validation:', err)
|
||||
setValidationError('Failed to submit validation. Please try again.')
|
||||
} finally {
|
||||
setValidationLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Get validation status style
|
||||
const getValidationStatusStyle = (status: string): string => {
|
||||
switch (status) {
|
||||
case 'Approved':
|
||||
return 'bg-green-100 text-green-800'
|
||||
case 'Denied':
|
||||
return 'bg-red-100 text-red-800'
|
||||
case 'Partial':
|
||||
return 'bg-yellow-100 text-yellow-800'
|
||||
case 'Not Validated':
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-600'
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||
@@ -91,7 +173,13 @@ export default function RequirementDetailPage() {
|
||||
<span className="font-semibold">Version:</span> {requirement.version}
|
||||
</p>
|
||||
<p className="text-sm text-gray-700 mb-2">
|
||||
<span className="font-semibold">Validation Status:</span> {validationStatus}
|
||||
<span className="font-semibold">Validation Status:</span>{' '}
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getValidationStatusStyle(validationStatus)}`}>
|
||||
{validationStatus}
|
||||
</span>
|
||||
{requirement.validated_by && (
|
||||
<span className="text-gray-500 ml-2">by @{requirement.validated_by}</span>
|
||||
)}
|
||||
</p>
|
||||
{requirement.created_at && (
|
||||
<p className="text-sm text-gray-700 mb-2">
|
||||
@@ -106,11 +194,14 @@ export default function RequirementDetailPage() {
|
||||
<div className="bg-teal-50 border border-gray-300 rounded min-h-[300px] p-4">
|
||||
<p className="text-gray-700">{requirement.req_desc || 'No description provided.'}</p>
|
||||
</div>
|
||||
<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">
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
{/* Hide Edit button for auditors */}
|
||||
{!isAuditor && (
|
||||
<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">
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -119,11 +210,13 @@ export default function RequirementDetailPage() {
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-gray-800 mb-4">Sub-Requirements</h3>
|
||||
<p className="text-gray-500">No sub-requirements defined yet.</p>
|
||||
<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">
|
||||
Add Sub-Requirement
|
||||
</button>
|
||||
</div>
|
||||
{!isAuditor && (
|
||||
<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">
|
||||
Add Sub-Requirement
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -132,11 +225,13 @@ export default function RequirementDetailPage() {
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-gray-800 mb-4">Co-Requirements</h3>
|
||||
<p className="text-gray-500">No co-requirements defined yet.</p>
|
||||
<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">
|
||||
Add Co-Requirement
|
||||
</button>
|
||||
</div>
|
||||
{!isAuditor && (
|
||||
<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">
|
||||
Add Co-Requirement
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -145,11 +240,13 @@ export default function RequirementDetailPage() {
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-gray-800 mb-4">Acceptance Criteria</h3>
|
||||
<p className="text-gray-500">No acceptance criteria defined yet.</p>
|
||||
<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">
|
||||
Add Criterion
|
||||
</button>
|
||||
</div>
|
||||
{!isAuditor && (
|
||||
<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">
|
||||
Add Criterion
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -175,27 +272,159 @@ export default function RequirementDetailPage() {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-gray-800 mb-4">Validate Requirement</h3>
|
||||
<div className="p-4 border border-gray-300 rounded bg-gray-50">
|
||||
<p className="text-gray-700 mb-4">
|
||||
Review all acceptance criteria and validate this requirement when ready.
|
||||
</p>
|
||||
<div className="space-y-2 mb-4">
|
||||
<label className="flex items-center gap-2">
|
||||
<input type="checkbox" className="w-4 h-4 rounded border-gray-300 text-teal-600" />
|
||||
<span className="text-sm">All acceptance criteria have been met</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2">
|
||||
<input type="checkbox" className="w-4 h-4 rounded border-gray-300 text-teal-600" />
|
||||
<span className="text-sm">Documentation is complete</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2">
|
||||
<input type="checkbox" className="w-4 h-4 rounded border-gray-300 text-teal-600" />
|
||||
<span className="text-sm">Stakeholders have approved</span>
|
||||
</label>
|
||||
|
||||
{/* Current Status */}
|
||||
<div className="mb-6 p-4 bg-gray-50 border border-gray-200 rounded">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Current Status:</p>
|
||||
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${getValidationStatusStyle(validationStatus)}`}>
|
||||
{validationStatus}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-gray-600">Requirement Version:</p>
|
||||
<span className="text-lg font-semibold text-gray-800">{requirement.version}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700">
|
||||
Validate Requirement
|
||||
</button>
|
||||
{requirement.validated_by && (
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
Last validated by @{requirement.validated_by}
|
||||
{requirement.validated_at && ` on ${new Date(requirement.validated_at).toLocaleDateString()}`}
|
||||
</p>
|
||||
)}
|
||||
{requirement.validation_version !== null && requirement.validation_version !== requirement.version && (
|
||||
<div className="mt-3 p-2 bg-orange-100 border border-orange-300 rounded">
|
||||
<p className="text-sm text-orange-800 flex items-center gap-2">
|
||||
<span>⚠️</span>
|
||||
<span>
|
||||
This requirement was modified after the last validation (validated at version {requirement.validation_version}, current version {requirement.version}).
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Validation Form - Only for auditors and admins */}
|
||||
{(isAuditor || user?.role_id === 1) && (
|
||||
<div className="mb-6 p-4 border border-gray-300 rounded">
|
||||
<h4 className="font-semibold text-gray-800 mb-4">Submit Validation</h4>
|
||||
|
||||
{validationError && (
|
||||
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded text-sm">
|
||||
{validationError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Status Selection */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Validation Status <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
value={selectedStatusId}
|
||||
onChange={(e) => setSelectedStatusId(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"
|
||||
disabled={validationLoading}
|
||||
>
|
||||
<option value="">Select a status...</option>
|
||||
{validationStatuses.map((status) => (
|
||||
<option key={status.id} value={status.id}>
|
||||
{status.status_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Comment */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Comment
|
||||
</label>
|
||||
<textarea
|
||||
value={validationComment}
|
||||
onChange={(e) => setValidationComment(e.target.value)}
|
||||
placeholder="Add a comment explaining your decision (optional but recommended)"
|
||||
rows={4}
|
||||
className="w-full p-3 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 resize-none"
|
||||
disabled={validationLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
onClick={handleSubmitValidation}
|
||||
disabled={validationLoading || !selectedStatusId}
|
||||
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"
|
||||
>
|
||||
{validationLoading ? 'Submitting...' : 'Submit Validation'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Validation History */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-800 mb-4">Validation History</h4>
|
||||
|
||||
{historyLoading ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-teal-600 mx-auto"></div>
|
||||
<p className="mt-2 text-gray-500 text-sm">Loading history...</p>
|
||||
</div>
|
||||
) : validationHistory.length === 0 ? (
|
||||
<p className="text-gray-500 text-center py-8">No validation history yet.</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border border-gray-200 rounded">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Version</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Validator</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Comment</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{validationHistory.map((validation) => {
|
||||
const isStale = validation.req_version_snapshot !== requirement.version
|
||||
return (
|
||||
<tr key={validation.id} className={isStale ? 'bg-orange-50' : ''}>
|
||||
<td className="px-4 py-3 text-sm text-gray-700">
|
||||
{validation.created_at
|
||||
? new Date(validation.created_at).toLocaleString()
|
||||
: 'N/A'}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getValidationStatusStyle(validation.status_name)}`}>
|
||||
{validation.status_name}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-700">
|
||||
<span className="flex items-center gap-1">
|
||||
v{validation.req_version_snapshot}
|
||||
{isStale && (
|
||||
<span className="text-orange-600" title="Requirement was modified after this validation">
|
||||
⚠️
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-700">
|
||||
@{validation.validator_username}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-700">
|
||||
{validation.comment || <span className="text-gray-400 italic">No comment</span>}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -244,8 +473,8 @@ export default function RequirementDetailPage() {
|
||||
<path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span className="text-sm text-gray-700">
|
||||
{user?.full_name || user?.preferred_username || 'Ricardo Belo'}{' '}
|
||||
<span className="text-gray-500">(admin)</span>
|
||||
{user?.full_name || user?.preferred_username || 'User'}{' '}
|
||||
<span className="text-gray-500">({user?.role || 'user'})</span>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -7,6 +7,21 @@ 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' }
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to lighten a hex color for backgrounds
|
||||
function lightenColor(hex: string, percent: number): string {
|
||||
const num = parseInt(hex.replace('#', ''), 16)
|
||||
@@ -23,7 +38,7 @@ function lightenColor(hex: string, percent: number): string {
|
||||
}
|
||||
|
||||
export default function RequirementsPage() {
|
||||
const { user, logout } = useAuth()
|
||||
const { user, logout, isAuditor } = useAuth()
|
||||
const { currentProject, isLoading: projectLoading } = useProject()
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const navigate = useNavigate()
|
||||
@@ -335,8 +350,8 @@ export default function RequirementsPage() {
|
||||
<path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span className="text-sm text-gray-700">
|
||||
{user?.full_name || user?.preferred_username || 'Ricardo Belo'}{' '}
|
||||
<span className="text-gray-500">(admin)</span>
|
||||
{user?.full_name || user?.preferred_username || 'User'}{' '}
|
||||
<span className="text-gray-500">({user?.role || 'user'})</span>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
@@ -354,15 +369,17 @@ export default function RequirementsPage() {
|
||||
<div className="flex gap-8">
|
||||
{/* Main Panel */}
|
||||
<div className="flex-1">
|
||||
{/* New Requirement Button */}
|
||||
<div className="mb-6">
|
||||
<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>
|
||||
{/* New Requirement Button - Hidden for auditors */}
|
||||
{!isAuditor && (
|
||||
<div className="mb-6">
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="flex gap-2 mb-6">
|
||||
@@ -432,6 +449,8 @@ export default function RequirementsPage() {
|
||||
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 (
|
||||
<div
|
||||
@@ -451,9 +470,21 @@ export default function RequirementsPage() {
|
||||
|
||||
{/* Validation status */}
|
||||
<div className="flex-1 px-6 py-4 text-center">
|
||||
<span className="text-sm text-gray-600">
|
||||
Validation: {validationStatus}
|
||||
</span>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${validationStyle.bgColor} ${validationStyle.textColor}`}>
|
||||
{validationStatus}
|
||||
</span>
|
||||
{isStale && (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800" title="Requirement was modified after validation">
|
||||
⚠ Stale
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{req.validated_by && (
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
by @{req.validated_by}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Priority and Version */}
|
||||
@@ -470,12 +501,14 @@ export default function RequirementsPage() {
|
||||
>
|
||||
Details
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleRemove(req.id)}
|
||||
className="px-3 py-1 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
{!isAuditor && (
|
||||
<button
|
||||
onClick={() => handleRemove(req.id)}
|
||||
className="px-3 py-1 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -9,3 +9,4 @@ export { requirementService } from './requirementService'
|
||||
export type { Requirement, RequirementCreateRequest, RequirementUpdateRequest } from './requirementService'
|
||||
export { projectService } from './projectService'
|
||||
export type { Project, ProjectCreateRequest, ProjectUpdateRequest } from './projectService'
|
||||
export { validationService } from './validationService'
|
||||
|
||||
@@ -16,6 +16,9 @@ export interface Requirement {
|
||||
priority: Priority | null
|
||||
groups: Group[]
|
||||
validation_status: string | null
|
||||
validated_by: string | null
|
||||
validated_at: string | null
|
||||
validation_version: number | null
|
||||
}
|
||||
|
||||
export interface RequirementCreateRequest {
|
||||
|
||||
83
frontend/src/services/validationService.ts
Normal file
83
frontend/src/services/validationService.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { ValidationStatus, ValidationHistory, ValidationCreateRequest } from '@/types'
|
||||
|
||||
const API_BASE_URL = '/api'
|
||||
|
||||
class ValidationService {
|
||||
/**
|
||||
* Get all validation statuses from the API.
|
||||
*/
|
||||
async getStatuses(): Promise<ValidationStatus[]> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/validation-statuses`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const statuses: ValidationStatus[] = await response.json()
|
||||
return statuses
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch validation statuses:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a validation for a requirement.
|
||||
*/
|
||||
async createValidation(requirementId: number, data: ValidationCreateRequest): Promise<ValidationHistory> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/requirements/${requirementId}/validations`, {
|
||||
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 validation: ValidationHistory = await response.json()
|
||||
return validation
|
||||
} catch (error) {
|
||||
console.error('Failed to create validation:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation history for a requirement.
|
||||
*/
|
||||
async getValidationHistory(requirementId: number): Promise<ValidationHistory[]> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/requirements/${requirementId}/validations`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const history: ValidationHistory[] = await response.json()
|
||||
return history
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch validation history:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const validationService = new ValidationService()
|
||||
@@ -2,12 +2,15 @@ export interface User {
|
||||
preferred_username: string
|
||||
email: string | null
|
||||
full_name: string | null
|
||||
role: string | null
|
||||
role_id: number | null
|
||||
}
|
||||
|
||||
export interface AuthContextType {
|
||||
user: User | null
|
||||
isLoading: boolean
|
||||
isAuthenticated: boolean
|
||||
isAuditor: boolean
|
||||
login: () => void
|
||||
logout: () => Promise<void>
|
||||
refreshUser: () => Promise<void>
|
||||
|
||||
@@ -1 +1,23 @@
|
||||
export * from './auth'
|
||||
|
||||
// Validation types
|
||||
export interface ValidationStatus {
|
||||
id: number
|
||||
status_name: string
|
||||
}
|
||||
|
||||
export interface ValidationHistory {
|
||||
id: number
|
||||
status_name: string
|
||||
status_id: number
|
||||
req_version_snapshot: number
|
||||
comment: string | null
|
||||
created_at: string | null
|
||||
validator_username: string
|
||||
validator_id: number
|
||||
}
|
||||
|
||||
export interface ValidationCreateRequest {
|
||||
status_id: number
|
||||
comment?: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user