Compare commits

..

No commits in common. "d0a315c365cebbfc1bd2df940c66d64eb9b56ba7" and "99e43c7b309d36e64086b6bd33100c0854ff30e8" have entirely different histories.

34 changed files with 1403 additions and 1574 deletions

View File

@ -1,6 +1,4 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from backend.models.user_model import User
from ..services import ResourceService, UserService from ..services import ResourceService, UserService
from ..models.resource_model import Resource from ..models.resource_model import Resource
@ -17,40 +15,12 @@ openapi_tags = {
# TODO: Add security using HTTP Bearer Tokens # TODO: Add security using HTTP Bearer Tokens
# TODO: Enable authorization by passing user uuid to API # TODO: Enable authorization by passing user uuid to API
# TODO: Create custom exceptions # TODO: Create custom exceptions
@api.post("", response_model=Resource, tags=["Resource"])
def create(
uuid: str, resource: Resource, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends()
):
subject = user_svc.get_user_by_uuid(uuid)
return resource_svc.create(subject, resource)
@api.get("", response_model=List[Resource], tags=["Resource"]) @api.get("", response_model=List[Resource], tags=["Resource"])
def get_all( def get_all(
uuid: str, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends() user_id: str,
resource_svc: ResourceService = Depends(),
user_svc: UserService = Depends(),
): ):
subject = user_svc.get_user_by_uuid(uuid) subject = user_svc.get_user_by_uuid(user_id)
return resource_svc.get_resource_by_user(subject) return resource_svc.get_resource_by_user(subject)
@api.get("/{name}", response_model=Resource, tags=["Resource"])
def get_by_name(
name:str, uuid:str, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends()
):
subject = user_svc.get_user_by_uuid(uuid)
return resource_svc.get_resource_by_name(name, subject)
@api.put("", response_model=Resource, tags=["Resource"])
def update(
uuid: str, resource: Resource, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends()
):
subject = user_svc.get_user_by_uuid(uuid)
return resource_svc.update(subject, resource)
@api.delete("", response_model=None, tags=["Resource"])
def delete(
uuid: str, resource: Resource, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends()
):
subject = user_svc.get_user_by_uuid(uuid)
resource_svc.delete(subject, resource)

View File

@ -1,6 +1,4 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from backend.models.user_model import User
from ..services import ServiceService, UserService from ..services import ServiceService, UserService
from ..models.service_model import Service from ..models.service_model import Service
@ -17,38 +15,12 @@ openapi_tags = {
# TODO: Add security using HTTP Bearer Tokens # TODO: Add security using HTTP Bearer Tokens
# TODO: Enable authorization by passing user uuid to API # TODO: Enable authorization by passing user uuid to API
# TODO: Create custom exceptions # TODO: Create custom exceptions
@api.post("", response_model=Service, tags=["Service"])
def create(
uuid: str, service: Service, user_svc: UserService = Depends(), service_svc: ServiceService = Depends()
):
subject = user_svc.get_user_by_uuid(uuid)
return service_svc.create(subject, service)
@api.get("", response_model=List[Service], tags=["Service"]) @api.get("", response_model=List[Service], tags=["Service"])
def get_all( def get_all(
uuid: str, user_svc: UserService = Depends(), service_svc: ServiceService = Depends() user_id: str,
service_svc: ServiceService = Depends(),
user_svc: UserService = Depends(),
): ):
subject = user_svc.get_user_by_uuid(uuid) subject = user_svc.get_user_by_uuid(user_id)
return service_svc.get_service_by_user(subject) return service_svc.get_service_by_user(subject)
@api.get("/{name}", response_model=Service, tags=["Service"])
def get_by_name(
name: str, uuid: str, user_svc: UserService = Depends(), service_svc: ServiceService = Depends()
):
subject = user_svc.get_user_by_uuid(uuid)
return service_svc.get_service_by_name(name, subject)
@api.put("", response_model=Service, tags=["Service"])
def update(
uuid: str, service: Service, user_svc: UserService = Depends(), service_svc: ServiceService = Depends()
):
subject = user_svc.get_user_by_uuid(uuid)
return service_svc.update(subject, service)
@api.delete("", response_model=None, tags=["Service"])
def delete(
uuid: str, service: Service, user_svc: UserService = Depends(), service_svc: ServiceService = Depends()
):
subject = user_svc.get_user_by_uuid(uuid)
service_svc.delete(subject, service)

View File

@ -1,51 +0,0 @@
from fastapi import APIRouter, Depends
from backend.models.tag_model import Tag
from backend.models.user_model import User
from backend.services.tag import TagService
from ..services import ResourceService, UserService
from ..models.resource_model import Resource
from typing import List
api = APIRouter(prefix="/api/tag")
openapi_tags = {
"name": "Tag",
"description": "Tag CRUD operations.",
}
# TODO: Add security using HTTP Bearer Tokens
# TODO: Enable authorization by passing user uuid to API
# TODO: Create custom exceptions
@api.post("", response_model=Tag, tags=["Tag"])
def create(
subject: User,
tag: Tag,
tag_service: TagService=Depends()
):
return tag_service.create(subject, tag)
@api.get("", response_model=List[Tag], tags=["Tag"])
def get_all(
subject: User,
tag_svc: TagService=Depends()
):
return tag_svc.get_all()
@api.put("", response_model=Tag, tags=["Tag"])
def update(
subject: User,
tag: Tag,
tag_svc: TagService=Depends()
):
return tag_svc.delete(subject, tag)
@api.delete("", response_model=None, tags=["Tag"])
def delete(
subject: User,
tag: Tag,
tag_svc: TagService=Depends()
):
tag_svc.delete(subject, tag)

View File

@ -1,147 +0,0 @@
# Synopsis
Collection of sample curl requests for api routes.
# Resources
## Get All
Given an admin UUID, gets all of the resources from ResourceEntity.
```
curl -X 'GET' \
'http://127.0.0.1:8000/api/resource?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \
-H 'accept: application/json'
```
## Get by Name
Given the name of a resource and an admin UUID, gets a resource from ResourceEntity by name.
```
curl -X 'GET' \
'http://127.0.0.1:8000/api/resource/Financial%20Empowerment%20Center?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \
-H 'accept: application/json'
```
## Create
Given an admin UUID and a new resource object, adds a resource to ResourceEntity.
```
curl -X 'POST' \
'http://127.0.0.1:8000/api/resource?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"id": 25,
"name": "algorithms and analysis textbook",
"summary": "textbook written by kevin sun for c550",
"link": "kevinsun.org",
"program": "DOMESTIC",
"created_at": "2024-11-04T20:07:31.875166"
}'
```
## Update
Given an admin UUID and a modified resource object, updates the resource with a matching ID if it exists.
```
curl -X 'PUT' \
'http://127.0.0.1:8000/api/resource?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"id": 25,
"name": "algorithms and analysis textbook",
"summary": "textbook written by the goat himself, kevin sun, for c550",
"link": "kevinsun.org",
"program": "DOMESTIC",
"created_at": "2024-11-04T20:07:31.875166"
}'
```
## Delete
Given an admin UUID and a resource object, deletes the resource with a matching ID if it exists.
```
curl -X 'DELETE' \
'http://127.0.0.1:8000/api/resource?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"id": 25,
"name": "algorithms and analysis textbook",
"summary": "textbook written by the goat himself, kevin sun, for c550",
"link": "kevinsun.org",
"program": "DOMESTIC",
"created_at": "2024-11-04T20:07:31.875166"
}'
```
# Services
## Get All
Given an admin UUID, gets all of the services from ServiceEntity.
```
curl -X 'GET' \
'http://127.0.0.1:8000/api/service?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \
-H 'accept: application/json'
```
## Get by Name
Given the name of a service and an admin UUID, gets a service from ServiceEntity by name.
```
curl -X 'GET' \
'http://127.0.0.1:8000/api/service/Shelter%20Placement?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \
-H 'accept: application/json'
```
## Create
Given an admin UUID and a new service object, adds a service to ServiceEntity.
```
curl -X 'POST' \
'http://127.0.0.1:8000/api/service?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"id": 25,
"created_at": "2024-11-04T20:07:31.890412",
"name": "c550 tutoring",
"status": "open",
"summary": "tutoring for kevin sun'\''s c550 class",
"requirements": [
"must be in c550"
],
"program": "COMMUNITY"
}'
```
## Update
Given an admin UUID and a modified service object, updates the service with a matching ID if it exists.
```
curl -X 'PUT' \
'http://127.0.0.1:8000/api/service?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"id": 25,
"created_at": "2024-11-04T20:07:31.890412",
"name": "c550 tutoring",
"status": "closed",
"summary": "tutoring for kevin sun'\''s c550 class",
"requirements": [
"must be in c550"
],
"program": "COMMUNITY"
}'
```
## Delete
Given an admin UUID and a service object, deletes the service with a matching ID if it exists.
```
curl -X 'DELETE' \
'http://127.0.0.1:8000/api/service?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"id": 25,
"created_at": "2024-11-04T20:07:31.890412",
"name": "c550 tutoring",
"status": "closed",
"summary": "tutoring for kevin sun'\''s c550 class",
"requirements": [
"must be in c550"
],
"program": "COMMUNITY"
}'
```

View File

@ -23,4 +23,3 @@ class ServiceTagEntity(EntityBase):
# relationships # relationships
service: Mapped["ServiceEntity"] = relationship(back_populates="serviceTags") service: Mapped["ServiceEntity"] = relationship(back_populates="serviceTags")
tag: Mapped["TagEntity"] = relationship(back_populates="serviceTags") tag: Mapped["TagEntity"] = relationship(back_populates="serviceTags")

View File

@ -45,7 +45,6 @@ class TagEntity(EntityBase):
return cls( return cls(
id=model.id, id=model.id,
created_at=model.created_at,
content=model.id, content=model.id,
) )
@ -59,6 +58,8 @@ class TagEntity(EntityBase):
return Tag( return Tag(
id=self.id, id=self.id,
create_at=self.created_at,
content=self.content, content=self.content,
) )

View File

@ -2,8 +2,7 @@ from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.middleware.gzip import GZipMiddleware from fastapi.middleware.gzip import GZipMiddleware
from .api import user, health, service, resource
from .api import user, health, service, resource, tag
description = """ description = """
Welcome to the **COMPASS** RESTful Application Programming Interface. Welcome to the **COMPASS** RESTful Application Programming Interface.
@ -18,13 +17,12 @@ app = FastAPI(
health.openapi_tags, health.openapi_tags,
service.openapi_tags, service.openapi_tags,
resource.openapi_tags, resource.openapi_tags,
tag.openapi_tags
], ],
) )
app.add_middleware(GZipMiddleware) app.add_middleware(GZipMiddleware)
feature_apis = [user, health, service, resource, tag] feature_apis = [user, health, service, resource]
for feature_api in feature_apis: for feature_api in feature_apis:
app.include_router(feature_api.api) app.include_router(feature_api.api)

