From a52a669521b5ad30a2f05bc28eb40a6de0196548 Mon Sep 17 00:00:00 2001 From: gulimabr Date: Mon, 1 Dec 2025 12:39:13 -0300 Subject: [PATCH] Added admin page --- backend/src/db_models.py | 7 +- backend/src/main.py | 245 +++++- backend/src/models.py | 54 +- .../src/repositories/project_repository.py | 1 + .../relationship_type_repository.py | 34 + backend/src/repositories/user_repository.py | 135 +-- frontend/src/App.tsx | 9 + frontend/src/pages/AdminPage.tsx | 814 ++++++++++++++++++ frontend/src/pages/DashboardPage.tsx | 13 +- frontend/src/pages/index.ts | 1 + frontend/src/services/index.ts | 6 +- frontend/src/services/relationshipService.ts | 85 ++ frontend/src/services/userService.ts | 91 ++ 13 files changed, 1430 insertions(+), 65 deletions(-) create mode 100644 frontend/src/pages/AdminPage.tsx create mode 100644 frontend/src/services/userService.ts diff --git a/backend/src/db_models.py b/backend/src/db_models.py index 5d70c03..37486bf 100644 --- a/backend/src/db_models.py +++ b/backend/src/db_models.py @@ -318,7 +318,12 @@ class RelationshipType(Base): # Relationships project: Mapped["Project"] = relationship("Project", back_populates="relationship_types") - links: Mapped[List["RequirementLink"]] = relationship("RequirementLink", back_populates="relationship_type") + links: Mapped[List["RequirementLink"]] = relationship( + "RequirementLink", + back_populates="relationship_type", + cascade="all, delete-orphan", + passive_deletes=True + ) # Constraints __table_args__ = ( diff --git a/backend/src/main.py b/backend/src/main.py index b01de9d..6704562 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -11,8 +11,9 @@ from src.models import ( RequirementCreateRequest, RequirementUpdateRequest, ProjectResponse, ProjectCreateRequest, ProjectUpdateRequest, ProjectMemberRequest, ValidationStatusResponse, ValidationHistoryResponse, ValidationCreateRequest, - RelationshipTypeResponse, RelationshipTypeCreateRequest, - RequirementLinkResponse, RequirementLinkCreateRequest, RequirementSearchResult + RelationshipTypeResponse, RelationshipTypeCreateRequest, RelationshipTypeUpdateRequest, + RequirementLinkResponse, RequirementLinkCreateRequest, RequirementSearchResult, + RoleResponse, ProjectMemberResponse, UserRoleUpdateRequest, ROLE_DISPLAY_NAMES ) from src.controller import AuthController from src.config import get_openid, get_settings @@ -521,6 +522,240 @@ async def remove_project_member( await db.commit() +# =========================================== +# Admin Endpoints (Role Management, Project Admin) +# =========================================== + +@app.get("/api/roles", response_model=List[RoleResponse]) +async def get_all_roles( + request: Request, + db: AsyncSession = Depends(get_db) +): + """ + Get all available roles with their display names. + + Returns: + List of roles with id, role_name, and display_name. + """ + # Ensure user is authenticated + await _get_current_user_db(request, db) + + role_repo = RoleRepository(db) + roles = await role_repo.get_all() + return [RoleResponse.from_role(r) for r in roles] + + +@app.get("/api/projects/{project_id}/members", response_model=List[ProjectMemberResponse]) +async def get_project_members( + project_id: int, + request: Request, + db: AsyncSession = Depends(get_db) +): + """ + Get all members of a project with their role information. + User must be a member of the project. + + Args: + project_id: The project ID + + Returns: + List of project members with role info. + """ + user = await _get_current_user_db(request, db) + await _verify_project_membership(project_id, user.id, db) + + project_repo = ProjectRepository(db) + members = await project_repo.get_members(project_id) + + return [ + ProjectMemberResponse( + id=member.id, + sub=member.sub, + role_id=member.role_id, + role_name=member.role.role_name if member.role else "unknown", + role_display_name=ROLE_DISPLAY_NAMES.get(member.role.role_name, member.role.role_name.title()) if member.role else "Unknown", + created_at=member.created_at + ) + for member in members + ] + + +@app.put("/api/projects/{project_id}/members/{user_id}/role", response_model=ProjectMemberResponse) +async def update_member_role( + project_id: int, + user_id: int, + request: Request, + role_data: UserRoleUpdateRequest, + db: AsyncSession = Depends(get_db) +): + """ + Update a project member's role. + Only project admins (role_id=3) can update roles. + Admin cannot demote themselves. + + Args: + project_id: The project ID + user_id: The user ID to update + role_data: The new role ID + + Returns: + The updated member info. + """ + current_user = await _get_current_user_db(request, db) + + # Only admins (role_id=3) can update roles + _require_role(current_user, [3], "update member roles") + + await _verify_project_membership(project_id, current_user.id, db) + + # Check target user is a member of the project + project_repo = ProjectRepository(db) + if not await project_repo.is_member(project_id, user_id): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User is not a member of this project" + ) + + # Prevent self-demotion + if current_user.id == user_id and role_data.role_id != 3: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="You cannot demote yourself. Ask another admin to change your role." + ) + + # Verify role exists + role_repo = RoleRepository(db) + role = await role_repo.get_by_id(role_data.role_id) + if not role: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid role id {role_data.role_id}" + ) + + # Update the user's role + from src.repositories import UserRepository + user_repo = UserRepository(db) + updated_user = await user_repo.update_role(user_id, role_data.role_id) + + if not updated_user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"User with id {user_id} not found" + ) + + await db.commit() + + return ProjectMemberResponse( + id=updated_user.id, + sub=updated_user.sub, + role_id=updated_user.role_id, + role_name=role.role_name, + role_display_name=ROLE_DISPLAY_NAMES.get(role.role_name, role.role_name.title()), + created_at=updated_user.created_at + ) + + +@app.put("/api/projects/{project_id}/relationship-types/{type_id}", response_model=RelationshipTypeResponse) +async def update_relationship_type( + project_id: int, + type_id: int, + request: Request, + type_data: RelationshipTypeUpdateRequest, + db: AsyncSession = Depends(get_db) +): + """ + Update a relationship type. + Only project admins (role_id=3) can update relationship types. + + Args: + project_id: The project ID + type_id: The relationship type ID to update + type_data: The updated relationship type data + + Returns: + The updated relationship type. + """ + user = await _get_current_user_db(request, db) + + # Only admins (role_id=3) can update relationship types + _require_role(user, [3], "update relationship types") + + await _verify_project_membership(project_id, user.id, db) + + rel_type_repo = RelationshipTypeRepository(db) + + # Check if relationship type exists and belongs to the project + existing_type = await rel_type_repo.get_by_id(type_id) + if not existing_type: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Relationship type with id {type_id} not found" + ) + if existing_type.project_id != project_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Relationship type does not belong to this project" + ) + + updated_type = await rel_type_repo.update( + relationship_type_id=type_id, + type_name=type_data.type_name, + type_description=type_data.type_description, + inverse_type_name=type_data.inverse_type_name + ) + + await db.commit() + return RelationshipTypeResponse.model_validate(updated_type) + + +@app.delete("/api/projects/{project_id}/relationship-types/{type_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_relationship_type( + project_id: int, + type_id: int, + request: Request, + db: AsyncSession = Depends(get_db) +): + """ + Delete a relationship type. + Only project admins (role_id=3) can delete relationship types. + This will also delete all links using this relationship type. + + Args: + project_id: The project ID + type_id: The relationship type ID to delete + """ + user = await _get_current_user_db(request, db) + + # Only admins (role_id=3) can delete relationship types + _require_role(user, [3], "delete relationship types") + + await _verify_project_membership(project_id, user.id, db) + + rel_type_repo = RelationshipTypeRepository(db) + + # Check if relationship type exists and belongs to the project + existing_type = await rel_type_repo.get_by_id(type_id) + if not existing_type: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Relationship type with id {type_id} not found" + ) + if existing_type.project_id != project_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Relationship type does not belong to this project" + ) + + deleted = await rel_type_repo.delete(type_id) + if not deleted: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Relationship type with id {type_id} not found" + ) + + await db.commit() + + # =========================================== # Requirements Endpoints # =========================================== @@ -968,7 +1203,7 @@ async def create_relationship_type( ): """ Create a new relationship type for a project. - Only admins (role_id=1) can create relationship types. + Only project admins (role_id=3) can create relationship types. Args: project_id: The project ID @@ -979,8 +1214,8 @@ async def create_relationship_type( """ user = await _get_current_user_db(request, db) - # Only admins can create relationship types - _require_role(user, [1], "create relationship types") + # Only admins (role_id=3) can create relationship types + _require_role(user, [3], "create relationship types") await _verify_project_membership(project_id, user.id, db) diff --git a/backend/src/models.py b/backend/src/models.py index 096e3cb..348990b 100644 --- a/backend/src/models.py +++ b/backend/src/models.py @@ -20,7 +20,52 @@ class UserInfo(BaseModel): full_name: Optional[str] = None db_user_id: Optional[int] = None # Database user ID (populated after login) role: Optional[str] = None # User role name - role_id: Optional[int] = None # User role ID (1=admin, 2=auditor, 3=user, etc.) + role_id: Optional[int] = None # User role ID (1=editor, 2=auditor, 3=admin) + + +# Role schemas +ROLE_DISPLAY_NAMES = { + "editor": "Editor", + "auditor": "Auditor", + "admin": "Project Admin" +} + + +class RoleResponse(BaseModel): + """Response schema for a role.""" + id: int + role_name: str + display_name: str + + class Config: + from_attributes = True + + @classmethod + def from_role(cls, role) -> "RoleResponse": + """Create a RoleResponse from a Role model.""" + return cls( + id=role.id, + role_name=role.role_name, + display_name=ROLE_DISPLAY_NAMES.get(role.role_name, role.role_name.title()) + ) + + +class ProjectMemberResponse(BaseModel): + """Response schema for a project member with role info.""" + id: int + sub: str + role_id: int + role_name: str + role_display_name: str + created_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +class UserRoleUpdateRequest(BaseModel): + """Request schema for updating a user's role.""" + role_id: int # Project schemas @@ -215,6 +260,13 @@ class RelationshipTypeCreateRequest(BaseModel): inverse_type_name: Optional[str] = None +class RelationshipTypeUpdateRequest(BaseModel): + """Request schema for updating a relationship type.""" + type_name: Optional[str] = None + type_description: Optional[str] = None + inverse_type_name: Optional[str] = None + + # Requirement Link schemas class LinkedRequirementInfo(BaseModel): """Brief info about a linked requirement.""" diff --git a/backend/src/repositories/project_repository.py b/backend/src/repositories/project_repository.py index e8eb344..22ea825 100644 --- a/backend/src/repositories/project_repository.py +++ b/backend/src/repositories/project_repository.py @@ -229,6 +229,7 @@ class ProjectRepository: """ result = await self.session.execute( select(User) + .options(selectinload(User.role)) .join(ProjectMember, User.id == ProjectMember.user_id) .where(ProjectMember.project_id == project_id) ) diff --git a/backend/src/repositories/relationship_type_repository.py b/backend/src/repositories/relationship_type_repository.py index 85dc64d..8ee27b6 100644 --- a/backend/src/repositories/relationship_type_repository.py +++ b/backend/src/repositories/relationship_type_repository.py @@ -76,6 +76,40 @@ class RelationshipTypeRepository: await self.db.refresh(relationship_type) return relationship_type + async def update( + self, + relationship_type_id: int, + type_name: Optional[str] = None, + type_description: Optional[str] = None, + inverse_type_name: Optional[str] = None + ) -> Optional[RelationshipType]: + """ + Update a relationship type. + + Args: + relationship_type_id: The ID of the relationship type to update + type_name: New name (optional) + type_description: New description (optional) + inverse_type_name: New inverse name (optional) + + Returns: + The updated relationship type or None if not found. + """ + relationship_type = await self.get_by_id(relationship_type_id) + if not relationship_type: + return None + + if type_name is not None: + relationship_type.type_name = type_name + if type_description is not None: + relationship_type.type_description = type_description + if inverse_type_name is not None: + relationship_type.inverse_type_name = inverse_type_name + + await self.db.flush() + await self.db.refresh(relationship_type) + return relationship_type + async def delete(self, relationship_type_id: int) -> bool: """ Delete a relationship type by ID. diff --git a/backend/src/repositories/user_repository.py b/backend/src/repositories/user_repository.py index a2384a8..1777f95 100644 --- a/backend/src/repositories/user_repository.py +++ b/backend/src/repositories/user_repository.py @@ -2,7 +2,7 @@ Repository layer for User database operations. Handles CRUD operations and user provisioning on first login. """ -from typing import Optional +from typing import Optional, List from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload @@ -11,8 +11,64 @@ import logging logger = logging.getLogger(__name__) -# Default role for new users -DEFAULT_ROLE_NAME = "user" +# Default role for new users (editor = role_id 1) +DEFAULT_ROLE_NAME = "editor" + + +class RoleRepository: + """Repository for Role-related database operations.""" + + def __init__(self, session: AsyncSession): + self.session = session + + async def get_by_name(self, role_name: str) -> Optional[Role]: + """Get a role by name.""" + result = await self.session.execute( + select(Role).where(Role.role_name == role_name) + ) + return result.scalar_one_or_none() + + async def get_by_id(self, role_id: int) -> Optional[Role]: + """Get a role by ID.""" + result = await self.session.execute( + select(Role).where(Role.id == role_id) + ) + return result.scalar_one_or_none() + + async def create(self, role_name: str) -> Role: + """Create a new role.""" + role = Role(role_name=role_name) + self.session.add(role) + await self.session.flush() + await self.session.refresh(role) + return role + + async def get_all(self) -> List[Role]: + """ + Get all roles. + + Returns: + List of all roles + """ + result = await self.session.execute( + select(Role).order_by(Role.id) + ) + return list(result.scalars().all()) + + async def ensure_default_roles_exist(self) -> None: + """ + Ensure default roles exist in the database. + Called during application startup. + Creates roles in order: editor (1), auditor (2), admin (3) + """ + # Order matters for role IDs: editor=1, auditor=2, admin=3 + default_roles = ["editor", "auditor", "admin"] + + for role_name in default_roles: + existing = await self.get_by_name(role_name) + if existing is None: + logger.info(f"Creating default role: {role_name}") + await self.create(role_name) class UserRepository: @@ -72,6 +128,26 @@ class UserRepository: await self.session.refresh(user) return user + async def update_role(self, user_id: int, role_id: int) -> Optional[User]: + """ + Update a user's role. + + Args: + user_id: The user ID to update + role_id: The new role ID + + Returns: + The updated User, or None if not found + """ + user = await self.get_by_id(user_id) + if not user: + return None + + user.role_id = role_id + await self.session.flush() + await self.session.refresh(user) + return user + async def get_or_create_default_role(self) -> Role: """ Get the default user role, creating it if it doesn't exist. @@ -79,17 +155,12 @@ class UserRepository: Returns: The default Role """ - result = await self.session.execute( - select(Role).where(Role.role_name == DEFAULT_ROLE_NAME) - ) - role = result.scalar_one_or_none() + role_repo = RoleRepository(self.session) + role = await role_repo.get_by_name(DEFAULT_ROLE_NAME) if role is None: logger.info(f"Creating default role: {DEFAULT_ROLE_NAME}") - role = Role(role_name=DEFAULT_ROLE_NAME) - self.session.add(role) - await self.session.flush() - await self.session.refresh(role) + role = await role_repo.create(DEFAULT_ROLE_NAME) return role @@ -122,45 +193,3 @@ class UserRepository: logger.info(f"Created new user with id: {user.id}, sub: {sub}") return user, True - - -class RoleRepository: - """Repository for Role-related database operations.""" - - def __init__(self, session: AsyncSession): - self.session = session - - async def get_by_name(self, role_name: str) -> Optional[Role]: - """Get a role by name.""" - result = await self.session.execute( - select(Role).where(Role.role_name == role_name) - ) - return result.scalar_one_or_none() - - async def get_by_id(self, role_id: int) -> Optional[Role]: - """Get a role by ID.""" - result = await self.session.execute( - select(Role).where(Role.id == role_id) - ) - return result.scalar_one_or_none() - - async def create(self, role_name: str) -> Role: - """Create a new role.""" - role = Role(role_name=role_name) - self.session.add(role) - await self.session.flush() - await self.session.refresh(role) - return role - - async def ensure_default_roles_exist(self) -> None: - """ - Ensure default roles exist in the database. - Called during application startup. - """ - default_roles = ["admin", "user", "viewer"] - - for role_name in default_roles: - existing = await self.get_by_name(role_name) - if existing is None: - logger.info(f"Creating default role: {role_name}") - await self.create(role_name) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 177a83a..11c1c71 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,6 +4,7 @@ import HomePage from '@/pages/HomePage' import DashboardPage from '@/pages/DashboardPage' import RequirementsPage from '@/pages/RequirementsPage' import RequirementDetailPage from '@/pages/RequirementDetailPage' +import AdminPage from '@/pages/AdminPage' import ProtectedRoute from '@/components/ProtectedRoute' function App() { @@ -41,6 +42,14 @@ function App() { } /> + + + + } + /> ) } diff --git a/frontend/src/pages/AdminPage.tsx b/frontend/src/pages/AdminPage.tsx new file mode 100644 index 0000000..0644dca --- /dev/null +++ b/frontend/src/pages/AdminPage.tsx @@ -0,0 +1,814 @@ +import { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { useAuth, useProject } from '@/hooks' +import { + projectService, + relationshipService, + userService, + RelationshipType, + ProjectMember, + Role, +} from '@/services' + +type TabType = 'project' | 'members' | 'relationships' + +export default function AdminPage() { + const { user } = useAuth() + const { currentProject, setCurrentProject } = useProject() + const navigate = useNavigate() + + // Tab state + const [activeTab, setActiveTab] = useState('project') + + // Project settings state + const [projectName, setProjectName] = useState('') + const [projectDesc, setProjectDesc] = useState('') + const [projectLoading, setProjectLoading] = useState(false) + const [projectError, setProjectError] = useState(null) + const [projectSuccess, setProjectSuccess] = useState(null) + + // Members state + const [members, setMembers] = useState([]) + const [roles, setRoles] = useState([]) + const [membersLoading, setMembersLoading] = useState(true) + const [membersError, setMembersError] = useState(null) + const [updatingMember, setUpdatingMember] = useState(null) + + // Relationship types state + const [relationshipTypes, setRelationshipTypes] = useState([]) + const [relTypesLoading, setRelTypesLoading] = useState(true) + const [relTypesError, setRelTypesError] = useState(null) + + // Modal states + const [showCreateRelTypeModal, setShowCreateRelTypeModal] = useState(false) + const [showEditRelTypeModal, setShowEditRelTypeModal] = useState(false) + const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false) + const [selectedRelType, setSelectedRelType] = useState(null) + + // Relationship type form state + const [relTypeName, setRelTypeName] = useState('') + const [relTypeDesc, setRelTypeDesc] = useState('') + const [relTypeInverse, setRelTypeInverse] = useState('') + const [relTypeFormLoading, setRelTypeFormLoading] = useState(false) + const [relTypeFormError, setRelTypeFormError] = useState(null) + + // Check if user is admin + const isAdmin = user?.role_id === 3 + + // Redirect if not admin + useEffect(() => { + if (!isAdmin) { + navigate('/dashboard') + } + }, [isAdmin, navigate]) + + // Initialize project form + useEffect(() => { + if (currentProject) { + setProjectName(currentProject.project_name) + setProjectDesc(currentProject.project_desc || '') + } + }, [currentProject]) + + // Fetch members and roles + useEffect(() => { + const fetchMembersAndRoles = async () => { + if (!currentProject) return + + try { + setMembersLoading(true) + setMembersError(null) + + const [fetchedMembers, fetchedRoles] = await Promise.all([ + userService.getProjectMembers(currentProject.id), + userService.getRoles(), + ]) + + setMembers(fetchedMembers) + setRoles(fetchedRoles) + } catch (err) { + console.error('Failed to fetch members:', err) + setMembersError('Failed to load project members') + } finally { + setMembersLoading(false) + } + } + + fetchMembersAndRoles() + }, [currentProject]) + + // Fetch relationship types + useEffect(() => { + const fetchRelTypes = async () => { + if (!currentProject) return + + try { + setRelTypesLoading(true) + setRelTypesError(null) + + const fetchedTypes = await relationshipService.getRelationshipTypes(currentProject.id) + setRelationshipTypes(fetchedTypes) + } catch (err) { + console.error('Failed to fetch relationship types:', err) + setRelTypesError('Failed to load relationship types') + } finally { + setRelTypesLoading(false) + } + } + + fetchRelTypes() + }, [currentProject]) + + // Handle project update + const handleProjectUpdate = async (e: React.FormEvent) => { + e.preventDefault() + if (!currentProject) return + + try { + setProjectLoading(true) + setProjectError(null) + setProjectSuccess(null) + + const updatedProject = await projectService.updateProject(currentProject.id, { + project_name: projectName.trim(), + project_desc: projectDesc.trim() || undefined, + }) + + setCurrentProject(updatedProject) + setProjectSuccess('Project updated successfully!') + setTimeout(() => setProjectSuccess(null), 3000) + } catch (err) { + console.error('Failed to update project:', err) + setProjectError('Failed to update project') + } finally { + setProjectLoading(false) + } + } + + // Handle member role update + const handleRoleUpdate = async (memberId: number, newRoleId: number) => { + if (!currentProject) return + + // Prevent self-demotion + if (memberId === user?.db_user_id && newRoleId !== 3) { + setMembersError('You cannot demote yourself. Ask another admin to change your role.') + setTimeout(() => setMembersError(null), 5000) + return + } + + try { + setUpdatingMember(memberId) + setMembersError(null) + + const updatedMember = await userService.updateMemberRole( + currentProject.id, + memberId, + newRoleId + ) + + setMembers((prev) => + prev.map((m) => (m.id === memberId ? updatedMember : m)) + ) + } catch (err: any) { + console.error('Failed to update member role:', err) + setMembersError(err.message || 'Failed to update member role') + setTimeout(() => setMembersError(null), 5000) + } finally { + setUpdatingMember(null) + } + } + + // Handle create relationship type + const handleCreateRelType = async (e: React.FormEvent) => { + e.preventDefault() + if (!currentProject) return + + try { + setRelTypeFormLoading(true) + setRelTypeFormError(null) + + const newType = await relationshipService.createRelationshipType(currentProject.id, { + type_name: relTypeName.trim(), + type_description: relTypeDesc.trim() || null, + inverse_type_name: relTypeInverse.trim() || null, + }) + + setRelationshipTypes((prev) => [...prev, newType]) + setShowCreateRelTypeModal(false) + resetRelTypeForm() + } catch (err: any) { + console.error('Failed to create relationship type:', err) + setRelTypeFormError(err.message || 'Failed to create relationship type') + } finally { + setRelTypeFormLoading(false) + } + } + + // Handle update relationship type + const handleUpdateRelType = async (e: React.FormEvent) => { + e.preventDefault() + if (!currentProject || !selectedRelType) return + + try { + setRelTypeFormLoading(true) + setRelTypeFormError(null) + + const updatedType = await relationshipService.updateRelationshipType( + currentProject.id, + selectedRelType.id, + { + type_name: relTypeName.trim() || null, + type_description: relTypeDesc.trim() || null, + inverse_type_name: relTypeInverse.trim() || null, + } + ) + + setRelationshipTypes((prev) => + prev.map((t) => (t.id === selectedRelType.id ? updatedType : t)) + ) + setShowEditRelTypeModal(false) + setSelectedRelType(null) + resetRelTypeForm() + } catch (err: any) { + console.error('Failed to update relationship type:', err) + setRelTypeFormError(err.message || 'Failed to update relationship type') + } finally { + setRelTypeFormLoading(false) + } + } + + // Handle delete relationship type + const handleDeleteRelType = async () => { + if (!currentProject || !selectedRelType) return + + try { + setRelTypeFormLoading(true) + setRelTypeFormError(null) + + await relationshipService.deleteRelationshipType(currentProject.id, selectedRelType.id) + + setRelationshipTypes((prev) => prev.filter((t) => t.id !== selectedRelType.id)) + setShowDeleteConfirmModal(false) + setSelectedRelType(null) + } catch (err: any) { + console.error('Failed to delete relationship type:', err) + setRelTypeFormError(err.message || 'Failed to delete relationship type') + } finally { + setRelTypeFormLoading(false) + } + } + + // Reset relationship type form + const resetRelTypeForm = () => { + setRelTypeName('') + setRelTypeDesc('') + setRelTypeInverse('') + setRelTypeFormError(null) + } + + // Open edit modal + const openEditModal = (relType: RelationshipType) => { + setSelectedRelType(relType) + setRelTypeName(relType.type_name) + setRelTypeDesc(relType.type_description || '') + setRelTypeInverse(relType.inverse_type_name || '') + setRelTypeFormError(null) + setShowEditRelTypeModal(true) + } + + // Open delete modal + const openDeleteModal = (relType: RelationshipType) => { + setSelectedRelType(relType) + setRelTypeFormError(null) + setShowDeleteConfirmModal(true) + } + + if (!isAdmin) { + return null + } + + if (!currentProject) { + return ( +
+
+

No Project Selected

+

Please select a project from the dashboard first.

+ +
+
+ ) + } + + return ( +
+ {/* Header */} +
+

Admin Panel

+

+ Managing: {currentProject.project_name} +

+
+ + {/* Navigation */} +
+
+ +
+
+ + {/* Back Button */} +
+ +
+ + {/* Content */} +
+ {/* Project Settings Tab */} + {activeTab === 'project' && ( +
+

Project Settings

+ +
+ {projectError && ( +
+ {projectError} +
+ )} + + {projectSuccess && ( +
+ {projectSuccess} +
+ )} + +
+ + setProjectName(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" + required + /> +
+ +
+ +