Added status to the requirements

This commit is contained in:
gulimabr
2025-12-02 11:47:45 -03:00
parent 459ceaa162
commit 9428c4d2de
12 changed files with 340 additions and 15 deletions

View File

@@ -104,6 +104,19 @@ class Priority(Base):
requirements: Mapped[List["Requirement"]] = relationship("Requirement", back_populates="priority") requirements: Mapped[List["Requirement"]] = relationship("Requirement", back_populates="priority")
class RequirementStatus(Base):
"""Requirement lifecycle statuses (Draft, Regular, etc.)."""
__tablename__ = "requirement_statuses"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
status_code: Mapped[str] = mapped_column(String(20), nullable=False, unique=True)
status_name: Mapped[str] = mapped_column(Text, nullable=False)
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
# Relationships
requirements: Mapped[List["Requirement"]] = relationship("Requirement", back_populates="status")
class Project(Base): class Project(Base):
"""Projects - containers for requirements that can have multiple members.""" """Projects - containers for requirements that can have multiple members."""
__tablename__ = "projects" __tablename__ = "projects"
@@ -172,6 +185,12 @@ class Requirement(Base):
project_id: Mapped[int] = mapped_column(Integer, ForeignKey("projects.id", ondelete="CASCADE"), nullable=False) project_id: Mapped[int] = mapped_column(Integer, ForeignKey("projects.id", ondelete="CASCADE"), nullable=False)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False) user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False)
tag_id: Mapped[int] = mapped_column(Integer, ForeignKey("tags.id"), nullable=False) tag_id: Mapped[int] = mapped_column(Integer, ForeignKey("tags.id"), nullable=False)
status_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("requirement_statuses.id"),
nullable=False,
default=1 # Default to 'Draft' status
)
last_editor_id: Mapped[Optional[int]] = mapped_column( last_editor_id: Mapped[Optional[int]] = mapped_column(
Integer, Integer,
ForeignKey("users.id"), ForeignKey("users.id"),
@@ -210,6 +229,7 @@ class Requirement(Base):
foreign_keys=[last_editor_id] foreign_keys=[last_editor_id]
) )
tag: Mapped["Tag"] = relationship("Tag", back_populates="requirements") tag: Mapped["Tag"] = relationship("Tag", back_populates="requirements")
status: Mapped["RequirementStatus"] = relationship("RequirementStatus", back_populates="requirements")
priority: Mapped[Optional["Priority"]] = relationship("Priority", back_populates="requirements") priority: Mapped[Optional["Priority"]] = relationship("Priority", back_populates="requirements")
groups: Mapped[List["Group"]] = relationship( groups: Mapped[List["Group"]] = relationship(
"Group", "Group",
@@ -239,6 +259,7 @@ class Requirement(Base):
Index("idx_req_tag", "tag_id"), Index("idx_req_tag", "tag_id"),
Index("idx_req_priority", "priority_id"), Index("idx_req_priority", "priority_id"),
Index("idx_req_user", "user_id"), Index("idx_req_user", "user_id"),
Index("idx_req_status", "status_id"),
) )

View File