View File

@ -12,4 +12,4 @@ class Resource(BaseModel):
summary: str = Field(..., max_length=300, description="The summary of the resource") summary: str = Field(..., max_length=300, description="The summary of the resource")
link: str = Field(..., max_length=150, description="link to the resource") link: str = Field(..., max_length=150, description="link to the resource")
program: ProgramTypeEnum program: ProgramTypeEnum
created_at: Optional[datetime] = datetime.now() created_at: Optional[datetime]

View File

@ -8,7 +8,7 @@ from .enum_for_models import ProgramTypeEnum
class Service(BaseModel): class Service(BaseModel):
id: int | None = None id: int | None = None
created_at: datetime | None = datetime.now() created_at: datetime | None = None
name: str name: str
status: str status: str
summary: str summary: str

View File

@ -10,4 +10,4 @@ class Tag(BaseModel):
content: str = Field( content: str = Field(
..., max_length=600, description="content associated with the tag" ..., max_length=600, description="content associated with the tag"
) )
created_at: datetime | None = datetime.now() created_at: datetime | None = None

View File

@ -14,5 +14,5 @@ class User(BaseModel):
group: str group: str
program: List[ProgramTypeEnum] program: List[ProgramTypeEnum]
role: UserTypeEnum role: UserTypeEnum
created_at: Optional[datetime] = datetime.now() created_at: Optional[datetime]
uuid: str | None = None uuid: str | None = None

View File

@ -20,8 +20,6 @@ class UserPermissionException(Exception):
class ServiceNotFoundException(Exception): class ServiceNotFoundException(Exception):
"""Exception for when the service being requested is not in the table.""" """Exception for when the service being requested is not in the table."""
class TagNotFoundException(Exception):
"""Exception for when the tag being requested is not in the table."""
class ProgramNotAssignedException(Exception): class ProgramNotAssignedException(Exception):
"""Exception for when the user does not have correct access for requested services.""" """Exception for when the user does not have correct access for requested services."""

View File

