Added functions to fetch requirements from db.
Added functionality to create requirements on page
This commit is contained in:
@@ -1,15 +1,19 @@
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import List
|
from typing import List, Optional
|
||||||
from fastapi import FastAPI, Depends, Request
|
from fastapi import FastAPI, Depends, Request, HTTPException, status
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from src.models import TokenResponse, UserInfo, GroupResponse
|
from src.models import (
|
||||||
|
TokenResponse, UserInfo, GroupResponse,
|
||||||
|
TagResponse, RequirementResponse, PriorityResponse,
|
||||||
|
RequirementCreateRequest, RequirementUpdateRequest
|
||||||
|
)
|
||||||
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
|
||||||
from src.database import init_db, close_db, get_db
|
from src.database import init_db, close_db, get_db
|
||||||
from src.repositories import RoleRepository, GroupRepository
|
from src.repositories import RoleRepository, GroupRepository, TagRepository, RequirementRepository, PriorityRepository
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
@@ -176,3 +180,254 @@ async def get_groups(db: AsyncSession = Depends(get_db)):
|
|||||||
group_repo = GroupRepository(db)
|
group_repo = GroupRepository(db)
|
||||||
groups = await group_repo.get_all()
|
groups = await group_repo.get_all()
|
||||||
return [GroupResponse.model_validate(g) for g in groups]
|
return [GroupResponse.model_validate(g) for g in groups]
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# Tags Endpoints
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
@app.get("/api/tags", response_model=List[TagResponse])
|
||||||
|
async def get_tags(db: AsyncSession = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Get all tags.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of all tags with their codes and descriptions.
|
||||||
|
"""
|
||||||
|
tag_repo = TagRepository(db)
|
||||||
|
tags = await tag_repo.get_all()
|
||||||
|
return [TagResponse.model_validate(t) for t in tags]
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/tags/{tag_id}", response_model=TagResponse)
|
||||||
|
async def get_tag(tag_id: int, db: AsyncSession = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Get a specific tag by ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tag_id: The tag ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The tag if found.
|
||||||
|
"""
|
||||||
|
tag_repo = TagRepository(db)
|
||||||
|
tag = await tag_repo.get_by_id(tag_id)
|
||||||
|
if not tag:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Tag with id {tag_id} not found"
|
||||||
|
)
|
||||||
|
return TagResponse.model_validate(tag)
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# Priorities Endpoints
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
@app.get("/api/priorities", response_model=List[PriorityResponse])
|
||||||
|
async def get_priorities(db: AsyncSession = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Get all priorities.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of all priorities ordered by priority_num.
|
||||||
|
"""
|
||||||
|
priority_repo = PriorityRepository(db)
|
||||||
|
priorities = await priority_repo.get_all()
|
||||||
|
return [PriorityResponse.model_validate(p) for p in priorities]
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# Requirements Endpoints
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
def _build_requirement_response(req) -> RequirementResponse:
|
||||||
|
"""Helper function to build RequirementResponse from a Requirement model."""
|
||||||
|
# Determine validation status from latest validation
|
||||||
|
validation_status = "Not Validated"
|
||||||
|
if req.validations:
|
||||||
|
# Get the latest validation
|
||||||
|
latest_validation = max(req.validations, key=lambda v: v.created_at or req.created_at)
|
||||||
|
validation_status = latest_validation.status.status_name if latest_validation.status else "Not Validated"
|
||||||
|
|
||||||
|
return RequirementResponse(
|
||||||
|
id=req.id,
|
||||||
|
req_name=req.req_name,
|
||||||
|
req_desc=req.req_desc,
|
||||||
|
version=req.version,
|
||||||
|
created_at=req.created_at,
|
||||||
|
updated_at=req.updated_at,
|
||||||
|
tag=TagResponse.model_validate(req.tag),
|
||||||
|
priority=req.priority if req.priority else None,
|
||||||
|
groups=[GroupResponse.model_validate(g) for g in req.groups],
|
||||||
|
validation_status=validation_status,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/requirements", response_model=List[RequirementResponse])
|
||||||
|
async def get_requirements(
|
||||||
|
group_id: Optional[int] = None,
|
||||||
|
tag_id: Optional[int] = None,
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get all requirements, optionally filtered by group or tag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
group_id: Optional group ID to filter by
|
||||||
|
tag_id: Optional tag ID to filter by
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of all requirements with their related data.
|
||||||
|
"""
|
||||||
|
req_repo = RequirementRepository(db)
|
||||||
|
|
||||||
|
if group_id:
|
||||||
|
requirements = await req_repo.get_by_group_id(group_id)
|
||||||
|
elif tag_id:
|
||||||
|
requirements = await req_repo.get_by_tag_id(tag_id)
|
||||||
|
else:
|
||||||
|
requirements = await req_repo.get_all()
|
||||||
|
|
||||||
|
return [_build_requirement_response(req) for req in requirements]
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/requirements/{requirement_id}", response_model=RequirementResponse)
|
||||||
|
async def get_requirement(requirement_id: int, db: AsyncSession = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Get a specific requirement by ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
requirement_id: The requirement ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The requirement if found.
|
||||||
|
"""
|
||||||
|
req_repo = RequirementRepository(db)
|
||||||
|
requirement = await req_repo.get_by_id(requirement_id)
|
||||||
|
if not requirement:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Requirement with id {requirement_id} not found"
|
||||||
|
)
|
||||||
|
return _build_requirement_response(requirement)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/requirements", response_model=RequirementResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_requirement(
|
||||||
|
request: Request,
|
||||||
|
req_data: RequirementCreateRequest,
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Create a new requirement.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
req_data: The requirement data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created requirement.
|
||||||
|
"""
|
||||||
|
# Get the current user from cookie
|
||||||
|
user_info = AuthController.get_current_user(request)
|
||||||
|
|
||||||
|
# Get the user's database ID
|
||||||
|
from src.repositories import UserRepository
|
||||||
|
user_repo = UserRepository(db)
|
||||||
|
user = await user_repo.get_by_sub(user_info.sub)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="User not found in database"
|
||||||
|
)
|
||||||
|
|
||||||
|
req_repo = RequirementRepository(db)
|
||||||
|
requirement = await req_repo.create(
|
||||||
|
user_id=user.id,
|
||||||
|
tag_id=req_data.tag_id,
|
||||||
|
req_name=req_data.req_name,
|
||||||
|
req_desc=req_data.req_desc,
|
||||||
|
priority_id=req_data.priority_id,
|
||||||
|
group_ids=req_data.group_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
return _build_requirement_response(requirement)
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/api/requirements/{requirement_id}", response_model=RequirementResponse)
|
||||||
|
async def update_requirement(
|
||||||
|
requirement_id: int,
|
||||||
|
request: Request,
|
||||||
|
req_data: RequirementUpdateRequest,
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Update an existing requirement.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
requirement_id: The requirement ID to update
|
||||||
|
req_data: The updated requirement data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The updated requirement.
|
||||||
|
"""
|
||||||
|
# Get the current user from cookie
|
||||||
|
user_info = AuthController.get_current_user(request)
|
||||||
|
|
||||||
|
# Get the user's database ID
|
||||||
|
from src.repositories import UserRepository
|
||||||
|
user_repo = UserRepository(db)
|
||||||
|
user = await user_repo.get_by_sub(user_info.sub)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="User not found in database"
|
||||||
|
)
|
||||||
|
|
||||||
|
req_repo = RequirementRepository(db)
|
||||||
|
requirement = await req_repo.update(
|
||||||
|
requirement_id=requirement_id,
|
||||||
|
editor_id=user.id,
|
||||||
|
req_name=req_data.req_name,
|
||||||
|
req_desc=req_data.req_desc,
|
||||||
|
tag_id=req_data.tag_id,
|
||||||
|
priority_id=req_data.priority_id,
|
||||||
|
group_ids=req_data.group_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not requirement:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Requirement with id {requirement_id} not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
return _build_requirement_response(requirement)
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/requirements/{requirement_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def delete_requirement(
|
||||||
|
requirement_id: int,
|
||||||
|
request: Request,
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Delete a requirement.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
requirement_id: The requirement ID to delete
|
||||||
|
"""
|
||||||
|
# Verify user is authenticated
|
||||||
|
AuthController.get_current_user(request)
|
||||||
|
|
||||||
|
req_repo = RequirementRepository(db)
|
||||||
|
deleted = await req_repo.delete(requirement_id)
|
||||||
|
|
||||||
|
if not deleted:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Requirement with id {requirement_id} not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
|
from datetime import datetime
|
||||||
from pydantic import BaseModel, SecretStr
|
from pydantic import BaseModel, SecretStr
|
||||||
|
|
||||||
|
|
||||||
@@ -35,3 +36,84 @@ class GroupResponse(BaseModel):
|
|||||||
class GroupListResponse(BaseModel):
|
class GroupListResponse(BaseModel):
|
||||||
"""Response schema for list of groups."""
|
"""Response schema for list of groups."""
|
||||||
groups: List[GroupResponse]
|
groups: List[GroupResponse]
|
||||||
|
|
||||||
|
|
||||||
|
# Tag schemas
|
||||||
|
class TagResponse(BaseModel):
|
||||||
|
"""Response schema for a single tag."""
|
||||||
|
id: int
|
||||||
|
tag_code: str
|
||||||
|
tag_description: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class TagListResponse(BaseModel):
|
||||||
|
"""Response schema for list of tags."""
|
||||||
|
tags: List[TagResponse]
|
||||||
|
|
||||||
|
|
||||||
|
# Priority schemas
|
||||||
|
class PriorityResponse(BaseModel):
|
||||||
|
"""Response schema for a priority."""
|
||||||
|
id: int
|
||||||
|
priority_name: str
|
||||||
|
priority_num: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
# Validation schemas
|
||||||
|
class ValidationResponse(BaseModel):
|
||||||
|
"""Response schema for a validation."""
|
||||||
|
id: int
|
||||||
|
status_name: str
|
||||||
|
req_version_snapshot: int
|
||||||
|
comment: Optional[str] = None
|
||||||
|
created_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
# Requirement schemas
|
||||||
|
class RequirementResponse(BaseModel):
|
||||||
|
"""Response schema for a single requirement."""
|
||||||
|
id: int
|
||||||
|
req_name: str
|
||||||
|
req_desc: Optional[str] = None
|
||||||
|
version: int
|
||||||
|
created_at: Optional[datetime] = None
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
tag: TagResponse
|
||||||
|
priority: Optional[PriorityResponse] = None
|
||||||
|
groups: List[GroupResponse] = []
|
||||||
|
validation_status: Optional[str] = None # Computed from latest validation
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class RequirementListResponse(BaseModel):
|
||||||
|
"""Response schema for list of requirements."""
|
||||||
|
requirements: List[RequirementResponse]
|
||||||
|
|
||||||
|
|
||||||
|
class RequirementCreateRequest(BaseModel):
|
||||||
|
"""Request schema for creating a requirement."""
|
||||||
|
tag_id: int
|
||||||
|
req_name: str
|
||||||
|
req_desc: Optional[str] = None
|
||||||
|
priority_id: Optional[int] = None
|
||||||
|
group_ids: Optional[List[int]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class RequirementUpdateRequest(BaseModel):
|
||||||
|
"""Request schema for updating a requirement."""
|
||||||
|
req_name: Optional[str] = None
|
||||||
|
req_desc: Optional[str] = None
|
||||||
|
tag_id: Optional[int] = None
|
||||||
|
priority_id: Optional[int] = None
|
||||||
|
group_ids: Optional[List[int]] = None
|
||||||
|
|||||||
@@ -3,9 +3,15 @@ Repository layer for database operations.
|
|||||||
"""
|
"""
|
||||||
from src.repositories.user_repository import UserRepository, RoleRepository
|
from src.repositories.user_repository import UserRepository, RoleRepository
|
||||||
from src.repositories.group_repository import GroupRepository
|
from src.repositories.group_repository import GroupRepository
|
||||||
|
from src.repositories.tag_repository import TagRepository
|
||||||
|
from src.repositories.requirement_repository import RequirementRepository
|
||||||
|
from src.repositories.priority_repository import PriorityRepository
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"UserRepository",
|
"UserRepository",
|
||||||
"RoleRepository",
|
"RoleRepository",
|
||||||
"GroupRepository",
|
"GroupRepository",
|
||||||
|
"TagRepository",
|
||||||
|
"RequirementRepository",
|
||||||
|
"PriorityRepository",
|
||||||
]
|
]
|
||||||
|
|||||||
76
backend/src/repositories/priority_repository.py
Normal file
76
backend/src/repositories/priority_repository.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
"""
|
||||||
|
Repository layer for Priority database operations.
|
||||||
|
"""
|
||||||
|
from typing import List, Optional
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from src.db_models import Priority
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PriorityRepository:
|
||||||
|
"""Repository for Priority-related database operations."""
|
||||||
|
|
||||||
|
def __init__(self, session: AsyncSession):
|
||||||
|
self.session = session
|
||||||
|
|
||||||
|
async def get_all(self) -> List[Priority]:
|
||||||
|
"""
|
||||||
|
Get all priorities ordered by priority_num.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of all priorities
|
||||||
|
"""
|
||||||
|
result = await self.session.execute(
|
||||||
|
select(Priority).order_by(Priority.priority_num)
|
||||||
|
)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
async def get_by_id(self, priority_id: int) -> Optional[Priority]:
|
||||||
|
"""
|
||||||
|
Get a priority by ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
priority_id: The priority ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Priority if found, None otherwise
|
||||||
|
"""
|
||||||
|
result = await self.session.execute(
|
||||||
|
select(Priority).where(Priority.id == priority_id)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
async def get_by_name(self, priority_name: str) -> Optional[Priority]:
|
||||||
|
"""
|
||||||
|
Get a priority by name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
priority_name: The priority name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Priority if found, None otherwise
|
||||||
|
"""
|
||||||
|
result = await self.session.execute(
|
||||||
|
select(Priority).where(Priority.priority_name == priority_name)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
async def create(self, priority_name: str, priority_num: int) -> Priority:
|
||||||
|
"""
|
||||||
|
Create a new priority.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
priority_name: The priority name
|
||||||
|
priority_num: The priority number for ordering
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created Priority
|
||||||
|
"""
|
||||||
|
priority = Priority(priority_name=priority_name, priority_num=priority_num)
|
||||||
|
self.session.add(priority)
|
||||||
|
await self.session.flush()
|
||||||
|
await self.session.refresh(priority)
|
||||||
|
return priority
|
||||||
227
backend/src/repositories/requirement_repository.py
Normal file
227
backend/src/repositories/requirement_repository.py
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
"""
|
||||||
|
Repository layer for Requirement database operations.
|
||||||
|
"""
|
||||||
|
from typing import List, Optional
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from src.db_models import Requirement, Group, Tag, Priority, Validation
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RequirementRepository:
|
||||||
|
"""Repository for Requirement-related database operations."""
|
||||||
|
|
||||||
|
def __init__(self, session: AsyncSession):
|
||||||
|
self.session = session
|
||||||
|
|
||||||
|
async def get_all(self) -> List[Requirement]:
|
||||||
|
"""
|
||||||
|
Get all requirements with their related data (tag, priority, groups, validations).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of all requirements with eager-loaded relationships
|
||||||
|
"""
|
||||||
|
result = await self.session.execute(
|
||||||
|
select(Requirement)
|
||||||
|
.options(
|
||||||
|
selectinload(Requirement.tag),
|
||||||
|
selectinload(Requirement.priority),
|
||||||
|
selectinload(Requirement.groups),
|
||||||
|
selectinload(Requirement.validations).selectinload(Validation.status),
|
||||||
|
)
|
||||||
|
.order_by(Requirement.created_at.desc())
|
||||||
|
)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
async def get_by_id(self, requirement_id: int) -> Optional[Requirement]:
|
||||||
|
"""
|
||||||
|
Get a requirement by ID with its related data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
requirement_id: The requirement ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Requirement if found, None otherwise
|
||||||
|
"""
|
||||||
|
result = await self.session.execute(
|
||||||
|
select(Requirement)
|
||||||
|
.options(
|
||||||
|
selectinload(Requirement.tag),
|
||||||
|
selectinload(Requirement.priority),
|
||||||
|
selectinload(Requirement.groups),
|
||||||
|
selectinload(Requirement.validations).selectinload(Validation.status),
|
||||||
|
selectinload(Requirement.user),
|
||||||
|
selectinload(Requirement.last_editor),
|
||||||
|
)
|
||||||
|
.where(Requirement.id == requirement_id)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
async def get_by_group_id(self, group_id: int) -> List[Requirement]:
|
||||||
|
"""
|
||||||
|
Get all requirements belonging to a specific group.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
group_id: The group ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of requirements in the group
|
||||||
|
"""
|
||||||
|
result = await self.session.execute(
|
||||||
|
select(Requirement)
|
||||||
|
.options(
|
||||||
|
selectinload(Requirement.tag),
|
||||||
|
selectinload(Requirement.priority),
|
||||||
|
selectinload(Requirement.groups),
|
||||||
|
selectinload(Requirement.validations).selectinload(Validation.status),
|
||||||
|
)
|
||||||
|
.join(Requirement.groups)
|
||||||
|
.where(Group.id == group_id)
|
||||||
|
.order_by(Requirement.created_at.desc())
|
||||||
|
)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
async def get_by_tag_id(self, tag_id: int) -> List[Requirement]:
|
||||||
|
"""
|
||||||
|
Get all requirements with a specific tag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tag_id: The tag ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of requirements with the tag
|
||||||
|
"""
|
||||||
|
result = await self.session.execute(
|
||||||
|
select(Requirement)
|
||||||
|
.options(
|
||||||
|
selectinload(Requirement.tag),
|
||||||
|
selectinload(Requirement.priority),
|
||||||
|
selectinload(Requirement.groups),
|
||||||
|
selectinload(Requirement.validations).selectinload(Validation.status),
|
||||||
|
)
|
||||||
|
.where(Requirement.tag_id == tag_id)
|
||||||
|
.order_by(Requirement.created_at.desc())
|
||||||
|
)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
async def create(
|
||||||
|
self,
|
||||||
|
user_id: int,
|
||||||
|
tag_id: int,
|
||||||
|
req_name: str,
|
||||||
|
req_desc: Optional[str] = None,
|
||||||
|
priority_id: Optional[int] = None,
|
||||||
|
group_ids: Optional[List[int]] = None,
|
||||||
|
) -> Requirement:
|
||||||
|
"""
|
||||||
|
Create a new requirement.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The creating user's ID
|
||||||
|
tag_id: The tag ID
|
||||||
|
req_name: The requirement name
|
||||||
|
req_desc: The requirement description (optional)
|
||||||
|
priority_id: The priority ID (optional)
|
||||||
|
group_ids: List of group IDs to associate (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created Requirement
|
||||||
|
"""
|
||||||
|
requirement = Requirement(
|
||||||
|
user_id=user_id,
|
||||||
|
tag_id=tag_id,
|
||||||
|
req_name=req_name,
|
||||||
|
req_desc=req_desc,
|
||||||
|
priority_id=priority_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add groups if provided
|
||||||
|
if group_ids:
|
||||||
|
groups_result = await self.session.execute(
|
||||||
|
select(Group).where(Group.id.in_(group_ids))
|
||||||
|
)
|
||||||
|
groups = list(groups_result.scalars().all())
|
||||||
|
requirement.groups = groups
|
||||||
|
|
||||||
|
self.session.add(requirement)
|
||||||
|
await self.session.flush()
|
||||||
|
await self.session.refresh(requirement)
|
||||||
|
|
||||||
|
# Reload with relationships
|
||||||
|
return await self.get_by_id(requirement.id)
|
||||||
|
|
||||||
|
async def update(
|
||||||
|
self,
|
||||||
|
requirement_id: int,
|
||||||
|
editor_id: int,
|
||||||
|
req_name: Optional[str] = None,
|
||||||
|
req_desc: Optional[str] = None,
|
||||||
|
tag_id: Optional[int] = None,
|
||||||
|
priority_id: Optional[int] = None,
|
||||||
|
group_ids: Optional[List[int]] = None,
|
||||||
|
) -> Optional[Requirement]:
|
||||||
|
"""
|
||||||
|
Update an existing requirement.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
requirement_id: The requirement ID to update
|
||||||
|
editor_id: The ID of the user making the edit
|
||||||
|
req_name: New requirement name (optional)
|
||||||
|
req_desc: New requirement description (optional)
|
||||||
|
tag_id: New tag ID (optional)
|
||||||
|
priority_id: New priority ID (optional)
|
||||||
|
group_ids: New list of group IDs (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The updated Requirement, or None if not found
|
||||||
|
"""
|
||||||
|
requirement = await self.get_by_id(requirement_id)
|
||||||
|
if not requirement:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Update fields if provided
|
||||||
|
if req_name is not None:
|
||||||
|
requirement.req_name = req_name
|
||||||
|
if req_desc is not None:
|
||||||
|
requirement.req_desc = req_desc
|
||||||
|
if tag_id is not None:
|
||||||
|
requirement.tag_id = tag_id
|
||||||
|
if priority_id is not None:
|
||||||
|
requirement.priority_id = priority_id
|
||||||
|
|
||||||
|
# Set the last editor
|
||||||
|
requirement.last_editor_id = editor_id
|
||||||
|
|
||||||
|
# Update groups if provided
|
||||||
|
if group_ids is not None:
|
||||||
|
groups_result = await self.session.execute(
|
||||||
|
select(Group).where(Group.id.in_(group_ids))
|
||||||
|
)
|
||||||
|
groups = list(groups_result.scalars().all())
|
||||||
|
requirement.groups = groups
|
||||||
|
|
||||||
|
await self.session.flush()
|
||||||
|
await self.session.refresh(requirement)
|
||||||
|
|
||||||
|
return await self.get_by_id(requirement_id)
|
||||||
|
|
||||||
|
async def delete(self, requirement_id: int) -> bool:
|
||||||
|
"""
|
||||||
|
Delete a requirement.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
requirement_id: The requirement ID to delete
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if deleted, False if not found
|
||||||
|
"""
|
||||||
|
requirement = await self.get_by_id(requirement_id)
|
||||||
|
if not requirement:
|
||||||
|
return False
|
||||||
|
|
||||||
|
await self.session.delete(requirement)
|
||||||
|
await self.session.flush()
|
||||||
|
return True
|
||||||
76
backend/src/repositories/tag_repository.py
Normal file
76
backend/src/repositories/tag_repository.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
"""
|
||||||
|
Repository layer for Tag database operations.
|
||||||
|
"""
|
||||||
|
from typing import List, Optional
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from src.db_models import Tag
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TagRepository:
|
||||||
|
"""Repository for Tag-related database operations."""
|
||||||
|
|
||||||
|
def __init__(self, session: AsyncSession):
|
||||||
|
self.session = session
|
||||||
|
|
||||||
|
async def get_all(self) -> List[Tag]:
|
||||||
|
"""
|
||||||
|
Get all tags.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of all tags
|
||||||
|
"""
|
||||||
|
result = await self.session.execute(
|
||||||
|
select(Tag).order_by(Tag.tag_code)
|
||||||
|
)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
async def get_by_id(self, tag_id: int) -> Optional[Tag]:
|
||||||
|
"""
|
||||||
|
Get a tag by ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tag_id: The tag ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tag if found, None otherwise
|
||||||
|
"""
|
||||||
|
result = await self.session.execute(
|
||||||
|
select(Tag).where(Tag.id == tag_id)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
async def get_by_code(self, tag_code: str) -> Optional[Tag]:
|
||||||
|
"""
|
||||||
|
Get a tag by code.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tag_code: The tag code (e.g., GSR, SFR)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tag if found, None otherwise
|
||||||
|
"""
|
||||||
|
result = await self.session.execute(
|
||||||
|
select(Tag).where(Tag.tag_code == tag_code)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
async def create(self, tag_code: str, tag_description: str) -> Tag:
|
||||||
|
"""
|
||||||
|
Create a new tag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tag_code: The tag code (e.g., GSR, SFR)
|
||||||
|
tag_description: The tag description
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created Tag
|
||||||
|
"""
|
||||||
|
tag = Tag(tag_code=tag_code, tag_description=tag_description)
|
||||||
|
self.session.add(tag)
|
||||||
|
await self.session.flush()
|
||||||
|
await self.session.refresh(tag)
|
||||||
|
return tag
|
||||||
@@ -1,140 +1,108 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { useAuth } from '@/hooks'
|
import { useAuth } from '@/hooks'
|
||||||
import { useSearchParams, Link, useNavigate } from 'react-router-dom'
|
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'
|
||||||
|
|
||||||
// Types for requirements
|
// Helper to lighten a hex color for backgrounds
|
||||||
interface Requirement {
|
function lightenColor(hex: string, percent: number): string {
|
||||||
id: string
|
const num = parseInt(hex.replace('#', ''), 16)
|
||||||
tag: string
|
const amt = Math.round(2.55 * percent)
|
||||||
title: string
|
const R = (num >> 16) + amt
|
||||||
validation: string
|
const G = (num >> 8 & 0x00FF) + amt
|
||||||
priority: number
|
const B = (num & 0x0000FF) + amt
|
||||||
progress: number
|
return '#' + (
|
||||||
group: RequirementGroup
|
0x1000000 +
|
||||||
|
(R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
|
||||||
|
(G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
|
||||||
|
(B < 255 ? (B < 1 ? 0 : B) : 255)
|
||||||
|
).toString(16).slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
type RequirementGroup =
|
|
||||||
| 'Data Services'
|
|
||||||
| 'Integration'
|
|
||||||
| 'Intelligence'
|
|
||||||
| 'User Experience'
|
|
||||||
| 'Management'
|
|
||||||
| 'Trustworthiness'
|
|
||||||
|
|
||||||
// Color mapping for requirement groups
|
|
||||||
const groupColors: Record<RequirementGroup, { bg: string; border: string }> = {
|
|
||||||
'Data Services': { bg: 'bg-blue-200', border: 'border-blue-300' },
|
|
||||||
'Integration': { bg: 'bg-amber-200', border: 'border-amber-300' },
|
|
||||||
'Intelligence': { bg: 'bg-purple-200', border: 'border-purple-300' },
|
|
||||||
'User Experience': { bg: 'bg-green-200', border: 'border-green-300' },
|
|
||||||
'Management': { bg: 'bg-red-200', border: 'border-red-300' },
|
|
||||||
'Trustworthiness': { bg: 'bg-teal-600', border: 'border-teal-700' },
|
|
||||||
}
|
|
||||||
|
|
||||||
// Static mock data
|
|
||||||
const mockRequirements: Requirement[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
tag: 'GSR#7',
|
|
||||||
title: 'Controle e monitoramento em right-time',
|
|
||||||
validation: 'Not Validated',
|
|
||||||
priority: 0,
|
|
||||||
progress: 0,
|
|
||||||
group: 'Trustworthiness',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
tag: 'GSR#10',
|
|
||||||
title: 'Interface de software DT-externo bem definida.',
|
|
||||||
validation: 'Not Validated',
|
|
||||||
priority: 1,
|
|
||||||
progress: 0,
|
|
||||||
group: 'Intelligence',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
tag: 'GSR#12',
|
|
||||||
title: 'Visualizacao',
|
|
||||||
validation: 'Not Validated',
|
|
||||||
priority: 1,
|
|
||||||
progress: 0,
|
|
||||||
group: 'User Experience',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
tag: 'GSR#1',
|
|
||||||
title: 'Estado corrente atualizado',
|
|
||||||
validation: 'Not Validated',
|
|
||||||
priority: 1,
|
|
||||||
progress: 0,
|
|
||||||
group: 'Data Services',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
tag: 'GSR#5',
|
|
||||||
title: 'Sincronização de dados em tempo real',
|
|
||||||
validation: 'Not Validated',
|
|
||||||
priority: 2,
|
|
||||||
progress: 25,
|
|
||||||
group: 'Data Services',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '6',
|
|
||||||
tag: 'GSR#8',
|
|
||||||
title: 'Integração com sistemas legados',
|
|
||||||
validation: 'Not Validated',
|
|
||||||
priority: 1,
|
|
||||||
progress: 50,
|
|
||||||
group: 'Integration',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '7',
|
|
||||||
tag: 'GSR#15',
|
|
||||||
title: 'Dashboard de gestão',
|
|
||||||
validation: 'Not Validated',
|
|
||||||
priority: 3,
|
|
||||||
progress: 0,
|
|
||||||
group: 'Management',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
// Caption types
|
|
||||||
const captionItems = [
|
|
||||||
{ abbr: 'SFR', description: 'Specific Functional Requirement', underline: true },
|
|
||||||
{ abbr: 'SNFR', description: 'Specific Non-Functional Requirement', underline: true },
|
|
||||||
{ abbr: 'GNFR', description: 'Generic Non-Functional Requirement', underline: true },
|
|
||||||
{ abbr: 'GSR', description: 'Generic Service Requirement', underline: true },
|
|
||||||
{ abbr: 'GDR', description: 'Generic Data Requirement', underline: true },
|
|
||||||
{ abbr: 'GCR', description: 'Generic Connection Requirement', underline: true },
|
|
||||||
]
|
|
||||||
|
|
||||||
export default function RequirementsPage() {
|
export default function RequirementsPage() {
|
||||||
const { user, logout } = useAuth()
|
const { user, logout } = useAuth()
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
// State
|
// Data state
|
||||||
|
const [groups, setGroups] = useState<Group[]>([])
|
||||||
|
const [tags, setTags] = useState<Tag[]>([])
|
||||||
|
const [priorities, setPriorities] = useState<Priority[]>([])
|
||||||
|
const [requirements, setRequirements] = useState<Requirement[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Filter state
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
const [selectedGroups, setSelectedGroups] = useState<RequirementGroup[]>([])
|
const [selectedGroups, setSelectedGroups] = useState<number[]>([])
|
||||||
const [orderBy, setOrderBy] = useState<'Date' | 'Priority' | 'Name'>('Date')
|
const [orderBy, setOrderBy] = useState<'Date' | 'Priority' | 'Name'>('Date')
|
||||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('list')
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('list')
|
||||||
|
|
||||||
|
// Modal state
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||||
|
const [createLoading, setCreateLoading] = useState(false)
|
||||||
|
const [createError, setCreateError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Form state for new requirement
|
||||||
|
const [newReqName, setNewReqName] = useState('')
|
||||||
|
const [newReqDesc, setNewReqDesc] = useState('')
|
||||||
|
const [newReqTagId, setNewReqTagId] = useState<number | ''>('')
|
||||||
|
const [newReqPriorityId, setNewReqPriorityId] = useState<number | ''>('')
|
||||||
|
const [newReqGroupIds, setNewReqGroupIds] = useState<number[]>([])
|
||||||
|
|
||||||
|
// Fetch data on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
// Fetch groups, tags, priorities, and requirements in parallel
|
||||||
|
const [groupsData, tagsData, prioritiesData, requirementsData] = await Promise.all([
|
||||||
|
groupService.getGroups(),
|
||||||
|
tagService.getTags(),
|
||||||
|
priorityService.getPriorities(),
|
||||||
|
requirementService.getRequirements(),
|
||||||
|
])
|
||||||
|
|
||||||
|
setGroups(groupsData)
|
||||||
|
setTags(tagsData)
|
||||||
|
setPriorities(prioritiesData)
|
||||||
|
setRequirements(requirementsData)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch data:', err)
|
||||||
|
setError('Failed to load data. Please try again.')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData()
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Initialize filters from URL params
|
// Initialize filters from URL params
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const groupParam = searchParams.get('group')
|
const groupParam = searchParams.get('group')
|
||||||
if (groupParam) {
|
if (groupParam && groups.length > 0) {
|
||||||
setSelectedGroups([groupParam as RequirementGroup])
|
const group = groups.find(g => g.group_name === groupParam)
|
||||||
|
if (group) {
|
||||||
|
setSelectedGroups([group.id])
|
||||||
}
|
}
|
||||||
}, [searchParams])
|
}
|
||||||
|
}, [searchParams, groups])
|
||||||
|
|
||||||
// Filter requirements based on search and selected groups
|
// Filter requirements based on search and selected groups
|
||||||
const filteredRequirements = mockRequirements.filter(req => {
|
const filteredRequirements = requirements.filter(req => {
|
||||||
|
const tagLabel = `${req.tag.tag_code}#${req.id}`
|
||||||
const matchesSearch = searchQuery === '' ||
|
const matchesSearch = searchQuery === '' ||
|
||||||
req.tag.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
tagLabel.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
req.title.toLowerCase().includes(searchQuery.toLowerCase())
|
req.req_name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
|
||||||
const matchesGroup = selectedGroups.length === 0 ||
|
const matchesGroup = selectedGroups.length === 0 ||
|
||||||
selectedGroups.includes(req.group)
|
req.groups.some(g => selectedGroups.includes(g.id))
|
||||||
|
|
||||||
return matchesSearch && matchesGroup
|
return matchesSearch && matchesGroup
|
||||||
})
|
})
|
||||||
@@ -143,19 +111,24 @@ export default function RequirementsPage() {
|
|||||||
const sortedRequirements = [...filteredRequirements].sort((a, b) => {
|
const sortedRequirements = [...filteredRequirements].sort((a, b) => {
|
||||||
switch (orderBy) {
|
switch (orderBy) {
|
||||||
case 'Priority':
|
case 'Priority':
|
||||||
return b.priority - a.priority
|
const priorityA = a.priority?.priority_num ?? 0
|
||||||
|
const priorityB = b.priority?.priority_num ?? 0
|
||||||
|
return priorityB - priorityA
|
||||||
case 'Name':
|
case 'Name':
|
||||||
return a.title.localeCompare(b.title)
|
return a.req_name.localeCompare(b.req_name)
|
||||||
|
case 'Date':
|
||||||
default:
|
default:
|
||||||
return 0
|
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 = (group: RequirementGroup) => {
|
const handleGroupToggle = (groupId: number) => {
|
||||||
setSelectedGroups(prev =>
|
setSelectedGroups(prev =>
|
||||||
prev.includes(group)
|
prev.includes(groupId)
|
||||||
? prev.filter(g => g !== group)
|
? prev.filter(id => id !== groupId)
|
||||||
: [...prev, group]
|
: [...prev, groupId]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,18 +139,128 @@ export default function RequirementsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
// For now, filtering is automatic - this could trigger an API call later
|
// Filtering is automatic, but this could trigger a fresh API call
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRemove = (id: string) => {
|
const handleRemove = async (id: number) => {
|
||||||
// For now, just a placeholder - will connect to backend later
|
if (!confirm('Are you sure you want to delete this requirement?')) {
|
||||||
console.log('Remove requirement:', id)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDetails = (id: string) => {
|
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}`)
|
navigate(`/requirements/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the primary group color for a requirement (first group or default)
|
||||||
|
const getRequirementColor = (req: Requirement): string => {
|
||||||
|
if (req.groups.length > 0) {
|
||||||
|
return req.groups[0].hex_color
|
||||||
|
}
|
||||||
|
return '#6B7280' // default gray
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setCreateLoading(true)
|
||||||
|
setCreateError(null)
|
||||||
|
|
||||||
|
const data: RequirementCreateRequest = {
|
||||||
|
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) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-teal-600 mx-auto"></div>
|
||||||
|
<p className="mt-4 text-gray-600">Loading requirements...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-red-600 mb-4">{error}</p>
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="px-4 py-2 bg-teal-600 text-white rounded hover:bg-teal-700"
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -236,7 +319,10 @@ export default function RequirementsPage() {
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
{/* New Requirement Button */}
|
{/* New Requirement Button */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<button className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-50">
|
<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
|
New Requirement
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -268,15 +354,15 @@ export default function RequirementsPage() {
|
|||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<p className="text-sm text-gray-600 mb-3">Filter Group</p>
|
<p className="text-sm text-gray-600 mb-3">Filter Group</p>
|
||||||
<div className="grid grid-cols-3 gap-x-8 gap-y-2">
|
<div className="grid grid-cols-3 gap-x-8 gap-y-2">
|
||||||
{(['Data Services', 'Intelligence', 'Management', 'Integration', 'User Experience', 'Trustworthiness'] as RequirementGroup[]).map((group) => (
|
{groups.map((group) => (
|
||||||
<label key={group} className="flex items-center gap-2 cursor-pointer">
|
<label key={group.id} className="flex items-center gap-2 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selectedGroups.includes(group)}
|
checked={selectedGroups.includes(group.id)}
|
||||||
onChange={() => handleGroupToggle(group)}
|
onChange={() => handleGroupToggle(group.id)}
|
||||||
className="w-4 h-4 rounded border-gray-300 text-teal-600 focus:ring-teal-500"
|
className="w-4 h-4 rounded border-gray-300 text-teal-600 focus:ring-teal-500"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-blue-600 underline">{group}</span>
|
<span className="text-sm text-blue-600 underline">{group.group_name}</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -327,30 +413,39 @@ export default function RequirementsPage() {
|
|||||||
{/* Requirements List */}
|
{/* Requirements List */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{sortedRequirements.map((req) => {
|
{sortedRequirements.map((req) => {
|
||||||
const colors = groupColors[req.group]
|
const primaryColor = getRequirementColor(req)
|
||||||
|
const bgColor = lightenColor(primaryColor, 60)
|
||||||
|
const tagLabel = `${req.tag.tag_code}#${req.id}`
|
||||||
|
const priorityNum = req.priority?.priority_num ?? 0
|
||||||
|
const validationStatus = req.validation_status || 'Not Validated'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={req.id}
|
key={req.id}
|
||||||
className={`flex items-center border ${colors.border} rounded overflow-hidden`}
|
className="flex items-center rounded overflow-hidden"
|
||||||
|
style={{ borderColor: primaryColor, borderWidth: '1px', borderStyle: 'solid' }}
|
||||||
>
|
>
|
||||||
{/* Colored tag section */}
|
{/* Colored tag section */}
|
||||||
<div className={`${colors.bg} px-4 py-4 min-w-[320px]`}>
|
<div
|
||||||
|
className="px-4 py-4 min-w-[320px]"
|
||||||
|
style={{ backgroundColor: bgColor }}
|
||||||
|
>
|
||||||
<span className="font-bold text-gray-800">
|
<span className="font-bold text-gray-800">
|
||||||
{req.tag} - {req.title}
|
{tagLabel} - {req.req_name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Validation status */}
|
{/* Validation status */}
|
||||||
<div className="flex-1 px-6 py-4 text-center">
|
<div className="flex-1 px-6 py-4 text-center">
|
||||||
<span className="text-sm text-gray-600">
|
<span className="text-sm text-gray-600">
|
||||||
Validation: {req.validation}
|
Validation: {validationStatus}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Priority and Progress */}
|
{/* Priority and Version */}
|
||||||
<div className="px-6 py-4 text-right">
|
<div className="px-6 py-4 text-right">
|
||||||
<p className="text-sm text-gray-700">Priority: {req.priority}</p>
|
<p className="text-sm text-gray-700">Priority: {priorityNum}</p>
|
||||||
<p className="text-sm text-gray-600">{req.progress.toFixed(2)}% Done</p>
|
<p className="text-sm text-gray-600">Version: {req.version}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action buttons */}
|
{/* Action buttons */}
|
||||||
@@ -385,10 +480,10 @@ export default function RequirementsPage() {
|
|||||||
<div className="border-l border-gray-200 pl-6">
|
<div className="border-l border-gray-200 pl-6">
|
||||||
<h3 className="font-semibold text-gray-800 mb-4">Caption:</h3>
|
<h3 className="font-semibold text-gray-800 mb-4">Caption:</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{captionItems.map((item) => (
|
{tags.map((tag) => (
|
||||||
<p key={item.abbr} className="text-sm">
|
<p key={tag.id} className="text-sm">
|
||||||
<span className="text-blue-600 underline">{item.abbr}</span>
|
<span className="text-blue-600 underline">{tag.tag_code}</span>
|
||||||
<span className="text-gray-600">: {item.description}</span>
|
<span className="text-gray-600">: {tag.tag_description}</span>
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -397,6 +492,145 @@ export default function RequirementsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Create Requirement Modal */}
|
||||||
|
{showCreateModal && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-lg mx-4">
|
||||||
|
{/* Modal Header */}
|
||||||
|
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-800">New Requirement</h2>
|
||||||
|
<button
|
||||||
|
onClick={closeCreateModal}
|
||||||
|
className="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Body */}
|
||||||
|
<form onSubmit={handleCreateRequirement}>
|
||||||
|
<div className="px-6 py-4 space-y-4">
|
||||||
|
{/* Error message */}
|
||||||
|
{createError && (
|
||||||
|
<div className="p-3 bg-red-100 border border-red-400 text-red-700 rounded text-sm">
|
||||||
|
{createError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tag Selection */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Tag <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={newReqTagId}
|
||||||
|
onChange={(e) => setNewReqTagId(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"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Select a tag...</option>
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<option key={tag.id} value={tag.id}>
|
||||||
|
{tag.tag_code} - {tag.tag_description}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Requirement Name */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Name <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newReqName}
|
||||||
|
onChange={(e) => 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
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={newReqDesc}
|
||||||
|
onChange={(e) => setNewReqDesc(e.target.value)}
|
||||||
|
placeholder="Enter requirement description (optional)"
|
||||||
|
rows={3}
|
||||||
|
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 resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Priority Selection */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Priority
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={newReqPriorityId}
|
||||||
|
onChange={(e) => setNewReqPriorityId(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"
|
||||||
|
>
|
||||||
|
<option value="">Select a priority (optional)...</option>
|
||||||
|
{priorities.map((priority) => (
|
||||||
|
<option key={priority.id} value={priority.id}>
|
||||||
|
{priority.priority_name} ({priority.priority_num})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Groups Selection */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Groups
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-2 gap-2 max-h-40 overflow-y-auto border border-gray-200 rounded p-3">
|
||||||
|
{groups.map((group) => (
|
||||||
|
<label key={group.id} className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={newReqGroupIds.includes(group.id)}
|
||||||
|
onChange={() => handleCreateGroupToggle(group.id)}
|
||||||
|
className="w-4 h-4 rounded border-gray-300 text-teal-600 focus:ring-teal-500"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700">{group.group_name}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Footer */}
|
||||||
|
<div className="flex justify-end gap-3 px-6 py-4 border-t border-gray-200 bg-gray-50 rounded-b-lg">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={closeCreateModal}
|
||||||
|
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-100"
|
||||||
|
disabled={createLoading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
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"
|
||||||
|
disabled={createLoading}
|
||||||
|
>
|
||||||
|
{createLoading ? 'Creating...' : 'Create Requirement'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
export { authService } from './authService'
|
export { authService } from './authService'
|
||||||
export { groupService } from './groupService'
|
export { groupService } from './groupService'
|
||||||
export type { Group } from './groupService'
|
export type { Group } from './groupService'
|
||||||
|
export { tagService } from './tagService'
|
||||||
|
export type { Tag } from './tagService'
|
||||||
|
export { priorityService } from './priorityService'
|
||||||
|
export type { Priority } from './priorityService'
|
||||||
|
export { requirementService } from './requirementService'
|
||||||
|
export type { Requirement, RequirementCreateRequest, RequirementUpdateRequest } from './requirementService'
|
||||||
|
|||||||
36
frontend/src/services/priorityService.ts
Normal file
36
frontend/src/services/priorityService.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
const API_BASE_URL = '/api'
|
||||||
|
|
||||||
|
export interface Priority {
|
||||||
|
id: number
|
||||||
|
priority_name: string
|
||||||
|
priority_num: number
|
||||||
|
}
|
||||||
|
|
||||||
|
class PriorityService {
|
||||||
|
/**
|
||||||
|
* Get all priorities from the API.
|
||||||
|
*/
|
||||||
|
async getPriorities(): Promise<Priority[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/priorities`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const priorities: Priority[] = await response.json()
|
||||||
|
return priorities
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch priorities:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const priorityService = new PriorityService()
|
||||||
176
frontend/src/services/requirementService.ts
Normal file
176
frontend/src/services/requirementService.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import { Group } from './groupService'
|
||||||
|
import { Tag } from './tagService'
|
||||||
|
import { Priority } from './priorityService'
|
||||||
|
|
||||||
|
const API_BASE_URL = '/api'
|
||||||
|
|
||||||
|
export interface Requirement {
|
||||||
|
id: number
|
||||||
|
req_name: string
|
||||||
|
req_desc: string | null
|
||||||
|
version: number
|
||||||
|
created_at: string | null
|
||||||
|
updated_at: string | null
|
||||||
|
tag: Tag
|
||||||
|
priority: Priority | null
|
||||||
|
groups: Group[]
|
||||||
|
validation_status: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequirementCreateRequest {
|
||||||
|
tag_id: number
|
||||||
|
req_name: string
|
||||||
|
req_desc?: string
|
||||||
|
priority_id?: number
|
||||||
|
group_ids?: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequirementUpdateRequest {
|
||||||
|
req_name?: string
|
||||||
|
req_desc?: string
|
||||||
|
tag_id?: number
|
||||||
|
priority_id?: number
|
||||||
|
group_ids?: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
class RequirementService {
|
||||||
|
/**
|
||||||
|
* Get all requirements from the API.
|
||||||
|
* Optionally filter by group_id or tag_id.
|
||||||
|
*/
|
||||||
|
async getRequirements(params?: { group_id?: number; tag_id?: number }): Promise<Requirement[]> {
|
||||||
|
try {
|
||||||
|
let url = `${API_BASE_URL}/requirements`
|
||||||
|
|
||||||
|
if (params) {
|
||||||
|
const queryParams = new URLSearchParams()
|
||||||
|
if (params.group_id) queryParams.append('group_id', params.group_id.toString())
|
||||||
|
if (params.tag_id) queryParams.append('tag_id', params.tag_id.toString())
|
||||||
|
|
||||||
|
const queryString = queryParams.toString()
|
||||||
|
if (queryString) {
|
||||||
|
url += `?${queryString}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const requirements: Requirement[] = await response.json()
|
||||||
|
return requirements
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch requirements:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific requirement by ID.
|
||||||
|
*/
|
||||||
|
async getRequirement(requirementId: number): Promise<Requirement> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/requirements/${requirementId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const requirement: Requirement = await response.json()
|
||||||
|
return requirement
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch requirement:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new requirement.
|
||||||
|
*/
|
||||||
|
async createRequirement(data: RequirementCreateRequest): Promise<Requirement> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/requirements`, {
|
||||||
|
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 requirement: Requirement = await response.json()
|
||||||
|
return requirement
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create requirement:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing requirement.
|
||||||
|
*/
|
||||||
|
async updateRequirement(requirementId: number, data: RequirementUpdateRequest): Promise<Requirement> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/requirements/${requirementId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const requirement: Requirement = await response.json()
|
||||||
|
return requirement
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update requirement:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a requirement.
|
||||||
|
*/
|
||||||
|
async deleteRequirement(requirementId: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/requirements/${requirementId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete requirement:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requirementService = new RequirementService()
|
||||||
61
frontend/src/services/tagService.ts
Normal file
61
frontend/src/services/tagService.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
const API_BASE_URL = '/api'
|
||||||
|
|
||||||
|
export interface Tag {
|
||||||
|
id: number
|
||||||
|
tag_code: string
|
||||||
|
tag_description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class TagService {
|
||||||
|
/**
|
||||||
|
* Get all tags from the API.
|
||||||
|
*/
|
||||||
|
async getTags(): Promise<Tag[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/tags`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags: Tag[] = await response.json()
|
||||||
|
return tags
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch tags:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific tag by ID.
|
||||||
|
*/
|
||||||
|
async getTag(tagId: number): Promise<Tag> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/tags/${tagId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tag: Tag = await response.json()
|
||||||
|
return tag
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch tag:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tagService = new TagService()
|
||||||
Reference in New Issue
Block a user