@@ -14,7 +14,8 @@ from src.models import (
RelationshipTypeResponse, RelationshipTypeCreateRequest, RelationshipTypeUpdateRequest, RelationshipTypeResponse, RelationshipTypeCreateRequest, RelationshipTypeUpdateRequest,
RequirementLinkResponse, RequirementLinkCreateRequest, RequirementSearchResult, RequirementLinkResponse, RequirementLinkCreateRequest, RequirementSearchResult,
RoleResponse, ProjectMemberResponse, UserRoleUpdateRequest, ROLE_DISPLAY_NAMES, RoleResponse, ProjectMemberResponse, UserRoleUpdateRequest, ROLE_DISPLAY_NAMES,
CommentResponse, CommentReplyResponse, CommentCreateRequest, ReplyCreateRequest CommentResponse, CommentReplyResponse, CommentCreateRequest, ReplyCreateRequest,
RequirementStatusResponse
) )
from src.controller import AuthController from src.controller import AuthController
from src.config import get_openid, get_settings from src.config import get_openid, get_settings
@@ -22,7 +23,8 @@ from src.database import init_db, close_db, get_db
from src.repositories import ( from src.repositories import (
RoleRepository, GroupRepository, TagRepository, RequirementRepository, RoleRepository, GroupRepository, TagRepository, RequirementRepository,
PriorityRepository, ProjectRepository, ValidationStatusRepository, ValidationRepository, PriorityRepository, ProjectRepository, ValidationStatusRepository, ValidationRepository,
RelationshipTypeRepository, RequirementLinkRepository, CommentRepository, ReplyRepository RelationshipTypeRepository, RequirementLinkRepository, CommentRepository, ReplyRepository,
RequirementStatusRepository
) )
import logging import logging
@@ -54,6 +56,13 @@ async def lifespan(app: FastAPI):
await session.commit() await session.commit()
logger.info("Default roles ensured") logger.info("Default roles ensured")
# Ensure default requirement statuses exist
async with AsyncSessionLocal() as session:
req_status_repo = RequirementStatusRepository(session)
await req_status_repo.ensure_default_statuses_exist()
await session.commit()
logger.info("Default requirement statuses ensured")
# Ensure default validation statuses exist # Ensure default validation statuses exist
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
await session.execute( await session.execute(
@@ -277,6 +286,23 @@ async def get_priorities(db: AsyncSession = Depends(get_db)):
return [PriorityResponse.model_validate(p) for p in priorities] return [PriorityResponse.model_validate(p) for p in priorities]
# ===========================================
# Requirement Statuses Endpoints
# ===========================================
@app.get("/api/requirement-statuses", response_model=List[RequirementStatusResponse])
async def get_requirement_statuses(db: AsyncSession = Depends(get_db)):
"""
Get all requirement lifecycle statuses (Draft, Regular, etc.).
Returns:
List of all requirement statuses.
"""
status_repo = RequirementStatusRepository(db)
statuses = await status_repo.get_all()
return [RequirementStatusResponse.model_validate(s) for s in statuses]
# =========================================== # ===========================================
# Projects Endpoints # Projects Endpoints
# =========================================== # ===========================================
@@ -803,6 +829,11 @@ def _build_requirement_response(req) -> RequirementResponse:
if req.last_editor: if req.last_editor:
last_editor_username = _get_display_name(req.last_editor) last_editor_username = _get_display_name(req.last_editor)
# Get requirement lifecycle status
status_response = None
if req.status:
status_response = RequirementStatusResponse.model_validate(req.status)
return RequirementResponse( return RequirementResponse(
id=req.id, id=req.id,
project_id=req.project_id, project_id=req.project_id,
@@ -814,6 +845,7 @@ def _build_requirement_response(req) -> RequirementResponse:
tag=TagResponse.model_validate(req.tag), tag=TagResponse.model_validate(req.tag),
priority=req.priority if req.priority else None, priority=req.priority if req.priority else None,
groups=[GroupResponse.model_validate(g) for g in req.groups], groups=[GroupResponse.model_validate(g) for g in req.groups],
status=status_response,
validation_status=validation_status, validation_status=validation_status,
validated_by=validated_by, validated_by=validated_by,
validated_at=validated_at, validated_at=validated_at,
@@ -966,6 +998,7 @@ async def create_requirement(
req_desc=req_data.req_desc, req_desc=req_data.req_desc,
priority_id=req_data.priority_id, priority_id=req_data.priority_id,
group_ids=req_data.group_ids, group_ids=req_data.group_ids,
status_id=req_data.status_id,
) )
await db.commit() await db.commit()
@@ -1017,6 +1050,7 @@ async def update_requirement(
tag_id=req_data.tag_id, tag_id=req_data.tag_id,
priority_id=req_data.priority_id, priority_id=req_data.priority_id,
group_ids=req_data.group_ids, group_ids=req_data.group_ids,
status_id=req_data.status_id,
) )
await db.commit() await db.commit()

View File

@@ -151,6 +151,18 @@ class PriorityResponse(BaseModel):
from_attributes = True from_attributes = True
# Requirement Status schemas
class RequirementStatusResponse(BaseModel):
"""Response schema for a requirement lifecycle status."""
id: int
status_code: str
status_name: str
description: Optional[str] = None
class Config:
from_attributes = True
# Validation schemas # Validation schemas
class ValidationStatusResponse(BaseModel): class ValidationStatusResponse(BaseModel):
"""Response schema for a validation status.""" """Response schema for a validation status."""
@@ -207,6 +219,9 @@ class RequirementResponse(BaseModel):
tag: TagResponse tag: TagResponse
priority: Optional[PriorityResponse] = None priority: Optional[PriorityResponse] = None
groups: List[GroupResponse] = [] groups: List[GroupResponse] = []
# Requirement lifecycle status (Draft, Regular)
status: Optional[RequirementStatusResponse] = None
# Validation status (Approved, Denied, etc.)
validation_status: Optional[str] = None # Computed from latest validation validation_status: Optional[str] = None # Computed from latest validation
validated_by: Optional[str] = None # Username of the validator validated_by: Optional[str] = None # Username of the validator
validated_at: Optional[datetime] = None # When the latest validation was made validated_at: Optional[datetime] = None # When the latest validation was made
@@ -231,6 +246,7 @@ class RequirementCreateRequest(BaseModel):
req_desc: Optional[str] = None req_desc: Optional[str] = None
priority_id: Optional[int] = None priority_id: Optional[int] = None
group_ids: Optional[List[int]] = None group_ids: Optional[List[int]] = None
status_id: Optional[int] = None # Defaults to Draft (1) if not provided
class RequirementUpdateRequest(BaseModel): class RequirementUpdateRequest(BaseModel):
@@ -240,6 +256,7 @@ class RequirementUpdateRequest(BaseModel):
tag_id: Optional[int] = None tag_id: Optional[int] = None
priority_id: Optional[int] = None priority_id: Optional[int] = None
group_ids: Optional[List[int]] = None group_ids: Optional[List[int]] = None
status_id: Optional[int] = None
class RequirementHistoryResponse(BaseModel): class RequirementHistoryResponse(BaseModel):

View File

@@ -12,6 +12,7 @@ from src.repositories.validation_repository import ValidationRepository
from src.repositories.relationship_type_repository import RelationshipTypeRepository from src.repositories.relationship_type_repository import RelationshipTypeRepository
from src.repositories.requirement_link_repository import RequirementLinkRepository from src.repositories.requirement_link_repository import RequirementLinkRepository
from src.repositories.comment_repository import CommentRepository, ReplyRepository from src.repositories.comment_repository import CommentRepository, ReplyRepository
from src.repositories.requirement_status_repository import RequirementStatusRepository
__all__ = [ __all__ = [
"UserRepository", "UserRepository",
@@ -27,4 +28,5 @@ __all__ = [
"RequirementLinkRepository", "RequirementLinkRepository",
"CommentRepository", "CommentRepository",
"ReplyRepository", "ReplyRepository",
"RequirementStatusRepository",
] ]

View File

@@ -5,7 +5,7 @@ from typing import List, Optional, Dict, Any
from sqlalchemy import select, text from sqlalchemy import select, text
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from src.db_models import Requirement, Group, Tag, Priority, Validation, RequirementHistory from src.db_models import Requirement, Group, Tag, Priority, Validation, RequirementHistory, RequirementStatus
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -19,7 +19,7 @@ class RequirementRepository:
async def get_all(self) -> List[Requirement]: async def get_all(self) -> List[Requirement]:
""" """
Get all requirements with their related data (tag, priority, groups, validations). Get all requirements with their related data (tag, priority, groups, validations, status).
Returns: Returns:
List of all requirements with eager-loaded relationships List of all requirements with eager-loaded relationships
@@ -30,6 +30,7 @@ class RequirementRepository:
selectinload(Requirement.tag), selectinload(Requirement.tag),
selectinload(Requirement.priority), selectinload(Requirement.priority),
selectinload(Requirement.groups), selectinload(Requirement.groups),
selectinload(Requirement.status),
selectinload(Requirement.validations).selectinload(Validation.status), selectinload(Requirement.validations).selectinload(Validation.status),
selectinload(Requirement.validations).selectinload(Validation.user), selectinload(Requirement.validations).selectinload(Validation.user),
selectinload(Requirement.user), selectinload(Requirement.user),
@@ -55,6 +56,7 @@ class RequirementRepository:
selectinload(Requirement.tag), selectinload(Requirement.tag),
selectinload(Requirement.priority), selectinload(Requirement.priority),
selectinload(Requirement.groups), selectinload(Requirement.groups),
selectinload(Requirement.status),
selectinload(Requirement.validations).selectinload(Validation.status), selectinload(Requirement.validations).selectinload(Validation.status),
selectinload(Requirement.validations).selectinload(Validation.user), selectinload(Requirement.validations).selectinload(Validation.user),
selectinload(Requirement.user), selectinload(Requirement.user),
@@ -81,6 +83,7 @@ class RequirementRepository:
selectinload(Requirement.tag), selectinload(Requirement.tag),
selectinload(Requirement.priority), selectinload(Requirement.priority),
selectinload(Requirement.groups), selectinload(Requirement.groups),
selectinload(Requirement.status),
selectinload(Requirement.validations).selectinload(Validation.status), selectinload(Requirement.validations).selectinload(Validation.status),
selectinload(Requirement.validations).selectinload(Validation.user), selectinload(Requirement.validations).selectinload(Validation.user),
selectinload(Requirement.user), selectinload(Requirement.user),
@@ -107,6 +110,7 @@ class RequirementRepository:
selectinload(Requirement.tag), selectinload(Requirement.tag),
selectinload(Requirement.priority), selectinload(Requirement.priority),
selectinload(Requirement.groups), selectinload(Requirement.groups),
selectinload(Requirement.status),
selectinload(Requirement.validations).selectinload(Validation.status), selectinload(Requirement.validations).selectinload(Validation.status),
selectinload(Requirement.validations).selectinload(Validation.user), selectinload(Requirement.validations).selectinload(Validation.user),
selectinload(Requirement.user), selectinload(Requirement.user),
@@ -134,6 +138,7 @@ class RequirementRepository:
selectinload(Requirement.tag), selectinload(Requirement.tag),
selectinload(Requirement.priority), selectinload(Requirement.priority),
selectinload(Requirement.groups), selectinload(Requirement.groups),
selectinload(Requirement.status),
selectinload(Requirement.validations).selectinload(Validation.status), selectinload(Requirement.validations).selectinload(Validation.status),
selectinload(Requirement.validations).selectinload(Validation.user), selectinload(Requirement.validations).selectinload(Validation.user),
selectinload(Requirement.user), selectinload(Requirement.user),
@@ -171,6 +176,7 @@ class RequirementRepository:
selectinload(Requirement.tag), selectinload(Requirement.tag),
selectinload(Requirement.priority), selectinload(Requirement.priority),
selectinload(Requirement.groups), selectinload(Requirement.groups),
selectinload(Requirement.status),
selectinload(Requirement.validations).selectinload(Validation.status), selectinload(Requirement.validations).selectinload(Validation.status),
selectinload(Requirement.validations).selectinload(Validation.user), selectinload(Requirement.validations).selectinload(Validation.user),
selectinload(Requirement.user), selectinload(Requirement.user),
@@ -198,6 +204,7 @@ class RequirementRepository:
req_desc: Optional[str] = None, req_desc: Optional[str] = None,
priority_id: Optional[int] = None, priority_id: Optional[int] = None,
group_ids: Optional[List[int]] = None, group_ids: Optional[List[int]] = None,
status_id: Optional[int] = None,
) -> Requirement: ) -> Requirement:
""" """
Create a new requirement. Create a new requirement.
@@ -210,6 +217,7 @@ class RequirementRepository:
req_desc: The requirement description (optional) req_desc: The requirement description (optional)
priority_id: The priority ID (optional) priority_id: The priority ID (optional)
group_ids: List of group IDs to associate (optional) group_ids: List of group IDs to associate (optional)
status_id: The requirement status ID (optional, defaults to 1=Draft)
Returns: Returns:
The created Requirement The created Requirement
@@ -221,6 +229,7 @@ class RequirementRepository:
req_name=req_name, req_name=req_name,
req_desc=req_desc, req_desc=req_desc,
priority_id=priority_id, priority_id=priority_id,
status_id=status_id if status_id else 1, # Default to Draft
) )
# Add groups if provided # Add groups if provided
@@ -247,6 +256,7 @@ class RequirementRepository:
tag_id: Optional[int] = None, tag_id: Optional[int] = None,
priority_id: Optional[int] = None, priority_id: Optional[int] = None,
group_ids: Optional[List[int]] = None, group_ids: Optional[List[int]] = None,
status_id: Optional[int] = None,
) -> Optional[Requirement]: ) -> Optional[Requirement]:
""" """
Update an existing requirement. Update an existing requirement.
@@ -259,6 +269,7 @@ class RequirementRepository:
tag_id: New tag ID (optional) tag_id: New tag ID (optional)
priority_id: New priority ID (optional) priority_id: New priority ID (optional)
group_ids: New list of group IDs (optional) group_ids: New list of group IDs (optional)
status_id: New status ID (optional)
Returns: Returns:
The updated Requirement, or None if not found The updated Requirement, or None if not found
@@ -276,6 +287,8 @@ class RequirementRepository:
requirement.tag_id = tag_id requirement.tag_id = tag_id
if priority_id is not None: if priority_id is not None:
requirement.priority_id = priority_id requirement.priority_id = priority_id
if status_id is not None:
requirement.status_id = status_id
# Set the last editor # Set the last editor
requirement.last_editor_id = editor_id requirement.last_editor_id = editor_id