@ -1,12 +1,12 @@
from fastapi import Depends from fastapi import Depends
from ..database import db_session from ..database import db_session
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import and_, select from sqlalchemy import select
from ..models.resource_model import Resource from ..models.resource_model import Resource
from ..entities.resource_entity import ResourceEntity from ..entities.resource_entity import ResourceEntity
from ..models.user_model import User, UserTypeEnum from ..models.user_model import User, UserTypeEnum
from .exceptions import ProgramNotAssignedException, ResourceNotFoundException from .exceptions import ResourceNotFoundException
class ResourceService: class ResourceService:
@ -14,40 +14,25 @@ class ResourceService:
def __init__(self, session: Session = Depends(db_session)): def __init__(self, session: Session = Depends(db_session)):
self._session = session self._session = session
def get_resource_by_user(self, subject: User) -> list[Resource]: def get_resource_by_user(self, subject: User):
"""Resource method getting all of the resources that a user has access to based on role""" """Resource method getting all of the resources that a user has access to based on role"""
if subject.role != UserTypeEnum.VOLUNTEER: if subject.role != UserTypeEnum.VOLUNTEER:
query = select(ResourceEntity) query = select(ResourceEntity)
entities = self._session.scalars(query).all() entities = self._session.scalars(query).all()
return [resource.to_model() for resource in entities] return [resource.to_model() for resource in entities]
else: else:
programs = subject.program programs = subject.program
resources = [] resources = []
for program in programs: for program in programs:
entities = ( query = select(ResourceEntity).filter(ResourceEntity.program == program)
self._session.query(ResourceEntity) entities = self._session.scalars(query).all()
.where(ResourceEntity.program == program)
.all()
)
for entity in entities: for entity in entities:
resources.append(entity.to_model()) resources.append(entity)
return [resource for resource in resources]
def get_resource_by_name(self, name: str, subject: User) -> Resource: return [resource.to_model() for resource in resources]
"""Get a resource by name."""
query = select(ResourceEntity).where(
and_(
ResourceEntity.name == name, ResourceEntity.program.in_(subject.program)
)
)
entity = self._session.scalars(query).one_or_none()
if entity is None:
raise ResourceNotFoundException(
f"Resource with name: {name} does not exist or program has not been assigned."
)
return entity.to_model()
def create(self, subject: User, resource: Resource) -> Resource: def create(self, user: User, resource: Resource) -> Resource:
""" """
Creates a resource based on the input object and adds it to the table if the user has the right permissions. Creates a resource based on the input object and adds it to the table if the user has the right permissions.
Parameters: Parameters:
@ -56,16 +41,43 @@ class ResourceService:
Returns: Returns:
Resource: Object added to table Resource: Object added to table
""" """
if subject.role != UserTypeEnum.ADMIN: if user.role != UserTypeEnum.ADMIN:
raise ProgramNotAssignedException( raise PermissionError(
f"User is not {UserTypeEnum.ADMIN}, cannot update service" "User does not have permission to add resources in this program."
) )
resource_entity = ResourceEntity.from_model(resource) resource_entity = ResourceEntity.from_model(resource)
self._session.add(resource_entity) self._session.add(resource_entity)
self._session.commit() self._session.commit()
return resource_entity.to_model() return resource_entity.to_model()
def update(self, subject: User, resource: Resource) -> Resource: def get_by_id(self, user: User, id: int) -> Resource:
"""
Gets a resource based on the resource id that the user has access to
Parameters:
user: a valid User model representing the currently logged in User
id: int, the id of the resource
Returns:
Resource
Raises:
ResourceNotFoundException: If no resource is found with id
"""
resource = (
self._session.query(ResourceEntity)
.filter(
ResourceEntity.id == id,
ResourceEntity.program.in_(user.program),
)
.one_or_none()
)
if resource is None:
raise ResourceNotFoundException(f"No resource found with id: {id}")
return resource.to_model()
def update(self, user: User, resource: ResourceEntity) -> Resource:
""" """
Update the resource if the user has access Update the resource if the user has access
Parameters: Parameters:
@ -76,24 +88,28 @@ class ResourceService:
Raises: Raises:
ResourceNotFoundException: If no resource is found with the corresponding ID ResourceNotFoundException: If no resource is found with the corresponding ID
""" """
if subject.role != UserTypeEnum.ADMIN: if user.role != UserTypeEnum.ADMIN:
raise ProgramNotAssignedException( raise PermissionError(
f"User is not {UserTypeEnum.ADMIN}, cannot update service" "User does not have permission to update this resource."
) )
query = select(ResourceEntity).where(ResourceEntity.id == resource.id)
entity = self._session.scalars(query).one_or_none() obj = self._session.get(ResourceEntity, resource.id) if resource.id else None
if entity is None:
if obj is None:
raise ResourceNotFoundException( raise ResourceNotFoundException(
f"No resource found with matching id: {resource.id}" f"No resource found with matching id: {resource.id}"
) )
entity.name = resource.name
entity.summary = resource.summary
entity.link = resource.link
entity.program = resource.program
self._session.commit()
return entity.to_model()
def delete(self, subject: User, resource: Resource) -> None: obj.name = resource.name
obj.summary = resource.summary
obj.link = resource.link
obj.program = resource.program
self._session.commit()
return obj.to_model()
def delete(self, user: User, id: int) -> None:
""" """
Delete resource based on id that the user has access to Delete resource based on id that the user has access to
Parameters: Parameters:
@ -102,17 +118,23 @@ class ResourceService:
Raises: Raises:
ResourceNotFoundException: If no resource is found with the corresponding id ResourceNotFoundException: If no resource is found with the corresponding id
""" """
if subject.role != UserTypeEnum.ADMIN: if user.role != UserTypeEnum.ADMIN:
raise ProgramNotAssignedException( raise PermissionError(
f"User is not {UserTypeEnum.ADMIN}, cannot update service" "User does not have permission to delete this resource."
) )
query = select(ResourceEntity).where(ResourceEntity.id == resource.id)
entity = self._session.scalars(query).one_or_none() resource = (
if entity is None: self._session.query(ResourceEntity)
raise ResourceNotFoundException( .filter(
f"No resource found with matching id: {resource.id}" ResourceEntity.id == id,
) )
self._session.delete(entity) .one_or_none()
)
if resource is None:
raise ResourceNotFoundException(f"No resource found with matching id: {id}")
self._session.delete(resource)
self._session.commit() self._session.commit()
def get_by_slug(self, user: User, search_string: str) -> list[Resource]: def get_by_slug(self, user: User, search_string: str) -> list[Resource]:
@ -128,7 +150,7 @@ class ResourceService:
""" """
query = select(ResourceEntity).where( query = select(ResourceEntity).where(
ResourceEntity.name.ilike(f"%{search_string}%"), ResourceEntity.name.ilike(f"%{search_string}%"),
ResourceEntity.program.in_(user.program), ResourceEntity.program.in_(user.program)
) )
entities = self._session.scalars(query).all() entities = self._session.scalars(query).all()

View File

@ -19,34 +19,62 @@ class ServiceService:
def __init__(self, session: Session = Depends(db_session)): def __init__(self, session: Session = Depends(db_session)):
self._session = session self._session = session
def get_service_by_user(self, subject: User) -> list[Service]: def get_service_by_program(self, program: ProgramTypeEnum) -> list[Service]:
"""Resource method getting all of the resources that a user has access to based on role""" """Service method getting services belonging to a particular program."""
query = select(ServiceEntity).filter(ServiceEntity.program == program)
entities = self._session.scalars(query)
return [entity.to_model() for entity in entities]
def get_service_by_id(self, id: int) -> Service:
"""Service method getting services by id."""
query = select(ServiceEntity).filter(ServiceEntity.id == id)
entity = self._session.scalars(query).one_or_none()
if entity is None:
raise ServiceNotFoundException(f"Service with id: {id} does not exist")
return entity.to_model()
def get_service_by_name(self, name: str) -> Service:
"""Service method getting services by id."""
query = select(ServiceEntity).filter(ServiceEntity.name == name)
entity = self._session.scalars(query).one_or_none()
if entity is None:
raise ServiceNotFoundException(f"Service with name: {name} does not exist")
return entity.to_model()
def get_service_by_user(self, subject: User):
"""Service method getting all of the services that a user has access to based on role"""
if subject.role != UserTypeEnum.VOLUNTEER: if subject.role != UserTypeEnum.VOLUNTEER:
query = select(ServiceEntity) query = select(ServiceEntity)
entities = self._session.scalars(query).all() entities = self._session.scalars(query).all()
return [service.to_model() for service in entities] return [service.to_model() for service in entities]
else: else:
programs = subject.program programs = subject.program
services = [] services = []
for program in programs: for program in programs:
entities = self._session.query(ServiceEntity).where(ServiceEntity.program == program).all() query = select(ServiceEntity).filter(ServiceEntity.program == program)
entities = self._session.scalars(query).all()
for entity in entities: for entity in entities:
services.append(entity.to_model()) services.append(entity)
return [service for service in services]
def get_service_by_name(self, name: str, subject: User) -> Service: return [service.to_model() for service in services]
"""Service method getting services by id."""
query = select(ServiceEntity).where( def get_all(self, subject: User) -> list[Service]:
and_( """Service method retrieving all of the services in the table."""
ServiceEntity.name == name, ServiceEntity.program.in_(subject.program) if subject.role == UserTypeEnum.VOLUNTEER:
raise ProgramNotAssignedException(
f"User is not {UserTypeEnum.ADMIN} or {UserTypeEnum.VOLUNTEER}, cannot get all"
) )
)
entity = self._session.scalars(query).one_or_none()
if entity is None: query = select(ServiceEntity)
raise ServiceNotFoundException(f"Service with name: {name} does not exist or program has not been assigned") entities = self._session.scalars(query).all()
return entity.to_model() return [service.to_model() for service in entities]
def create(self, subject: User, service: Service) -> Service: def create(self, subject: User, service: Service) -> Service:
"""Creates/adds a service to the table.""" """Creates/adds a service to the table."""
@ -67,35 +95,33 @@ class ServiceService:
f"User is not {UserTypeEnum.ADMIN}, cannot update service" f"User is not {UserTypeEnum.ADMIN}, cannot update service"
) )
query = select(ServiceEntity).where(ServiceEntity.id == service.id) service_entity = self._session.get(ServiceEntity, service.id)
entity = self._session.scalars(query).one_or_none()
if entity is None: if service_entity is None:
raise ServiceNotFoundException( raise ServiceNotFoundException(
"The service you are searching for does not exist." "The service you are searching for does not exist."
) )
entity.name = service.name service_entity.name = service.name
entity.status = service.status service_entity.status = service.status
entity.summary = service.summary service_entity.summary = service.summary
entity.requirements = service.requirements service_entity.requirements = service.requirements
entity.program = service.program service_entity.program = service.program
self._session.commit() self._session.commit()
return entity.to_model() return service_entity.to_model()
def delete(self, subject: User, service: Service) -> None: def delete(self, subject: User, service: Service) -> None:
"""Deletes a service from the table.""" """Deletes a service from the table."""
if subject.role != UserTypeEnum.ADMIN: if subject.role != UserTypeEnum.ADMIN:
raise ProgramNotAssignedException(f"User is not {UserTypeEnum.ADMIN}") raise ProgramNotAssignedException(f"User is not {UserTypeEnum.ADMIN}")
service_entity = self._session.get(ServiceEntity, service.id)
query = select(ServiceEntity).where(ServiceEntity.id == service.id) if service_entity is None:
entity = self._session.scalars(query).one_or_none()
if entity is None:
raise ServiceNotFoundException( raise ServiceNotFoundException(
"The service you are searching for does not exist." "The service you are searching for does not exist."
) )
self._session.delete(entity) self._session.delete(service_entity)
self._session.commit() self._session.commit()

View File

@ -1,52 +1,20 @@
from fastapi import Depends from fastapi import Depends
from backend.models.enum_for_models import UserTypeEnum
from backend.models.user_model import User
from backend.services.exceptions import TagNotFoundException
from ..database import db_session from ..database import db_session
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ..models.tag_model import Tag from ..models.tag_model import Tag
from ..entities.tag_entity import TagEntity from ..entities.tag_entity import TagEntity
from sqlalchemy import select from sqlalchemy import select
# Add in checks for user permission?
class TagService: class TagService:
def __init__(self, session: Session = Depends(db_session)): def __init__(self, session: Session = Depends(db_session)):
self._session = session self._session = session
def get_all(self) -> list[Tag]: def all(self) -> list[Tag]:
"""Returns a list of all Tags""" """Returns a list of all Tags"""
query = select(TagEntity) query = select(TagEntity)
entities = self._session.scalars(query).all() entities = self._session.scalars(query).all()
return [entity.to_model() for entity in entities] return [entity.to_model() for entity in entities]
def create(self, subject: User, tag: Tag) -> Tag:
entity = TagEntity.from_model(tag)
self._session.add(entity)
self._session.commit()
return entity.to_model()
def update(self, subject: User, tag: Tag) -> Tag:
query = select(TagEntity).where(TagEntity.id == tag.id)
entity = self._session.scalars(query).one_or_none()
if entity is None:
raise TagNotFoundException(f"Tag with id {tag.id} does not exist")
entity.content = tag.content
self._session.commit()
return entity.to_model()
def delete(self, subject: User, tag: Tag) -> None:
query = select(TagEntity).where(TagEntity.id == tag.id)
entity = self._session.scalars(query).one_or_none()
if entity is None:
raise TagNotFoundException(f"Tag with id {tag.id} does not exist")
self._session.delete(entity)
self._session.commit()