View File

@@ -0,0 +1,102 @@
"""
Repository layer for RequirementStatus database operations.
"""
from typing import List, Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src.db_models import RequirementStatus
import logging
logger = logging.getLogger(__name__)
class RequirementStatusRepository:
"""Repository for RequirementStatus-related database operations."""
def __init__(self, session: AsyncSession):
self.session = session
async def get_all(self) -> List[RequirementStatus]:
"""
Get all requirement statuses.
Returns:
List of all requirement statuses
"""
result = await self.session.execute(
select(RequirementStatus).order_by(RequirementStatus.id)
)
return list(result.scalars().all())
async def get_by_id(self, status_id: int) -> Optional[RequirementStatus]:
"""
Get a requirement status by ID.
Args:
status_id: The status ID
Returns:
RequirementStatus if found, None otherwise
"""
result = await self.session.execute(
select(RequirementStatus).where(RequirementStatus.id == status_id)
)
return result.scalar_one_or_none()
async def get_by_code(self, status_code: str) -> Optional[RequirementStatus]:
"""
Get a requirement status by code.
Args:
status_code: The status code (e.g., 'DRAFT', 'REGULAR')
Returns:
RequirementStatus if found, None otherwise
"""
result = await self.session.execute(
select(RequirementStatus).where(RequirementStatus.status_code == status_code)
)
return result.scalar_one_or_none()
async def create(
self,
status_code: str,
status_name: str,
description: Optional[str] = None
) -> RequirementStatus:
"""
Create a new requirement status.
Args:
status_code: The unique status code
status_name: The display name
description: Optional description
Returns:
The created RequirementStatus
"""
status = RequirementStatus(
status_code=status_code,
status_name=status_name,
description=description
)
self.session.add(status)
await self.session.flush()
await self.session.refresh(status)
return status
async def ensure_default_statuses_exist(self) -> None:
"""
Ensure default requirement statuses exist in the database.
Called during application startup.
"""
default_statuses = [
("DRAFT", "Draft", "Initial version, not ready for review"),
("REGULAR", "Regular", "Active requirement"),
]
for status_code, status_name, description in default_statuses:
existing = await self.get_by_code(status_code)
if existing is None:
logger.info(f"Creating default requirement status: {status_code}")
await self.create(status_code, status_name, description)