View File

@ -2,6 +2,7 @@
import Sidebar from "@/components/Sidebar/Sidebar"; import Sidebar from "@/components/Sidebar/Sidebar";
import React, { useState } from "react"; import React, { useState } from "react";
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
import { createClient } from "@/utils/supabase/client"; import { createClient } from "@/utils/supabase/client";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
@ -13,7 +14,7 @@ export default function RootLayout({
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const router = useRouter(); const router = useRouter();
const [user, setUser] = useState<User>(); const [user, setUser] = useState<User>();
@ -55,13 +56,34 @@ export default function RootLayout({
<div className="flex-row"> <div className="flex-row">
{user ? ( {user ? (
<div> <div>
<Sidebar {/* button to open sidebar */}
setIsSidebarOpen={setIsSidebarOpen} <button
isSidebarOpen={isSidebarOpen} onClick={() => setIsSidebarOpen(!isSidebarOpen)}
name={user.username} className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`}
email={user.email} aria-label={"Open sidebar"}
isAdmin={user.role === Role.ADMIN} >
/> {
!isSidebarOpen && (
<ChevronDoubleRightIcon className="h-5 w-5" />
) // Icon for closing the sidebar
}
</button>
{/* sidebar */}
<div
className={`absolute inset-y-0 left-0 transform ${
isSidebarOpen
? "translate-x-0"
: "-translate-x-full"
} w-64 transition duration-300 ease-in-out`}
>
<Sidebar
setIsSidebarOpen={setIsSidebarOpen}
name={user.username}
email={user.email}
isAdmin={user.role === Role.ADMIN}
/>
</div>
{/* page ui */}
<div <div
className={`flex-1 transition duration-300 ease-in-out ${ className={`flex-1 transition duration-300 ease-in-out ${
isSidebarOpen ? "ml-64" : "ml-0" isSidebarOpen ? "ml-64" : "ml-0"

View File

@ -9,7 +9,7 @@ export async function GET(request: Request) {
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const uuid = searchParams.get("uuid"); const uuid = searchParams.get("uuid");
const data = await fetch(`${apiEndpoint}?uuid=${uuid}`); const data = await fetch(`${apiEndpoint}?user_id=${uuid}`);
const resourceData: Resource[] = await data.json(); const resourceData: Resource[] = await data.json();
// TODO: Remove make every resource visible // TODO: Remove make every resource visible

View File

@ -9,7 +9,7 @@ export async function GET(request: Request) {
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const uuid = searchParams.get("uuid"); const uuid = searchParams.get("uuid");
const data = await fetch(`${apiEndpoint}?uuid=${uuid}`); const data = await fetch(`${apiEndpoint}?user_id=${uuid}`);
const serviceData: Service[] = await data.json(); const serviceData: Service[] = await data.json();
// TODO: Remove make every service visible // TODO: Remove make every service visible

View File

@ -1,6 +1,7 @@
"use client"; "use client";
import Sidebar from "@/components/Sidebar/Sidebar"; import Sidebar from "@/components/Sidebar/Sidebar";
import React, { useState } from "react"; import React, { useState } from "react";
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
import { createClient } from "@/utils/supabase/client"; import { createClient } from "@/utils/supabase/client";
import { useEffect } from "react"; import { useEffect } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@ -12,7 +13,7 @@ export default function RootLayout({
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [user, setUser] = useState<User>(); const [user, setUser] = useState<User>();
const router = useRouter(); const router = useRouter();
@ -44,13 +45,34 @@ export default function RootLayout({
<div className="flex-row"> <div className="flex-row">
{user ? ( {user ? (
<div> <div>
<Sidebar {/* button to open sidebar */}
name={user.username} <button
email={user.email} onClick={() => setIsSidebarOpen(!isSidebarOpen)}
setIsSidebarOpen={setIsSidebarOpen} className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`}
isSidebarOpen={isSidebarOpen} aria-label={"Open sidebar"}
isAdmin={user.role === Role.ADMIN} >
/> {
!isSidebarOpen && (
<ChevronDoubleRightIcon className="h-5 w-5" />
) // Icon for closing the sidebar
}
</button>
{/* sidebar */}
<div
className={`absolute inset-y-0 left-0 transform ${
isSidebarOpen
? "translate-x-0"
: "-translate-x-full"
} w-64 transition duration-300 ease-in-out`}
>
<Sidebar
name={user.username}
email={user.email}
setIsSidebarOpen={setIsSidebarOpen}
isAdmin={user.role === Role.ADMIN}
/>
</div>
{/* page ui */}
<div <div
className={`flex-1 transition duration-300 ease-in-out ${ className={`flex-1 transition duration-300 ease-in-out ${
isSidebarOpen ? "ml-64" : "ml-0" isSidebarOpen ? "ml-64" : "ml-0"

View File

@ -2,6 +2,7 @@
import Sidebar from "@/components/Sidebar/Sidebar"; import Sidebar from "@/components/Sidebar/Sidebar";
import React, { useState } from "react"; import React, { useState } from "react";
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
import { createClient } from "@/utils/supabase/client"; import { createClient } from "@/utils/supabase/client";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
@ -13,7 +14,7 @@ export default function RootLayout({
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const router = useRouter(); const router = useRouter();
const [user, setUser] = useState<User>(); const [user, setUser] = useState<User>();
@ -47,14 +48,33 @@ export default function RootLayout({
<div className="flex-row"> <div className="flex-row">
{user ? ( {user ? (
<div> <div>
<Sidebar {/* button to open sidebar */}
setIsSidebarOpen={setIsSidebarOpen} <button
isSidebarOpen={isSidebarOpen} onClick={() => setIsSidebarOpen(!isSidebarOpen)}
name={user.username} className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`}
email={user.email} aria-label={"Open sidebar"}
isAdmin={user.role === Role.ADMIN} >
/> {
{/* </div>*/} !isSidebarOpen && (
<ChevronDoubleRightIcon className="h-5 w-5" />
) // Icon for closing the sidebar
}
</button>
{/* sidebar */}
<div
className={`absolute inset-y-0 left-0 transform ${
isSidebarOpen
? "translate-x-0"
: "-translate-x-full"
} w-64 transition duration-300 ease-in-out`}
>
<Sidebar
setIsSidebarOpen={setIsSidebarOpen}
name={user.username}
email={user.email}
isAdmin={user.role === Role.ADMIN}
/>
</div>
{/* page ui */} {/* page ui */}
<div <div
className={`flex-1 transition duration-300 ease-in-out ${ className={`flex-1 transition duration-300 ease-in-out ${

View File

@ -2,6 +2,7 @@
import Sidebar from "@/components/Sidebar/Sidebar"; import Sidebar from "@/components/Sidebar/Sidebar";
import React, { useState } from "react"; import React, { useState } from "react";
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
import { createClient } from "@/utils/supabase/client"; import { createClient } from "@/utils/supabase/client";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
@ -13,7 +14,7 @@ export default function RootLayout({
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const router = useRouter(); const router = useRouter();
const [user, setUser] = useState<User>(); const [user, setUser] = useState<User>();
@ -47,13 +48,34 @@ export default function RootLayout({
<div className="flex-row"> <div className="flex-row">
{user ? ( {user ? (
<div> <div>
<Sidebar {/* button to open sidebar */}
setIsSidebarOpen={setIsSidebarOpen} <button
isSidebarOpen={isSidebarOpen} onClick={() => setIsSidebarOpen(!isSidebarOpen)}
name={user.username} className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`}
email={user.email} aria-label={"Open sidebar"}
isAdmin={user.role === Role.ADMIN} >
/> {
!isSidebarOpen && (
<ChevronDoubleRightIcon className="h-5 w-5" />
) // Icon for closing the sidebar
}
</button>
{/* sidebar */}
<div
className={`absolute inset-y-0 left-0 transform ${
isSidebarOpen
? "translate-x-0"
: "-translate-x-full"
} w-64 transition duration-300 ease-in-out`}
>
<Sidebar
setIsSidebarOpen={setIsSidebarOpen}
name={user.username}
email={user.email}
isAdmin={user.role === Role.ADMIN}
/>
</div>
{/* page ui */}
<div <div
className={`flex-1 transition duration-300 ease-in-out ${ className={`flex-1 transition duration-300 ease-in-out ${
isSidebarOpen ? "ml-64" : "ml-0" isSidebarOpen ? "ml-64" : "ml-0"

View File

@ -45,11 +45,13 @@ const Drawer: FunctionComponent<DrawerProps> = ({
const [tempRowContent, setTempRowContent] = useState(rowContent); const [tempRowContent, setTempRowContent] = useState(rowContent);
const onRowUpdate = (updatedRow: any) => { const onRowUpdate = (updatedRow: any) => {
setData((prevData: any) => setData((prevData: any) => (
prevData.map((row: any) => prevData.map((row: any) => (
row.id === updatedRow.id ? updatedRow : row row.id === updatedRow.id
) ? updatedRow
); : row
))
))
}; };
const handleTempRowContentChange = (e) => { const handleTempRowContentChange = (e) => {

View File

@ -2,7 +2,6 @@ import React from "react";
import { import {
HomeIcon, HomeIcon,
ChevronDoubleLeftIcon, ChevronDoubleLeftIcon,
ChevronDoubleRightIcon,
BookmarkIcon, BookmarkIcon,
ClipboardIcon, ClipboardIcon,
BookOpenIcon, BookOpenIcon,
@ -13,7 +12,6 @@ import { UserProfile } from "../resource/UserProfile";
interface SidebarProps { interface SidebarProps {
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>; setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>;
isSidebarOpen: boolean;
name: string; name: string;
email: string; email: string;
isAdmin: boolean; isAdmin: boolean;
@ -21,96 +19,70 @@ interface SidebarProps {
const Sidebar: React.FC<SidebarProps> = ({ const Sidebar: React.FC<SidebarProps> = ({
setIsSidebarOpen, setIsSidebarOpen,
isSidebarOpen,
name, name,
email, email,
isAdmin: admin, isAdmin: admin,
}) => { }) => {
return ( return (
<> <div className="w-64 h-full border border-gray-200 bg-gray-50 px-4">
{/* Button to open the sidebar. */} {/* button to close sidebar */}
<button <div className="flex justify-end">
onClick={() => setIsSidebarOpen(true)} <button
className={ onClick={() => setIsSidebarOpen(false)}
"fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0" className="py-2 text-gray-500 hover:text-gray-800"
} aria-label="Close sidebar"
aria-label={"Open sidebar"} >
> <ChevronDoubleLeftIcon className="h-5 w-5" />
{!isSidebarOpen && ( </button>
<ChevronDoubleRightIcon className="h-5 w-5" /> </div>
)} <div className="flex flex-col space-y-8">
</button> {/* user + logout button */}
<div className="flex items-center p-4 space-x-2 border border-gray-200 rounded-md ">
{/* The sidebar itself. */} <UserProfile name={name} email={email} />
<div
className={
"fixed left-0 w-64 h-full border border-gray-200 bg-gray-50 px-4 " +
(isSidebarOpen
? "translate-x-0" // Open
: "-translate-x-full opacity-25") + // Closed
" transition duration-300 ease-out" // More animation properties
}
>
{/* Button to close sidebar */}
<div className="flex justify-end">
<button
onClick={() => setIsSidebarOpen(false)}
className="py-2 text-gray-500 hover:text-gray-800"
aria-label="Close sidebar"
>
<ChevronDoubleLeftIcon className="h-5 w-5" />
</button>
</div> </div>
{/* navigation menu */}
<div className="flex flex-col space-y-2">
<h4 className="text-xs font-semibold text-gray-500">
Pages
</h4>
<nav className="flex flex-col">
{admin && (
<SidebarItem
icon={<LockClosedIcon />}
text="Admin"
active={true}
redirect="/admin"
/>
)}
<div className="flex flex-col space-y-8"> <SidebarItem
{/* user + logout button */} icon={<HomeIcon />}
<div className="flex items-center p-4 space-x-2 border border-gray-200 rounded-md "> text="Home"
<UserProfile name={name} email={email} /> active={true}
</div> redirect="/home"
{/* navigation menu */} />
<div className="flex flex-col space-y-2"> <SidebarItem
<h4 className="text-xs font-semibold text-gray-500"> icon={<BookmarkIcon />}
Pages text="Resources"
</h4> active={true}
<nav className="flex flex-col"> redirect="/resource"
{admin && ( />
<SidebarItem <SidebarItem
icon={<LockClosedIcon />} icon={<ClipboardIcon />}
text="Admin" text="Services"
active={true} active={true}
redirect="/admin" redirect="/service"
/> />
)} <SidebarItem
icon={<BookOpenIcon />}
<SidebarItem text="Training Manuals"
icon={<HomeIcon />} active={true}
text="Home" redirect="/training-manuals"
active={true} />
redirect="/home" </nav>
/>
<SidebarItem
icon={<BookmarkIcon />}
text="Resources"
active={true}
redirect="/resource"
/>
<SidebarItem
icon={<ClipboardIcon />}
text="Services"
active={true}
redirect="/service"
/>
<SidebarItem
icon={<BookOpenIcon />}
text="Training Manuals"
active={true}
redirect="/training-manuals"
/>
</nav>
</div>
</div> </div>
</div> </div>
</> </div>
); );
}; };

View File

@ -8,20 +8,24 @@ import TagsInput from "@/components/TagsInput/Index";
import Resource from "@/utils/models/Resource"; import Resource from "@/utils/models/Resource";
type ResourceTableProps = { type ResourceTableProps = {
data: Resource[]; data: Resource[],
setData: Dispatch<SetStateAction<Resource[]>>; setData: Dispatch<SetStateAction<Resource[]>>
}; }
/** /**
* Table componenet used for displaying resources * Table componenet used for displaying resources
* @param props.data Stateful list of resources to be displayed by the table * @param props.data Stateful list of resources to be displayed by the table
* @param props.setData State setter for the list of resources * @param props.setData State setter for the list of resources
*/ */
export default function ResourceTable({ data, setData }: ResourceTableProps) { export default function ResourceTable({ data, setData }: ResourceTableProps ) {
const columnHelper = createColumnHelper<Resource>(); const columnHelper = createColumnHelper<Resource>();
// Set up tag handling // Set up tag handling
const programProps = useTagsHandler(["community", "domestic", "economic"]); const programProps = useTagsHandler([
"community",
"domestic",
"economic",
])
// Define Tanstack columns // Define Tanstack columns
const columns: ColumnDef<Resource, any>[] = [ const columns: ColumnDef<Resource, any>[] = [
@ -62,7 +66,10 @@ export default function ResourceTable({ data, setData }: ResourceTableProps) {
</> </>
), ),
cell: (info) => ( cell: (info) => (
<TagsInput presetValue={info.getValue()} {...programProps} /> <TagsInput
presetValue={info.getValue()}
{...programProps}
/>
), ),
}), }),
@ -78,5 +85,5 @@ export default function ResourceTable({ data, setData }: ResourceTableProps) {
}), }),
]; ];
return <Table data={data} setData={setData} columns={columns} />; return <Table data={data} setData={setData} columns={columns}/>
} }

View File

@ -3,16 +3,12 @@ import DataPoint from "@/utils/models/DataPoint";
import { Dispatch, SetStateAction, useState } from "react"; import { Dispatch, SetStateAction, useState } from "react";
type RowOpenActionProps<T extends DataPoint> = { type RowOpenActionProps<T extends DataPoint> = {
title: string; title: string,
rowData: T; rowData: T,
setData: Dispatch<SetStateAction<T[]>>; setData: Dispatch<SetStateAction<T[]>>
}; }
export function RowOpenAction<T extends DataPoint>({ export function RowOpenAction<T extends DataPoint>({ title, rowData, setData }: RowOpenActionProps<T>) {
title,
rowData,
setData,
}: RowOpenActionProps<T>) {
const [pageContent, setPageContent] = useState(""); const [pageContent, setPageContent] = useState("");
const handleDrawerContentChange = (newContent: string) => { const handleDrawerContentChange = (newContent: string) => {
@ -35,4 +31,4 @@ export function RowOpenAction<T extends DataPoint>({
</span> </span>
</div> </div>
); );
} };

View File

@ -8,31 +8,35 @@ import TagsInput from "@/components/TagsInput/Index";
import Service from "@/utils/models/Service"; import Service from "@/utils/models/Service";
type ServiceTableProps = { type ServiceTableProps = {
data: Service[]; data: Service[],
setData: Dispatch<SetStateAction<Service[]>>; setData: Dispatch<SetStateAction<Service[]>>
}; }
/** /**
* Table componenet used for displaying services * Table componenet used for displaying services
* @param props.data Stateful list of services to be displayed by the table * @param props.data Stateful list of services to be displayed by the table
* @param props.setData State setter for the list of services * @param props.setData State setter for the list of services
*/ */
export default function ServiceTable({ data, setData }: ServiceTableProps) { export default function ServiceTable({ data, setData }: ServiceTableProps ) {
const columnHelper = createColumnHelper<Service>(); const columnHelper = createColumnHelper<Service>();
// Set up tag handling // Set up tag handling
const programProps = useTagsHandler(["community", "domestic", "economic"]); const programProps = useTagsHandler([
"community",
"domestic",
"economic",
])
// TODO: Dynamically or statically get full list of preset requirement tag options // TODO: Dynamically or statically get full list of preset requirement tag options
const requirementProps = useTagsHandler([ const requirementProps = useTagsHandler([
"anonymous", 'anonymous',
"confidential", 'confidential',
"referral required", 'referral required',
"safety assessment", 'safety assessment',
"intake required", 'intake required',
"income eligibility", 'income eligibility',
"initial assessment", 'initial assessment',
]); ])
// Define Tanstack columns // Define Tanstack columns
const columns: ColumnDef<Service, any>[] = [ const columns: ColumnDef<Service, any>[] = [
@ -67,7 +71,10 @@ export default function ServiceTable({ data, setData }: ServiceTableProps) {
</> </>
), ),
cell: (info) => ( cell: (info) => (
<TagsInput presetValue={info.getValue()} {...programProps} /> <TagsInput
presetValue={info.getValue()}
{...programProps}
/>
), ),
}), }),
columnHelper.accessor("requirements", { columnHelper.accessor("requirements", {
@ -79,9 +86,7 @@ export default function ServiceTable({ data, setData }: ServiceTableProps) {
cell: (info) => ( cell: (info) => (
// TODO: Setup different tag handler for requirements // TODO: Setup different tag handler for requirements
<TagsInput <TagsInput
presetValue={ presetValue={info.getValue()[0] !== "" ? info.getValue() : ["N/A"]}
info.getValue()[0] !== "" ? info.getValue() : ["N/A"]
}
{...requirementProps} {...requirementProps}
/> />
), ),
@ -99,5 +104,5 @@ export default function ServiceTable({ data, setData }: ServiceTableProps) {
}), }),
]; ];
return <Table data={data} setData={setData} columns={columns} />; return <Table data={data} setData={setData} columns={columns} />
} };

View File

@ -4,15 +4,15 @@ import {
useReactTable, useReactTable,
getCoreRowModel, getCoreRowModel,
flexRender, flexRender,
createColumnHelper, createColumnHelper
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { import {
ChangeEvent, ChangeEvent,
useState, useState,
useEffect, useEffect,
Key, Key,
Dispatch, Dispatch,
SetStateAction, SetStateAction
} from "react"; } from "react";
import { TableAction } from "./TableAction"; import { TableAction } from "./TableAction";
import { PlusIcon } from "@heroicons/react/24/solid"; import { PlusIcon } from "@heroicons/react/24/solid";
@ -21,9 +21,9 @@ import { RowOptionMenu } from "./RowOptionMenu";
import DataPoint from "@/utils/models/DataPoint"; import DataPoint from "@/utils/models/DataPoint";
type TableProps<T extends DataPoint> = { type TableProps<T extends DataPoint> = {
data: T[]; data: T[],
setData: Dispatch<SetStateAction<T[]>>; setData: Dispatch<SetStateAction<T[]>>,
columns: ColumnDef<T, any>[]; columns: ColumnDef<T, any>[]
}; };
/** Fuzzy search function */ /** Fuzzy search function */
@ -48,21 +48,20 @@ const fuzzyFilter = (
* @param props.data Stateful list of data to be held in the table * @param props.data Stateful list of data to be held in the table
* @param props.setData State setter for the list of data * @param props.setData State setter for the list of data
* @param props.columns Column definitions made with Tanstack columnHelper * @param props.columns Column definitions made with Tanstack columnHelper
*/ */
export default function Table<T extends DataPoint>({ export default function Table<T extends DataPoint>({ data, setData, columns }: TableProps<T>) {
data,
setData,
columns,
}: TableProps<T>) {
const columnHelper = createColumnHelper<T>(); const columnHelper = createColumnHelper<T>();
/** Sorting function based on visibility */ /** Sorting function based on visibility */
const visibilitySort = (a: T, b: T) => const visibilitySort = (a: T, b: T) => (
a.visible === b.visible ? 0 : a.visible ? -1 : 1; a.visible === b.visible
? 0
: a.visible ? -1 : 1
)
// Sort data on load // Sort data on load
useEffect(() => { useEffect(() => {
setData((prevData) => prevData.sort(visibilitySort)); setData(prevData => prevData.sort(visibilitySort))
}, [setData]); }, [setData]);
// Data manipulation methods // Data manipulation methods
@ -76,13 +75,13 @@ export default function Table<T extends DataPoint>({
const hideData = (dataId: number) => { const hideData = (dataId: number) => {
console.log(`Toggling visibility for data with ID: ${dataId}`); console.log(`Toggling visibility for data with ID: ${dataId}`);
setData((currentData) => { setData(currentData => {
const newData = currentData const newData = currentData
.map((data) => .map(data => (
data.id === dataId data.id === dataId
? { ...data, visible: !data.visible } ? { ...data, visible: !data.visible }
: data : data
) ))
.sort(visibilitySort); .sort(visibilitySort);
console.log(newData); console.log(newData);
@ -105,7 +104,7 @@ export default function Table<T extends DataPoint>({
/> />
), ),
}) })
); )
// Searching // Searching
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
@ -222,4 +221,4 @@ export default function Table<T extends DataPoint>({
</table> </table>
</div> </div>
); );
} };

View File

@ -1,8 +1,4 @@
import { import { ArrowDownCircleIcon, AtSymbolIcon, Bars2Icon } from "@heroicons/react/24/solid";
ArrowDownCircleIcon,
AtSymbolIcon,
Bars2Icon,
} from "@heroicons/react/24/solid";
import { Dispatch, SetStateAction } from "react"; import { Dispatch, SetStateAction } from "react";
import useTagsHandler from "@/components/TagsInput/TagsHandler"; import useTagsHandler from "@/components/TagsInput/TagsHandler";
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
@ -12,16 +8,16 @@ import TagsInput from "@/components/TagsInput/Index";
import User from "@/utils/models/User"; import User from "@/utils/models/User";
type UserTableProps = { type UserTableProps = {
data: User[]; data: User[],
setData: Dispatch<SetStateAction<User[]>>; setData: Dispatch<SetStateAction<User[]>>
}; }
/** /**
* Table componenet used for displaying users * Table componenet used for displaying users
* @param props.data Stateful list of users to be displayed by the table * @param props.data Stateful list of users to be displayed by the table
* @param props.setData State setter for the list of users * @param props.setData State setter for the list of users
*/ */
export default function UserTable({ data, setData }: UserTableProps) { export default function UserTable({ data, setData }: UserTableProps ) {
const columnHelper = createColumnHelper<User>(); const columnHelper = createColumnHelper<User>();
// Set up tag handling // Set up tag handling
@ -29,9 +25,13 @@ export default function UserTable({ data, setData }: UserTableProps) {
"administrator", "administrator",
"volunteer", "volunteer",
"employee", "employee",
]); ])
const programProps = useTagsHandler(["community", "domestic", "economic"]); const programProps = useTagsHandler([
"community",
"domestic",
"economic",
])
// Define Tanstack columns // Define Tanstack columns
const columns: ColumnDef<User, any>[] = [ const columns: ColumnDef<User, any>[] = [
@ -57,7 +57,10 @@ export default function UserTable({ data, setData }: UserTableProps) {
</> </>
), ),
cell: (info) => ( cell: (info) => (
<TagsInput presetValue={info.getValue()} {...roleProps} /> <TagsInput
presetValue={info.getValue()}
{...roleProps}
/>
), ),
}), }),
columnHelper.accessor("email", { columnHelper.accessor("email", {
@ -80,10 +83,13 @@ export default function UserTable({ data, setData }: UserTableProps) {
</> </>
), ),
cell: (info) => ( cell: (info) => (
<TagsInput presetValue={info.getValue()} {...programProps} /> <TagsInput
presetValue={info.getValue()}
{...programProps}
/>
), ),
}), }),
]; ];
return <Table<User> data={data} setData={setData} columns={columns} />; return <Table<User> data={data} setData={setData} columns={columns}/>
} }

View File

@ -1,4 +1,4 @@
import { useState } from "react"; import { useState } from 'react';
/** /**
* Custom hook used to handle the state of tag options and colors * Custom hook used to handle the state of tag options and colors
@ -31,5 +31,5 @@ export default function useTagsHandler(initialOptions: string[]) {
return tagColors.get(tag) as string; return tagColors.get(tag) as string;
}; };
return { presetOptions, setPresetOptions, getTagColor }; return { presetOptions, setPresetOptions, getTagColor }
} }