View File

@@ -1,13 +1,13 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useAuth, useProject } from '@/hooks' import { useAuth, useProject } from '@/hooks'
import { useParams, Link } from 'react-router-dom' import { useParams, Link } from 'react-router-dom'
import { requirementService, validationService, relationshipService, commentService, tagService, priorityService, groupService } from '@/services' import { requirementService, validationService, relationshipService, commentService, tagService, priorityService, groupService, requirementStatusService } from '@/services'
import type { Requirement } from '@/services/requirementService' import type { Requirement } from '@/services/requirementService'
import type { RelationshipType, RequirementLink, RequirementSearchResult } from '@/services/relationshipService' import type { RelationshipType, RequirementLink, RequirementSearchResult } from '@/services/relationshipService'
import type { Tag } from '@/services/tagService' import type { Tag } from '@/services/tagService'
import type { Priority } from '@/services/priorityService' import type { Priority } from '@/services/priorityService'
import type { Group } from '@/services/groupService' import type { Group } from '@/services/groupService'
import type { ValidationStatus, ValidationHistory, Comment, RequirementHistory } from '@/types' import type { ValidationStatus, ValidationHistory, Comment, RequirementHistory, RequirementStatus } from '@/types'
// Tab types // Tab types
type TabType = 'description' | 'relationships' | 'acceptance-criteria' | 'shared-comments' | 'validate' | 'history' type TabType = 'description' | 'relationships' | 'acceptance-criteria' | 'shared-comments' | 'validate' | 'history'
@@ -64,9 +64,11 @@ export default function RequirementDetailPage() {
const [editTagId, setEditTagId] = useState<number | ''>('') const [editTagId, setEditTagId] = useState<number | ''>('')
const [editPriorityId, setEditPriorityId] = useState<number | ''>('') const [editPriorityId, setEditPriorityId] = useState<number | ''>('')
const [editGroupIds, setEditGroupIds] = useState<number[]>([]) const [editGroupIds, setEditGroupIds] = useState<number[]>([])
const [editStatusId, setEditStatusId] = useState<number | ''>(1)
const [availableTags, setAvailableTags] = useState<Tag[]>([]) const [availableTags, setAvailableTags] = useState<Tag[]>([])
const [availablePriorities, setAvailablePriorities] = useState<Priority[]>([]) const [availablePriorities, setAvailablePriorities] = useState<Priority[]>([])
const [availableGroups, setAvailableGroups] = useState<Group[]>([]) const [availableGroups, setAvailableGroups] = useState<Group[]>([])
const [availableStatuses, setAvailableStatuses] = useState<RequirementStatus[]>([])
const [editOptionsLoading, setEditOptionsLoading] = useState(false) const [editOptionsLoading, setEditOptionsLoading] = useState(false)
// Requirement history state // Requirement history state
@@ -429,21 +431,24 @@ export default function RequirementDetailPage() {
setEditTagId(requirement.tag.id) setEditTagId(requirement.tag.id)
setEditPriorityId(requirement.priority?.id || '') setEditPriorityId(requirement.priority?.id || '')
setEditGroupIds(requirement.groups.map(g => g.id)) setEditGroupIds(requirement.groups.map(g => g.id))
setEditStatusId(requirement.status?.id || 1)
setEditError(null) setEditError(null)
setShowEditModal(true) setShowEditModal(true)
// Fetch options if not already loaded // Fetch options if not already loaded
if (availableTags.length === 0 || availablePriorities.length === 0 || availableGroups.length === 0) { if (availableTags.length === 0 || availablePriorities.length === 0 || availableGroups.length === 0 || availableStatuses.length === 0) {
try { try {
setEditOptionsLoading(true) setEditOptionsLoading(true)
const [tags, priorities, groups] = await Promise.all([ const [tags, priorities, groups, statuses] = await Promise.all([
tagService.getTags(), tagService.getTags(),
priorityService.getPriorities(), priorityService.getPriorities(),
groupService.getGroups() groupService.getGroups(),
requirementStatusService.getStatuses()
]) ])
setAvailableTags(tags) setAvailableTags(tags)
setAvailablePriorities(priorities) setAvailablePriorities(priorities)
setAvailableGroups(groups) setAvailableGroups(groups)
setAvailableStatuses(statuses)
} catch (err) { } catch (err) {
console.error('Failed to fetch edit options:', err) console.error('Failed to fetch edit options:', err)
setEditError('Failed to load options. Please try again.') setEditError('Failed to load options. Please try again.')
@@ -475,7 +480,8 @@ export default function RequirementDetailPage() {
req_desc: editReqDesc.trim() || undefined, req_desc: editReqDesc.trim() || undefined,
tag_id: editTagId as number, tag_id: editTagId as number,
priority_id: editPriorityId ? editPriorityId as number : undefined, priority_id: editPriorityId ? editPriorityId as number : undefined,
group_ids: editGroupIds group_ids: editGroupIds,
status_id: editStatusId ? editStatusId as number : undefined,
}) })
// Refetch to ensure consistency (trigger updates version) // Refetch to ensure consistency (trigger updates version)
@@ -515,6 +521,9 @@ export default function RequirementDetailPage() {
} }
} }
// Check if requirement is in draft status
const isDraftStatus = requirement?.status?.status_code === 'DRAFT'
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-white flex items-center justify-center"> <div className="min-h-screen bg-white flex items-center justify-center">
@@ -561,9 +570,33 @@ export default function RequirementDetailPage() {
return ( return (
<div> <div>
<h3 className="text-xl font-bold text-gray-800 mb-2">Description</h3> <h3 className="text-xl font-bold text-gray-800 mb-2">Description</h3>
{/* Draft indicator banner */}
{isDraftStatus && (
<div className="mb-4 p-3 bg-amber-50 border border-amber-300 border-dashed rounded-lg">
<div className="flex items-center gap-2">
<span className="text-amber-600 text-lg">📝</span>
<div>
<p className="font-semibold text-amber-800">Draft Requirement</p>
<p className="text-sm text-amber-700">This requirement is still in draft status and is not finalized. It may be subject to changes.</p>
</div>
</div>
</div>
)}
<p className="text-sm text-gray-700 mb-2"> <p className="text-sm text-gray-700 mb-2">
<span className="font-semibold">Version:</span> {requirement.version} <span className="font-semibold">Version:</span> {requirement.version}
</p> </p>
<p className="text-sm text-gray-700 mb-2">
<span className="font-semibold">Status:</span>{' '}
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
isDraftStatus
? 'bg-amber-100 text-amber-800 border border-amber-300'
: 'bg-blue-100 text-blue-800'
}`}>
{requirement.status?.status_name || 'Unknown'}
</span>
</p>
<p className="text-sm text-gray-700 mb-2"> <p className="text-sm text-gray-700 mb-2">
<span className="font-semibold">Validation Status:</span>{' '} <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)}`}> <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getValidationStatusStyle(validationStatus)}`}>
@@ -1220,6 +1253,14 @@ export default function RequirementDetailPage() {
<div className="max-w-5xl mx-auto px-8 py-8"> <div className="max-w-5xl mx-auto px-8 py-8">
{/* Requirement Header */} {/* Requirement Header */}
<div className="text-center mb-8"> <div className="text-center mb-8">
{/* Draft Status Indicator */}
{isDraftStatus && (
<div className="mb-4 inline-flex items-center gap-2 px-4 py-2 bg-amber-100 border-2 border-dashed border-amber-400 rounded-lg">
<span className="text-amber-600 text-lg">📝</span>
<span className="text-amber-800 font-medium">Draft - Not Finalized</span>
</div>
)}
{/* Tag */} {/* Tag */}
<h2 className="text-2xl font-bold text-gray-800 mb-2">{tagCode}</h2> <h2 className="text-2xl font-bold text-gray-800 mb-2">{tagCode}</h2>
@@ -1491,6 +1532,28 @@ export default function RequirementDetailPage() {
</select> </select>
</div> </div>
{/* Status */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Status
</label>
<select
value={editStatusId}
onChange={(e) => setEditStatusId(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={editLoading}
>
{availableStatuses.map((status) => (
<option key={status.id} value={status.id}>
{status.status_name} {status.description ? `- ${status.description}` : ''}
</option>
))}
</select>
<p className="mt-1 text-xs text-gray-500">
Draft requirements are not finalized and marked with a visual indicator.
</p>
</div>
{/* Description */} {/* Description */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">

View File

@@ -22,6 +22,11 @@ const getValidationStatusStyle = (status: string): { bgColor: string; textColor:
} }
} }
// Check if requirement is in draft status
const isDraftStatus = (statusCode: string | undefined): boolean => {
return statusCode === 'DRAFT'
}
export default function RequirementsPage() { export default function RequirementsPage() {
const { user, logout, isAuditor } = useAuth() const { user, logout, isAuditor } = useAuth()
const { currentProject, isLoading: projectLoading } = useProject() const { currentProject, isLoading: projectLoading } = useProject()
@@ -426,17 +431,32 @@ export default function RequirementsPage() {
const validationStatus = req.validation_status || 'Not Validated' const validationStatus = req.validation_status || 'Not Validated'
const validationStyle = getValidationStatusStyle(validationStatus) const validationStyle = getValidationStatusStyle(validationStatus)
const isStale = req.validation_version !== null && req.validation_version !== req.version const isStale = req.validation_version !== null && req.validation_version !== req.version
const isDraft = isDraftStatus(req.status?.status_code)
return ( return (
<div <div
key={req.id} key={req.id}
className="flex items-center rounded overflow-hidden border border-gray-300 bg-white" className={`flex items-center rounded overflow-hidden border bg-white ${
isDraft
? 'border-amber-400 border-dashed border-2'
: 'border-gray-300'
}`}
> >
{/* Tag and name section */} {/* Tag and name section */}
<div className="px-4 py-4 min-w-[280px]"> <div className="px-4 py-4 min-w-[280px]">
<span className="font-bold text-gray-800"> <div className="flex items-center gap-2">
{tagLabel} - {req.req_name} {isDraft && (
</span> <span
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800 border border-amber-300"
title="This requirement is still in draft and not finalized"
>
📝 Draft
</span>
)}
<span className="font-bold text-gray-800">
{tagLabel} - {req.req_name}
</span>
</div>
</div> </div>
{/* Group chips */} {/* Group chips */}

View File

@@ -7,6 +7,7 @@ export { priorityService } from './priorityService'
export type { Priority } from './priorityService' export type { Priority } from './priorityService'
export { requirementService } from './requirementService' export { requirementService } from './requirementService'
export type { Requirement, RequirementCreateRequest, RequirementUpdateRequest } from './requirementService' export type { Requirement, RequirementCreateRequest, RequirementUpdateRequest } from './requirementService'
export { requirementStatusService } from './requirementStatusService'
export { projectService } from './projectService' export { projectService } from './projectService'
export type { Project, ProjectCreateRequest, ProjectUpdateRequest } from './projectService' export type { Project, ProjectCreateRequest, ProjectUpdateRequest } from './projectService'
export { validationService } from './validationService' export { validationService } from './validationService'

View File

@@ -1,7 +1,7 @@
import { Group } from './groupService' import { Group } from './groupService'
import { Tag } from './tagService' import { Tag } from './tagService'
import { Priority } from './priorityService' import { Priority } from './priorityService'
import type { RequirementHistory } from '@/types' import type { RequirementHistory, RequirementStatus } from '@/types'
const API_BASE_URL = '/api' const API_BASE_URL = '/api'
@@ -16,6 +16,9 @@ export interface Requirement {
tag: Tag tag: Tag
priority: Priority | null priority: Priority | null
groups: Group[] groups: Group[]
// Requirement lifecycle status (Draft, Regular)
status: RequirementStatus | null
// Validation status (Approved, Denied, etc.)
validation_status: string | null validation_status: string | null
validated_by: string | null validated_by: string | null
validated_at: string | null validated_at: string | null
@@ -31,6 +34,7 @@ export interface RequirementCreateRequest {
req_desc?: string req_desc?: string
priority_id?: number priority_id?: number
group_ids?: number[] group_ids?: number[]
status_id?: number
} }
export interface RequirementUpdateRequest { export interface RequirementUpdateRequest {
@@ -39,6 +43,7 @@ export interface RequirementUpdateRequest {
tag_id?: number tag_id?: number
priority_id?: number priority_id?: number
group_ids?: number[] group_ids?: number[]
status_id?: number
} }
class RequirementService { class RequirementService {

View File

@@ -0,0 +1,39 @@
import type { RequirementStatus } from '@/types'
const API_BASE_URL = '/api'
class RequirementStatusService {
/**
* Get all requirement lifecycle statuses.
*/
async getStatuses(): Promise<RequirementStatus[]> {
try {
const response = await fetch(`${API_BASE_URL}/requirement-statuses`, {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const statuses: RequirementStatus[] = await response.json()
return statuses
} catch (error) {
console.error('Failed to fetch requirement statuses:', error)
throw error
}
}
/**
* Check if a requirement is in draft status.
*/
isDraft(status: RequirementStatus | null): boolean {
return status?.status_code === 'DRAFT'
}
}
export const requirementStatusService = new RequirementStatusService()

View File

@@ -1,5 +1,13 @@
export * from './auth' export * from './auth'
// Requirement Status types (lifecycle status)
export interface RequirementStatus {
id: number
status_code: string
status_name: string
description: string | null
}
// Validation types // Validation types
export interface ValidationStatus { export interface ValidationStatus {
id: number id: number