mirror of
https://github.com/cssgunc/compass.git
synced 2025-04-03 19:40:16 -04:00
API Routes for Resources and Services (#40)
* Implemented API routes for getting all, creating, updating, and deleting resources, services, and tags. * Updated main.py for API routes to include tags and rolled entities back. * Created API routes for create, update, delete, get_all, and get_by_name. Deleted service methods for get by id. * Defaults created_at to current time * Write markdown file for HTTP requests using curl --------- Co-authored-by: pmoharana-cmd <pmoharana032474@gmail.com>
This commit is contained in:
parent
99e43c7b30
commit
7d705ac743
|
@ -1,26 +1,56 @@
|
|||
from fastapi import APIRouter, Depends
|
||||
from ..services import ResourceService, UserService
|
||||
from ..models.resource_model import Resource
|
||||
|
||||
from typing import List
|
||||
|
||||
api = APIRouter(prefix="/api/resource")
|
||||
|
||||
openapi_tags = {
|
||||
"name": "Resource",
|
||||
"description": "Resource search and related operations.",
|
||||
}
|
||||
|
||||
|
||||
# TODO: Add security using HTTP Bearer Tokens
|
||||
# TODO: Enable authorization by passing user uuid to API
|
||||
# TODO: Create custom exceptions
|
||||
@api.get("", response_model=List[Resource], tags=["Resource"])
|
||||
def get_all(
|
||||
user_id: str,
|
||||
resource_svc: ResourceService = Depends(),
|
||||
user_svc: UserService = Depends(),
|
||||
):
|
||||
subject = user_svc.get_user_by_uuid(user_id)
|
||||
|
||||
return resource_svc.get_resource_by_user(subject)
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from backend.models.user_model import User
|
||||
from ..services import ResourceService, UserService
|
||||
from ..models.resource_model import Resource
|
||||
|
||||
from typing import List
|
||||
|
||||
api = APIRouter(prefix="/api/resource")
|
||||
|
||||
openapi_tags = {
|
||||
"name": "Resource",
|
||||
"description": "Resource search and related 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=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"])
|
||||
def get_all(
|
||||
uuid: str, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends()
|
||||
):
|
||||
subject = user_svc.get_user_by_uuid(uuid)
|
||||
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)
|
||||
|
|
|
@ -1,26 +1,54 @@
|
|||
from fastapi import APIRouter, Depends
|
||||
from ..services import ServiceService, UserService
|
||||
from ..models.service_model import Service
|
||||
|
||||
from typing import List
|
||||
|
||||
api = APIRouter(prefix="/api/service")
|
||||
|
||||
openapi_tags = {
|
||||
"name": "Service",
|
||||
"description": "Service search and related operations.",
|
||||
}
|
||||
|
||||
|
||||
# TODO: Add security using HTTP Bearer Tokens
|
||||
# TODO: Enable authorization by passing user uuid to API
|
||||
# TODO: Create custom exceptions
|
||||
@api.get("", response_model=List[Service], tags=["Service"])
|
||||
def get_all(
|
||||
user_id: str,
|
||||
service_svc: ServiceService = Depends(),
|
||||
user_svc: UserService = Depends(),
|
||||
):
|
||||
subject = user_svc.get_user_by_uuid(user_id)
|
||||
|
||||
return service_svc.get_service_by_user(subject)
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from backend.models.user_model import User
|
||||
from ..services import ServiceService, UserService
|
||||
from ..models.service_model import Service
|
||||
|
||||
from typing import List
|
||||
|
||||
api = APIRouter(prefix="/api/service")
|
||||
|
||||
openapi_tags = {
|
||||
"name": "Service",
|
||||
"description": "Service search and related 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=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"])
|
||||
def get_all(
|
||||
uuid: str, user_svc: UserService = Depends(), service_svc: ServiceService = Depends()
|
||||
):
|
||||
subject = user_svc.get_user_by_uuid(uuid)
|
||||
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)
|
||||
|
|
51
backend/api/tag.py
Normal file
51
backend/api/tag.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
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)
|
147
backend/api/test_routes.md
Normal file
147
backend/api/test_routes.md
Normal file
|
@ -0,0 +1,147 @@
|
|||
# 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"
|
||||
}'
|
||||
```
|
|
@ -1,67 +1,67 @@
|
|||
""" Defines the table for storing resources """
|
||||
|
||||
# Import our mapped SQL types from SQLAlchemy
|
||||
from sqlalchemy import Integer, String, DateTime, Enum
|
||||
|
||||
# Import mapping capabilities from the SQLAlchemy ORM
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
# Import the EntityBase that we are extending
|
||||
from .entity_base import EntityBase
|
||||
|
||||
# Import datetime for created_at type
|
||||
from datetime import datetime
|
||||
|
||||
# Import self for to model
|
||||
from typing import Self
|
||||
from backend.entities.program_enum import Program_Enum
|
||||
from ..models.resource_model import Resource
|
||||
|
||||
|
||||
class ResourceEntity(EntityBase):
|
||||
|
||||
# set table name
|
||||
__tablename__ = "resource"
|
||||
|
||||
# set fields
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
summary: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
link: Mapped[str] = mapped_column(String, nullable=False)
|
||||
program: Mapped[Program_Enum] = mapped_column(Enum(Program_Enum), nullable=False)
|
||||
# relationships
|
||||
resourceTags: Mapped[list["ResourceTagEntity"]] = relationship(
|
||||
back_populates="resource", cascade="all,delete"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, model: Resource) -> Self:
|
||||
"""
|
||||
Create a UserEntity from a User model.
|
||||
|
||||
Args:
|
||||
model (User): The model to create the entity from.
|
||||
|
||||
Returns:
|
||||
Self: The entity (not yet persisted).
|
||||
"""
|
||||
|
||||
return cls(
|
||||
id=model.id,
|
||||
created_at=model.created_at,
|
||||
name=model.name,
|
||||
summary=model.summary,
|
||||
link=model.link,
|
||||
program=model.program,
|
||||
)
|
||||
|
||||
def to_model(self) -> Resource:
|
||||
return Resource(
|
||||
id=self.id,
|
||||
created_at=self.created_at,
|
||||
name=self.name,
|
||||
summary=self.summary,
|
||||
link=self.link,
|
||||
program=self.program,
|
||||
)
|
||||
""" Defines the table for storing resources """
|
||||
|
||||
# Import our mapped SQL types from SQLAlchemy
|
||||
from sqlalchemy import Integer, String, DateTime, Enum
|
||||
|
||||
# Import mapping capabilities from the SQLAlchemy ORM
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
# Import the EntityBase that we are extending
|
||||
from .entity_base import EntityBase
|
||||
|
||||
# Import datetime for created_at type
|
||||
from datetime import datetime
|
||||
|
||||
# Import self for to model
|
||||
from typing import Self
|
||||
from backend.entities.program_enum import Program_Enum
|
||||
from ..models.resource_model import Resource
|
||||
|
||||
|
||||
class ResourceEntity(EntityBase):
|
||||
|
||||
# set table name
|
||||
__tablename__ = "resource"
|
||||
|
||||
# set fields
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
summary: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
link: Mapped[str] = mapped_column(String, nullable=False)
|
||||
program: Mapped[Program_Enum] = mapped_column(Enum(Program_Enum), nullable=False)
|
||||
# relationships
|
||||
resourceTags: Mapped[list["ResourceTagEntity"]] = relationship(
|
||||
back_populates="resource", cascade="all,delete"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, model: Resource) -> Self:
|
||||
"""
|
||||
Create a UserEntity from a User model.
|
||||
|
||||
Args:
|
||||
model (User): The model to create the entity from.
|
||||
|
||||
Returns:
|
||||
Self: The entity (not yet persisted).
|
||||
"""
|
||||
|
||||
return cls(
|
||||
id=model.id,
|
||||
created_at=model.created_at,
|
||||
name=model.name,
|
||||
summary=model.summary,
|
||||
link=model.link,
|
||||
program=model.program,
|
||||
)
|
||||
|
||||
def to_model(self) -> Resource:
|
||||
return Resource(
|
||||
id=self.id,
|
||||
created_at=self.created_at,
|
||||
name=self.name,
|
||||
summary=self.summary,
|
||||
link=self.link,
|
||||
program=self.program,
|
||||
)
|
|
@ -1,46 +1,46 @@
|
|||
""" Defines the table for resource tags """
|
||||
|
||||
# Import our mapped SQL types from SQLAlchemy
|
||||
from sqlalchemy import ForeignKey, Integer, String, DateTime
|
||||
|
||||
# Import mapping capabilities from the SQLAlchemy ORM
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
# Import the EntityBase that we are extending
|
||||
from .entity_base import EntityBase
|
||||
|
||||
# Import datetime for created_at type
|
||||
from datetime import datetime
|
||||
|
||||
# Import self for to model
|
||||
from typing import Self
|
||||
|
||||
|
||||
class ResourceTagEntity(EntityBase):
|
||||
|
||||
# set table name to user in the database
|
||||
__tablename__ = "resource_tag"
|
||||
|
||||
# set fields or 'columns' for the user table
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
resourceId: Mapped[int] = mapped_column(ForeignKey("resource.id"))
|
||||
tagId: Mapped[int] = mapped_column(ForeignKey("tag.id"))
|
||||
|
||||
# relationships
|
||||
resource: Mapped["ResourceEntity"] = relationship(back_populates="resourceTags")
|
||||
tag: Mapped["TagEntity"] = relationship(back_populates="resourceTags")
|
||||
|
||||
# @classmethod
|
||||
# def from_model (cls, model: resource_tag_model) -> Self:
|
||||
# return cls (
|
||||
# id = model.id,
|
||||
# resourceId = model.resourceId,
|
||||
# tagId = model.tagId,
|
||||
# )
|
||||
|
||||
# def to_model (self) -> resource_tag_model:
|
||||
# return user_model(
|
||||
# id = self.id,
|
||||
# resourceId = self.resourceId,
|
||||
# tagId = self.tagId,
|
||||
# )
|
||||
""" Defines the table for resource tags """
|
||||
|
||||
# Import our mapped SQL types from SQLAlchemy
|
||||
from sqlalchemy import ForeignKey, Integer, String, DateTime
|
||||
|
||||
# Import mapping capabilities from the SQLAlchemy ORM
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
# Import the EntityBase that we are extending
|
||||
from .entity_base import EntityBase
|
||||
|
||||
# Import datetime for created_at type
|
||||
from datetime import datetime
|
||||
|
||||
# Import self for to model
|
||||
from typing import Self
|
||||
|
||||
|
||||
class ResourceTagEntity(EntityBase):
|
||||
|
||||
# set table name to user in the database
|
||||
__tablename__ = "resource_tag"
|
||||
|
||||
# set fields or 'columns' for the user table
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
resourceId: Mapped[int] = mapped_column(ForeignKey("resource.id"))
|
||||
tagId: Mapped[int] = mapped_column(ForeignKey("tag.id"))
|
||||
|
||||
# relationships
|
||||
resource: Mapped["ResourceEntity"] = relationship(back_populates="resourceTags")
|
||||
tag: Mapped["TagEntity"] = relationship(back_populates="resourceTags")
|
||||
|
||||
# @classmethod
|
||||
# def from_model (cls, model: resource_tag_model) -> Self:
|
||||
# return cls (
|
||||
# id = model.id,
|
||||
# resourceId = model.resourceId,
|
||||
# tagId = model.tagId,
|
||||
# )
|
||||
|
||||
# def to_model (self) -> resource_tag_model:
|
||||
# return user_model(
|
||||
# id = self.id,
|
||||
# resourceId = self.resourceId,
|
||||
# tagId = self.tagId,
|
||||
# )
|
|
@ -1,47 +1,47 @@
|
|||
""" Defines the table for storing services """
|
||||
|
||||
# Import our mapped SQL types from SQLAlchemy
|
||||
from sqlalchemy import Integer, String, DateTime, ARRAY
|
||||
|
||||
# Import mapping capabilities from the SQLAlchemy ORM
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
# Import the EntityBase that we are extending
|
||||
from .entity_base import EntityBase
|
||||
|
||||
# Import datetime for created_at type
|
||||
from datetime import datetime
|
||||
|
||||
# Import enums for Program
|
||||
import enum
|
||||
from sqlalchemy import Enum
|
||||
|
||||
from backend.models.service_model import Service
|
||||
from typing import Self
|
||||
from backend.models.enum_for_models import ProgramTypeEnum
|
||||
|
||||
class ServiceEntity(EntityBase):
|
||||
|
||||
# set table name
|
||||
__tablename__ = "service"
|
||||
|
||||
# set fields
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||
name: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
status: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
summary: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
requirements: Mapped[list[str]] = mapped_column(ARRAY(String))
|
||||
program: Mapped[ProgramTypeEnum] = mapped_column(Enum(ProgramTypeEnum), nullable=False)
|
||||
|
||||
# relationships
|
||||
serviceTags: Mapped[list["ServiceTagEntity"]] = relationship(
|
||||
back_populates="service", cascade="all,delete"
|
||||
)
|
||||
|
||||
def to_model(self) -> Service:
|
||||
return Service(id=self.id, name=self.name, status=self.status, summary=self.summary, requirements=self.requirements, program=self.program)
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, model:Service) -> Self:
|
||||
""" Defines the table for storing services """
|
||||
|
||||
# Import our mapped SQL types from SQLAlchemy
|
||||
from sqlalchemy import Integer, String, DateTime, ARRAY
|
||||
|
||||
# Import mapping capabilities from the SQLAlchemy ORM
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
# Import the EntityBase that we are extending
|
||||
from .entity_base import EntityBase
|
||||
|
||||
# Import datetime for created_at type
|
||||
from datetime import datetime
|
||||
|
||||
# Import enums for Program
|
||||
import enum
|
||||
from sqlalchemy import Enum
|
||||
|
||||
from backend.models.service_model import Service
|
||||
from typing import Self
|
||||
from backend.models.enum_for_models import ProgramTypeEnum
|
||||
|
||||
class ServiceEntity(EntityBase):
|
||||
|
||||
# set table name
|
||||
__tablename__ = "service"
|
||||
|
||||
# set fields
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||
name: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
status: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
summary: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
requirements: Mapped[list[str]] = mapped_column(ARRAY(String))
|
||||
program: Mapped[ProgramTypeEnum] = mapped_column(Enum(ProgramTypeEnum), nullable=False)
|
||||
|
||||
# relationships
|
||||
serviceTags: Mapped[list["ServiceTagEntity"]] = relationship(
|
||||
back_populates="service", cascade="all,delete"
|
||||
)
|
||||
|
||||
def to_model(self) -> Service:
|
||||
return Service(id=self.id, name=self.name, status=self.status, summary=self.summary, requirements=self.requirements, program=self.program)
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, model:Service) -> Self:
|
||||
return cls(id=model.id, name=model.name, status=model.status, summary=model.summary, requirements=model.requirements, program=model.program)
|
|
@ -1,25 +1,26 @@
|
|||
""" Defines the table for service tags """
|
||||
|
||||
# Import our mapped SQL types from SQLAlchemy
|
||||
from sqlalchemy import ForeignKey, Integer
|
||||
|
||||
# Import mapping capabilities from the SQLAlchemy ORM
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
# Import the EntityBase that we are extending
|
||||
from .entity_base import EntityBase
|
||||
|
||||
|
||||
class ServiceTagEntity(EntityBase):
|
||||
|
||||
# set table name to user in the database
|
||||
__tablename__ = "service_tag"
|
||||
|
||||
# set fields or 'columns' for the user table
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
serviceId: Mapped[int] = mapped_column(ForeignKey("service.id"))
|
||||
tagId: Mapped[int] = mapped_column(ForeignKey("tag.id"))
|
||||
|
||||
# relationships
|
||||
service: Mapped["ServiceEntity"] = relationship(back_populates="serviceTags")
|
||||
tag: Mapped["TagEntity"] = relationship(back_populates="serviceTags")
|
||||
""" Defines the table for service tags """
|
||||
|
||||
# Import our mapped SQL types from SQLAlchemy
|
||||
from sqlalchemy import ForeignKey, Integer
|
||||
|
||||
# Import mapping capabilities from the SQLAlchemy ORM
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
# Import the EntityBase that we are extending
|
||||
from .entity_base import EntityBase
|
||||
|
||||
|
||||
class ServiceTagEntity(EntityBase):
|
||||
|
||||
# set table name to user in the database
|
||||
__tablename__ = "service_tag"
|
||||
|
||||
# set fields or 'columns' for the user table
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
serviceId: Mapped[int] = mapped_column(ForeignKey("service.id"))
|
||||
tagId: Mapped[int] = mapped_column(ForeignKey("tag.id"))
|
||||
|
||||
# relationships
|
||||
service: Mapped["ServiceEntity"] = relationship(back_populates="serviceTags")
|
||||
tag: Mapped["TagEntity"] = relationship(back_populates="serviceTags")
|
||||
|
|
@ -1,65 +1,64 @@
|
|||
""" Defines the table for storing tags """
|
||||
|
||||
# Import our mapped SQL types from SQLAlchemy
|
||||
from sqlalchemy import Integer, String, DateTime
|
||||
|
||||
# Import mapping capabilities from the SQLAlchemy ORM
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
# Import the EntityBase that we are extending
|
||||
from .entity_base import EntityBase
|
||||
|
||||
# Import datetime for created_at type
|
||||
from datetime import datetime
|
||||
|
||||
from ..models.tag_model import Tag
|
||||
|
||||
from typing import Self
|
||||
|
||||
class TagEntity(EntityBase):
|
||||
|
||||
#set table name
|
||||
__tablename__ = "tag"
|
||||
|
||||
#set fields
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||
content: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
|
||||
#relationships
|
||||
resourceTags: Mapped[list["ResourceTagEntity"]] = relationship(back_populates="tag", cascade="all,delete")
|
||||
serviceTags: Mapped[list["ServiceTagEntity"]] = relationship(back_populates="tag", cascade="all,delete")
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, model: Tag) -> Self:
|
||||
"""
|
||||
Create a user entity from model
|
||||
|
||||
Args: model (User): the model to create the entity from
|
||||
|
||||
Returns:
|
||||
self: The entity
|
||||
"""
|
||||
|
||||
return cls(
|
||||
id=model.id,
|
||||
content=model.id,
|
||||
)
|
||||
|
||||
def to_model(self) -> Tag:
|
||||
"""
|
||||
Create a user model from entity
|
||||
|
||||
Returns:
|
||||
User: A User model for API usage
|
||||
"""
|
||||
|
||||
return Tag(
|
||||
id=self.id,
|
||||
content=self.content,
|
||||
)
|
||||
|
||||
|
||||
|
||||
""" Defines the table for storing tags """
|
||||
|
||||
# Import our mapped SQL types from SQLAlchemy
|
||||
from sqlalchemy import Integer, String, DateTime
|
||||
|
||||
# Import mapping capabilities from the SQLAlchemy ORM
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
# Import the EntityBase that we are extending
|
||||
from .entity_base import EntityBase
|
||||
|
||||
# Import datetime for created_at type
|
||||
from datetime import datetime
|
||||
|
||||
from ..models.tag_model import Tag
|
||||
|
||||
from typing import Self
|
||||
|
||||
class TagEntity(EntityBase):
|
||||
|
||||
#set table name
|
||||
__tablename__ = "tag"
|
||||
|
||||
#set fields
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||
content: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
|
||||
#relationships
|
||||
resourceTags: Mapped[list["ResourceTagEntity"]] = relationship(back_populates="tag", cascade="all,delete")
|
||||
serviceTags: Mapped[list["ServiceTagEntity"]] = relationship(back_populates="tag", cascade="all,delete")
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, model: Tag) -> Self:
|
||||
"""
|
||||
Create a user entity from model
|
||||
|
||||
Args: model (User): the model to create the entity from
|
||||
|
||||
Returns:
|
||||
self: The entity
|
||||
"""
|
||||
|
||||
return cls(
|
||||
id=model.id,
|
||||
created_at=model.created_at,
|
||||
content=model.id,
|
||||
)
|
||||
|
||||
def to_model(self) -> Tag:
|
||||
"""
|
||||
Create a user model from entity
|
||||
|
||||
Returns:
|
||||
User: A User model for API usage
|
||||
"""
|
||||
|
||||
return Tag(
|
||||
id=self.id,
|
||||
create_at=self.created_at,
|
||||
content=self.content,
|
||||
)
|
|
@ -1,34 +1,36 @@
|
|||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
|
||||
from .api import user, health, service, resource
|
||||
|
||||
description = """
|
||||
Welcome to the **COMPASS** RESTful Application Programming Interface.
|
||||
"""
|
||||
|
||||
app = FastAPI(
|
||||
title="Compass API",
|
||||
version="0.0.1",
|
||||
description=description,
|
||||
openapi_tags=[
|
||||
user.openapi_tags,
|
||||
health.openapi_tags,
|
||||
service.openapi_tags,
|
||||
resource.openapi_tags,
|
||||
],
|
||||
)
|
||||
|
||||
app.add_middleware(GZipMiddleware)
|
||||
|
||||
feature_apis = [user, health, service, resource]
|
||||
|
||||
for feature_api in feature_apis:
|
||||
app.include_router(feature_api.api)
|
||||
|
||||
|
||||
# Add application-wide exception handling middleware for commonly encountered API Exceptions
|
||||
@app.exception_handler(Exception)
|
||||
def permission_exception_handler(request: Request, e: Exception):
|
||||
return JSONResponse(status_code=403, content={"message": str(e)})
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
|
||||
|
||||
from .api import user, health, service, resource, tag
|
||||
|
||||
description = """
|
||||
Welcome to the **COMPASS** RESTful Application Programming Interface.
|
||||
"""
|
||||
|
||||
app = FastAPI(
|
||||
title="Compass API",
|
||||
version="0.0.1",
|
||||
description=description,
|
||||
openapi_tags=[
|
||||
user.openapi_tags,
|
||||
health.openapi_tags,
|
||||
service.openapi_tags,
|
||||
resource.openapi_tags,
|
||||
tag.openapi_tags
|
||||
],
|
||||
)
|
||||
|
||||
app.add_middleware(GZipMiddleware)
|
||||
|
||||
feature_apis = [user, health, service, resource, tag]
|
||||
|
||||
for feature_api in feature_apis:
|
||||
app.include_router(feature_api.api)
|
||||
|
||||
|
||||
# Add application-wide exception handling middleware for commonly encountered API Exceptions
|
||||
@app.exception_handler(Exception)
|
||||
def permission_exception_handler(request: Request, e: Exception):
|
||||
return JSONResponse(status_code=403, content={"message": str(e)})
|
||||
|
|
|
@ -12,4 +12,4 @@ class Resource(BaseModel):
|
|||
summary: str = Field(..., max_length=300, description="The summary of the resource")
|
||||
link: str = Field(..., max_length=150, description="link to the resource")
|
||||
program: ProgramTypeEnum
|
||||
created_at: Optional[datetime]
|
||||
created_at: Optional[datetime] = datetime.now()
|
||||
|
|
|
@ -8,7 +8,7 @@ from .enum_for_models import ProgramTypeEnum
|
|||
|
||||
class Service(BaseModel):
|
||||
id: int | None = None
|
||||
created_at: datetime | None = None
|
||||
created_at: datetime | None = datetime.now()
|
||||
name: str
|
||||
status: str
|
||||
summary: str
|
||||
|
|
|
@ -10,4 +10,4 @@ class Tag(BaseModel):
|
|||
content: str = Field(
|
||||
..., max_length=600, description="content associated with the tag"
|
||||
)
|
||||
created_at: datetime | None = None
|
||||
created_at: datetime | None = datetime.now()
|
||||
|
|
|
@ -14,5 +14,5 @@ class User(BaseModel):
|
|||
group: str
|
||||
program: List[ProgramTypeEnum]
|
||||
role: UserTypeEnum
|
||||
created_at: Optional[datetime]
|
||||
created_at: Optional[datetime] = datetime.now()
|
||||
uuid: str | None = None
|
||||
|
|
|
@ -20,6 +20,8 @@ class UserPermissionException(Exception):
|
|||
class ServiceNotFoundException(Exception):
|
||||
"""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):
|
||||
"""Exception for when the user does not have correct access for requested services."""
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
from fastapi import Depends
|
||||
from ..database import db_session
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import and_, select
|
||||
from ..models.resource_model import Resource
|
||||
from ..entities.resource_entity import ResourceEntity
|
||||
from ..models.user_model import User, UserTypeEnum
|
||||
|
||||
from .exceptions import ResourceNotFoundException
|
||||
from .exceptions import ProgramNotAssignedException, ResourceNotFoundException
|
||||
|
||||
|
||||
class ResourceService:
|
||||
|
@ -14,25 +14,40 @@ class ResourceService:
|
|||
def __init__(self, session: Session = Depends(db_session)):
|
||||
self._session = session
|
||||
|
||||
def get_resource_by_user(self, subject: User):
|
||||
def get_resource_by_user(self, subject: User) -> list[Resource]:
|
||||
"""Resource method getting all of the resources that a user has access to based on role"""
|
||||
if subject.role != UserTypeEnum.VOLUNTEER:
|
||||
query = select(ResourceEntity)
|
||||
entities = self._session.scalars(query).all()
|
||||
|
||||
return [resource.to_model() for resource in entities]
|
||||
else:
|
||||
programs = subject.program
|
||||
resources = []
|
||||
for program in programs:
|
||||
query = select(ResourceEntity).filter(ResourceEntity.program == program)
|
||||
entities = self._session.scalars(query).all()
|
||||
entities = (
|
||||
self._session.query(ResourceEntity)
|
||||
.where(ResourceEntity.program == program)
|
||||
.all()
|
||||
)
|
||||
for entity in entities:
|
||||
resources.append(entity)
|
||||
resources.append(entity.to_model())
|
||||
return [resource for resource in resources]
|
||||
|
||||
return [resource.to_model() for resource in resources]
|
||||
def get_resource_by_name(self, name: str, subject: User) -> Resource:
|
||||
"""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, user: User, resource: Resource) -> Resource:
|
||||
def create(self, subject: 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.
|
||||
Parameters:
|
||||
|
@ -41,43 +56,16 @@ class ResourceService:
|
|||
Returns:
|
||||
Resource: Object added to table
|
||||
"""
|
||||
if user.role != UserTypeEnum.ADMIN:
|
||||
raise PermissionError(
|
||||
"User does not have permission to add resources in this program."
|
||||
if subject.role != UserTypeEnum.ADMIN:
|
||||
raise ProgramNotAssignedException(
|
||||
f"User is not {UserTypeEnum.ADMIN}, cannot update service"
|
||||
)
|
||||
|
||||
resource_entity = ResourceEntity.from_model(resource)
|
||||
self._session.add(resource_entity)
|
||||
self._session.commit()
|
||||
|
||||
return resource_entity.to_model()
|
||||
|
||||
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:
|
||||
def update(self, subject: User, resource: Resource) -> Resource:
|
||||
"""
|
||||
Update the resource if the user has access
|
||||
Parameters:
|
||||
|
@ -88,28 +76,24 @@ class ResourceService:
|
|||
Raises:
|
||||
ResourceNotFoundException: If no resource is found with the corresponding ID
|
||||
"""
|
||||
if user.role != UserTypeEnum.ADMIN:
|
||||
raise PermissionError(
|
||||
"User does not have permission to update this resource."
|
||||
if subject.role != UserTypeEnum.ADMIN:
|
||||
raise ProgramNotAssignedException(
|
||||
f"User is not {UserTypeEnum.ADMIN}, cannot update service"
|
||||
)
|
||||
|
||||
obj = self._session.get(ResourceEntity, resource.id) if resource.id else None
|
||||
|
||||
if obj is None:
|
||||
query = select(ResourceEntity).where(ResourceEntity.id == resource.id)
|
||||
entity = self._session.scalars(query).one_or_none()
|
||||
if entity is None:
|
||||
raise ResourceNotFoundException(
|
||||
f"No resource found with matching id: {resource.id}"
|
||||
)
|
||||
|
||||
obj.name = resource.name
|
||||
obj.summary = resource.summary
|
||||
obj.link = resource.link
|
||||
obj.program = resource.program
|
||||
|
||||
entity.name = resource.name
|
||||
entity.summary = resource.summary
|
||||
entity.link = resource.link
|
||||
entity.program = resource.program
|
||||
self._session.commit()
|
||||
return entity.to_model()
|
||||
|
||||
return obj.to_model()
|
||||
|
||||
def delete(self, user: User, id: int) -> None:
|
||||
def delete(self, subject: User, resource: Resource) -> None:
|
||||
"""
|
||||
Delete resource based on id that the user has access to
|
||||
Parameters:
|
||||
|
@ -118,23 +102,17 @@ class ResourceService:
|
|||
Raises:
|
||||
ResourceNotFoundException: If no resource is found with the corresponding id
|
||||
"""
|
||||
if user.role != UserTypeEnum.ADMIN:
|
||||
raise PermissionError(
|
||||
"User does not have permission to delete this resource."
|
||||
if subject.role != UserTypeEnum.ADMIN:
|
||||
raise ProgramNotAssignedException(
|
||||
f"User is not {UserTypeEnum.ADMIN}, cannot update service"
|
||||
)
|
||||
|
||||
resource = (
|
||||
self._session.query(ResourceEntity)
|
||||
.filter(
|
||||
ResourceEntity.id == id,
|
||||
query = select(ResourceEntity).where(ResourceEntity.id == resource.id)
|
||||
entity = self._session.scalars(query).one_or_none()
|
||||
if entity is None:
|
||||
raise ResourceNotFoundException(
|
||||
f"No resource found with matching id: {resource.id}"
|
||||
)
|
||||
.one_or_none()
|
||||
)
|
||||
|
||||
if resource is None:
|
||||
raise ResourceNotFoundException(f"No resource found with matching id: {id}")
|
||||
|
||||
self._session.delete(resource)
|
||||
self._session.delete(entity)
|
||||
self._session.commit()
|
||||
|
||||
def get_by_slug(self, user: User, search_string: str) -> list[Resource]:
|
||||
|
@ -150,11 +128,11 @@ class ResourceService:
|
|||
"""
|
||||
query = select(ResourceEntity).where(
|
||||
ResourceEntity.name.ilike(f"%{search_string}%"),
|
||||
ResourceEntity.program.in_(user.program)
|
||||
ResourceEntity.program.in_(user.program),
|
||||
)
|
||||
entities = self._session.scalars(query).all()
|
||||
|
||||
if not entities:
|
||||
return []
|
||||
|
||||
return [entity.to_model() for entity in entities]
|
||||
return [entity.to_model() for entity in entities]
|
||||
|
|
|
@ -1,127 +1,101 @@
|
|||
from fastapi import Depends
|
||||
|
||||
from ..database import db_session
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, select, and_, func, or_, exists, or_
|
||||
|
||||
from backend.models.service_model import Service
|
||||
from backend.models.user_model import User
|
||||
from backend.entities.service_entity import ServiceEntity
|
||||
from backend.models.enum_for_models import ProgramTypeEnum, UserTypeEnum
|
||||
from backend.services.exceptions import (
|
||||
ServiceNotFoundException,
|
||||
ProgramNotAssignedException,
|
||||
)
|
||||
|
||||
|
||||
class ServiceService:
|
||||
|
||||
def __init__(self, session: Session = Depends(db_session)):
|
||||
self._session = session
|
||||
|
||||
def get_service_by_program(self, program: ProgramTypeEnum) -> list[Service]:
|
||||
"""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:
|
||||
query = select(ServiceEntity)
|
||||
entities = self._session.scalars(query).all()
|
||||
|
||||
return [service.to_model() for service in entities]
|
||||
else:
|
||||
programs = subject.program
|
||||
services = []
|
||||
for program in programs:
|
||||
query = select(ServiceEntity).filter(ServiceEntity.program == program)
|
||||
entities = self._session.scalars(query).all()
|
||||
for entity in entities:
|
||||
services.append(entity)
|
||||
|
||||
return [service.to_model() for service in services]
|
||||
|
||||
def get_all(self, subject: User) -> list[Service]:
|
||||
"""Service method retrieving all of the services in the table."""
|
||||
if subject.role == UserTypeEnum.VOLUNTEER:
|
||||
raise ProgramNotAssignedException(
|
||||
f"User is not {UserTypeEnum.ADMIN} or {UserTypeEnum.VOLUNTEER}, cannot get all"
|
||||
)
|
||||
|
||||
query = select(ServiceEntity)
|
||||
entities = self._session.scalars(query).all()
|
||||
|
||||
return [service.to_model() for service in entities]
|
||||
|
||||
def create(self, subject: User, service: Service) -> Service:
|
||||
"""Creates/adds a service to the table."""
|
||||
if subject.role != UserTypeEnum.ADMIN:
|
||||
raise ProgramNotAssignedException(
|
||||
f"User is not {UserTypeEnum.ADMIN}, cannot create service"
|
||||
)
|
||||
|
||||
service_entity = ServiceEntity.from_model(service)
|
||||
self._session.add(service_entity)
|
||||
self._session.commit()
|
||||
return service_entity.to_model()
|
||||
|
||||
def update(self, subject: User, service: Service) -> Service:
|
||||
"""Updates a service if in the table."""
|
||||
if subject.role != UserTypeEnum.ADMIN:
|
||||
raise ProgramNotAssignedException(
|
||||
f"User is not {UserTypeEnum.ADMIN}, cannot update service"
|
||||
)
|
||||
|
||||
service_entity = self._session.get(ServiceEntity, service.id)
|
||||
|
||||
if service_entity is None:
|
||||
raise ServiceNotFoundException(
|
||||
"The service you are searching for does not exist."
|
||||
)
|
||||
|
||||
service_entity.name = service.name
|
||||
service_entity.status = service.status
|
||||
service_entity.summary = service.summary
|
||||
service_entity.requirements = service.requirements
|
||||
service_entity.program = service.program
|
||||
|
||||
self._session.commit()
|
||||
|
||||
return service_entity.to_model()
|
||||
|
||||
def delete(self, subject: User, service: Service) -> None:
|
||||
"""Deletes a service from the table."""
|
||||
if subject.role != UserTypeEnum.ADMIN:
|
||||
raise ProgramNotAssignedException(f"User is not {UserTypeEnum.ADMIN}")
|
||||
service_entity = self._session.get(ServiceEntity, service.id)
|
||||
|
||||
if service_entity is None:
|
||||
raise ServiceNotFoundException(
|
||||
"The service you are searching for does not exist."
|
||||
)
|
||||
|
||||
self._session.delete(service_entity)
|
||||
self._session.commit()
|
||||
from fastapi import Depends
|
||||
|
||||
from ..database import db_session
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, select, and_, func, or_, exists, or_
|
||||
|
||||
from backend.models.service_model import Service
|
||||
from backend.models.user_model import User
|
||||
from backend.entities.service_entity import ServiceEntity
|
||||
from backend.models.enum_for_models import ProgramTypeEnum, UserTypeEnum
|
||||
from backend.services.exceptions import (
|
||||
ServiceNotFoundException,
|
||||
ProgramNotAssignedException,
|
||||
)
|
||||
|
||||
|
||||
class ServiceService:
|
||||
|
||||
def __init__(self, session: Session = Depends(db_session)):
|
||||
self._session = session
|
||||
|
||||
def get_service_by_user(self, subject: User) -> list[Service]:
|
||||
"""Resource method getting all of the resources that a user has access to based on role"""
|
||||
if subject.role != UserTypeEnum.VOLUNTEER:
|
||||
query = select(ServiceEntity)
|
||||
entities = self._session.scalars(query).all()
|
||||
return [service.to_model() for service in entities]
|
||||
else:
|
||||
programs = subject.program
|
||||
services = []
|
||||
for program in programs:
|
||||
entities = self._session.query(ServiceEntity).where(ServiceEntity.program == program).all()
|
||||
for entity in entities:
|
||||
services.append(entity.to_model())
|
||||
return [service for service in services]
|
||||
|
||||
def get_service_by_name(self, name: str, subject: User) -> Service:
|
||||
"""Service method getting services by id."""
|
||||
query = select(ServiceEntity).where(
|
||||
and_(
|
||||
ServiceEntity.name == name, ServiceEntity.program.in_(subject.program)
|
||||
)
|
||||
)
|
||||
entity = self._session.scalars(query).one_or_none()
|
||||
|
||||
if entity is None:
|
||||
raise ServiceNotFoundException(f"Service with name: {name} does not exist or program has not been assigned")
|
||||
|
||||
return entity.to_model()
|
||||
|
||||
def create(self, subject: User, service: Service) -> Service:
|
||||
"""Creates/adds a service to the table."""
|
||||
if subject.role != UserTypeEnum.ADMIN:
|
||||
raise ProgramNotAssignedException(
|
||||
f"User is not {UserTypeEnum.ADMIN}, cannot create service"
|
||||
)
|
||||
|
||||
service_entity = ServiceEntity.from_model(service)
|
||||
self._session.add(service_entity)
|
||||
self._session.commit()
|
||||
return service_entity.to_model()
|
||||
|
||||
def update(self, subject: User, service: Service) -> Service:
|
||||
"""Updates a service if in the table."""
|
||||
if subject.role != UserTypeEnum.ADMIN:
|
||||
raise ProgramNotAssignedException(
|
||||
f"User is not {UserTypeEnum.ADMIN}, cannot update service"
|
||||
)
|
||||
|
||||
query = select(ServiceEntity).where(ServiceEntity.id == service.id)
|
||||
entity = self._session.scalars(query).one_or_none()
|
||||
|
||||
if entity is None:
|
||||
raise ServiceNotFoundException(
|
||||
"The service you are searching for does not exist."
|
||||
)
|
||||
|
||||
entity.name = service.name
|
||||
entity.status = service.status
|
||||
entity.summary = service.summary
|
||||
entity.requirements = service.requirements
|
||||
entity.program = service.program
|
||||
self._session.commit()
|
||||
|
||||
return entity.to_model()
|
||||
|
||||
def delete(self, subject: User, service: Service) -> None:
|
||||
"""Deletes a service from the table."""
|
||||
if subject.role != UserTypeEnum.ADMIN:
|
||||
raise ProgramNotAssignedException(f"User is not {UserTypeEnum.ADMIN}")
|
||||
|
||||
query = select(ServiceEntity).where(ServiceEntity.id == service.id)
|
||||
entity = self._session.scalars(query).one_or_none()
|
||||
|
||||
if entity is None:
|
||||
raise ServiceNotFoundException(
|
||||
"The service you are searching for does not exist."
|
||||
)
|
||||
|
||||
self._session.delete(entity)
|
||||
self._session.commit()
|
||||
|
|
|
@ -1,20 +1,52 @@
|
|||
from fastapi import Depends
|
||||
from ..database import db_session
|
||||
from sqlalchemy.orm import Session
|
||||
from ..models.tag_model import Tag
|
||||
from ..entities.tag_entity import TagEntity
|
||||
from sqlalchemy import select
|
||||
|
||||
|
||||
class TagService:
|
||||
|
||||
def __init__(self, session: Session = Depends(db_session)):
|
||||
self._session = session
|
||||
|
||||
def all(self) -> list[Tag]:
|
||||
"""Returns a list of all Tags"""
|
||||
|
||||
query = select(TagEntity)
|
||||
entities = self._session.scalars(query).all()
|
||||
|
||||
return [entity.to_model() for entity in entities]
|
||||
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 sqlalchemy.orm import Session
|
||||
from ..models.tag_model import Tag
|
||||
from ..entities.tag_entity import TagEntity
|
||||
from sqlalchemy import select
|
||||
|
||||
# Add in checks for user permission?
|
||||
class TagService:
|
||||
|
||||
def __init__(self, session: Session = Depends(db_session)):
|
||||
self._session = session
|
||||
|
||||
def get_all(self) -> list[Tag]:
|
||||
"""Returns a list of all Tags"""
|
||||
query = select(TagEntity)
|
||||
entities = self._session.scalars(query).all()
|
||||
|
||||
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()
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ export default function Page() {
|
|||
);
|
||||
|
||||
const resourcesAPI: Resource[] = await userListData.json();
|
||||
|
||||
|
||||
setResources(resourcesAPI);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,257 +1,255 @@
|
|||
import { Dispatch, FunctionComponent, ReactNode, SetStateAction } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { ChevronDoubleLeftIcon } from "@heroicons/react/24/solid";
|
||||
import {
|
||||
StarIcon as SolidStarIcon,
|
||||
EnvelopeIcon,
|
||||
UserIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import {
|
||||
ArrowsPointingOutIcon,
|
||||
ArrowsPointingInIcon,
|
||||
StarIcon as OutlineStarIcon,
|
||||
ListBulletIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import TagsInput from "../TagsInput/Index";
|
||||
|
||||
type DrawerProps = {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
type?: "button" | "submit" | "reset"; // specify possible values for type
|
||||
disabled?: boolean;
|
||||
editableContent?: any;
|
||||
onSave?: (content: any) => void;
|
||||
rowContent?: any;
|
||||
setData: Dispatch<SetStateAction<any>>;
|
||||
};
|
||||
|
||||
interface EditContent {
|
||||
content: string;
|
||||
isEditing: boolean;
|
||||
}
|
||||
|
||||
const Drawer: FunctionComponent<DrawerProps> = ({
|
||||
title,
|
||||
children,
|
||||
onSave,
|
||||
editableContent,
|
||||
rowContent,
|
||||
setData,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isFull, setIsFull] = useState(false);
|
||||
const [isFavorite, setIsFavorite] = useState(false);
|
||||
const [tempRowContent, setTempRowContent] = useState(rowContent);
|
||||
|
||||
const onRowUpdate = (updatedRow: any) => {
|
||||
setData((prevData: any) => (
|
||||
prevData.map((row: any) => (
|
||||
row.id === updatedRow.id
|
||||
? updatedRow
|
||||
: row
|
||||
))
|
||||
))
|
||||
};
|
||||
|
||||
const handleTempRowContentChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
console.log(name);
|
||||
console.log(value);
|
||||
setTempRowContent((prevContent) => ({
|
||||
...prevContent,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleEnterPress = (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
// Update the rowContent with the temporaryRowContent
|
||||
if (onRowUpdate) {
|
||||
onRowUpdate(tempRowContent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleDrawer = () => {
|
||||
setIsOpen(!isOpen);
|
||||
if (isFull) {
|
||||
setIsFull(!isFull);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleDrawerFullScreen = () => setIsFull(!isFull);
|
||||
|
||||
const toggleFavorite = () => setIsFavorite(!isFavorite);
|
||||
|
||||
const drawerClassName = `fixed top-0 right-0 w-1/2 h-full bg-white transform ease-in-out duration-300 z-20 ${
|
||||
isOpen ? "translate-x-0 shadow-xl" : "translate-x-full"
|
||||
} ${isFull ? "w-full" : "w-1/2"}`;
|
||||
|
||||
const iconComponent = isFull ? (
|
||||
<ArrowsPointingInIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<ArrowsPointingOutIcon className="h-5 w-5" />
|
||||
);
|
||||
|
||||
const favoriteIcon = isFavorite ? (
|
||||
<SolidStarIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<OutlineStarIcon className="h-5 w-5" />
|
||||
);
|
||||
|
||||
const [presetOptions, setPresetOptions] = useState([
|
||||
"administrator",
|
||||
"volunteer",
|
||||
"employee",
|
||||
]);
|
||||
const [rolePresetOptions, setRolePresetOptions] = useState([
|
||||
"domestic",
|
||||
"community",
|
||||
"economic",
|
||||
]);
|
||||
const [tagColors, setTagColors] = useState(new Map());
|
||||
|
||||
const getTagColor = (tag: string) => {
|
||||
if (!tagColors.has(tag)) {
|
||||
const colors = [
|
||||
"bg-cyan-100",
|
||||
"bg-blue-100",
|
||||
"bg-green-100",
|
||||
"bg-yellow-100",
|
||||
"bg-purple-100",
|
||||
];
|
||||
const randomColor =
|
||||
colors[Math.floor(Math.random() * colors.length)];
|
||||
setTagColors(new Map(tagColors).set(tag, randomColor));
|
||||
}
|
||||
return tagColors.get(tag);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
className={
|
||||
"ml-2 text-xs uppercase opacity-0 group-hover:opacity-100 text-gray-500 font-medium border border-gray-200 bg-white shadow hover:bg-gray-50 p-2 rounded-md"
|
||||
}
|
||||
onClick={toggleDrawer}
|
||||
>
|
||||
Open
|
||||
</button>
|
||||
<div className={drawerClassName}></div>
|
||||
<div className={drawerClassName}>
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<div className="flex flex-row items-center justify-between space-x-2">
|
||||
<span className="h-5 text-purple-200 w-5">
|
||||
<UserIcon />
|
||||
</span>
|
||||
<h2 className="text-lg text-gray-800 font-semibold">
|
||||
{rowContent.username}
|
||||
</h2>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={toggleFavorite}
|
||||
className="py-2 text-gray-500 hover:text-gray-800 mr-2"
|
||||
>
|
||||
{favoriteIcon}
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleDrawerFullScreen}
|
||||
className="py-2 text-gray-500 hover:text-gray-800 mr-2"
|
||||
>
|
||||
{iconComponent}
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleDrawer}
|
||||
className="py-2 text-gray-500 hover:text-gray-800"
|
||||
>
|
||||
<ChevronDoubleLeftIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<table className="p-4">
|
||||
<tbody className="items-center">
|
||||
<tr className="w-full text-xs items-center flex flex-row justify-between">
|
||||
<div className="flex flex-row space-x-2 text-gray-500 items-center">
|
||||
<td>
|
||||
<UserIcon className="h-4 w-4" />
|
||||
</td>
|
||||
<td className="w-32">Username</td>
|
||||
</div>
|
||||
<td className="w-3/4 w-3/4 p-2 pl-0">
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
value={tempRowContent.username}
|
||||
onChange={handleTempRowContentChange}
|
||||
onKeyDown={handleEnterPress}
|
||||
className="ml-2 w-full p-1 focus:outline-gray-200 hover:bg-gray-50"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="w-full text-xs items-center flex flex-row justify-between">
|
||||
<div className="flex flex-row space-x-2 text-gray-500 items-center">
|
||||
<td>
|
||||
<ListBulletIcon className="h-4 w-4" />
|
||||
</td>
|
||||
<td className="w-32">Role</td>
|
||||
</div>
|
||||
<td className="w-3/4 hover:bg-gray-50">
|
||||
<TagsInput
|
||||
presetValue={tempRowContent.role}
|
||||
presetOptions={presetOptions}
|
||||
setPresetOptions={setPresetOptions}
|
||||
getTagColor={getTagColor}
|
||||
setTagColors={setTagColors}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="w-full text-xs items-center flex flex-row justify-between">
|
||||
<div className="flex flex-row space-x-2 text-gray-500 items-center">
|
||||
<td>
|
||||
<EnvelopeIcon className="h-4 w-4" />
|
||||
</td>
|
||||
<td className="w-32">Email</td>
|
||||
</div>
|
||||
<td className="w-3/4 p-2 pl-0">
|
||||
<input
|
||||
type="text"
|
||||
name="email"
|
||||
value={tempRowContent.email}
|
||||
onChange={handleTempRowContentChange}
|
||||
onKeyDown={handleEnterPress}
|
||||
className="ml-2 w-80 p-1 font-normal hover:text-gray-400 focus:outline-gray-200 hover:bg-gray-50 underline text-gray-500"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="w-full text-xs items-center flex flex-row justify-between">
|
||||
<div className="flex flex-row space-x-2 text-gray-500 items-center">
|
||||
<td>
|
||||
<ListBulletIcon className="h-4 w-4" />
|
||||
</td>
|
||||
<td className="w-32">Type of Program</td>
|
||||
</div>
|
||||
<td className="w-3/4 hover:bg-gray-50">
|
||||
{/* {rowContent.program} */}
|
||||
<TagsInput
|
||||
presetValue={tempRowContent.program}
|
||||
presetOptions={rolePresetOptions}
|
||||
setPresetOptions={setRolePresetOptions}
|
||||
getTagColor={getTagColor}
|
||||
setTagColors={setTagColors}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Drawer;
|
||||
import { Dispatch, FunctionComponent, ReactNode, SetStateAction } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { ChevronDoubleLeftIcon } from "@heroicons/react/24/solid";
|
||||
import {
|
||||
StarIcon as SolidStarIcon,
|
||||
EnvelopeIcon,
|
||||
UserIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import {
|
||||
ArrowsPointingOutIcon,
|
||||
ArrowsPointingInIcon,
|
||||
StarIcon as OutlineStarIcon,
|
||||
ListBulletIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import TagsInput from "../TagsInput/Index";
|
||||
|
||||
type DrawerProps = {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
type?: "button" | "submit" | "reset"; // specify possible values for type
|
||||
disabled?: boolean;
|
||||
editableContent?: any;
|
||||
onSave?: (content: any) => void;
|
||||
rowContent?: any;
|
||||
setData: Dispatch<SetStateAction<any>>;
|
||||
};
|
||||
|
||||
interface EditContent {
|
||||
content: string;
|
||||
isEditing: boolean;
|
||||
}
|
||||
|
||||
const Drawer: FunctionComponent<DrawerProps> = ({
|
||||
title,
|
||||
children,
|
||||
onSave,
|
||||
editableContent,
|
||||
rowContent,
|
||||
setData,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isFull, setIsFull] = useState(false);
|
||||
const [isFavorite, setIsFavorite] = useState(false);
|
||||
const [tempRowContent, setTempRowContent] = useState(rowContent);
|
||||
|
||||
const onRowUpdate = (updatedRow: any) => {
|
||||
setData((prevData: any) =>
|
||||
prevData.map((row: any) =>
|
||||
row.id === updatedRow.id ? updatedRow : row
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleTempRowContentChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
console.log(name);
|
||||
console.log(value);
|
||||
setTempRowContent((prevContent) => ({
|
||||
...prevContent,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleEnterPress = (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
// Update the rowContent with the temporaryRowContent
|
||||
if (onRowUpdate) {
|
||||
onRowUpdate(tempRowContent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleDrawer = () => {
|
||||
setIsOpen(!isOpen);
|
||||
if (isFull) {
|
||||
setIsFull(!isFull);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleDrawerFullScreen = () => setIsFull(!isFull);
|
||||
|
||||
const toggleFavorite = () => setIsFavorite(!isFavorite);
|
||||
|
||||
const drawerClassName = `fixed top-0 right-0 w-1/2 h-full bg-white transform ease-in-out duration-300 z-20 ${
|
||||
isOpen ? "translate-x-0 shadow-xl" : "translate-x-full"
|
||||
} ${isFull ? "w-full" : "w-1/2"}`;
|
||||
|
||||
const iconComponent = isFull ? (
|
||||
<ArrowsPointingInIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<ArrowsPointingOutIcon className="h-5 w-5" />
|
||||
);
|
||||
|
||||
const favoriteIcon = isFavorite ? (
|
||||
<SolidStarIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<OutlineStarIcon className="h-5 w-5" />
|
||||
);
|
||||
|
||||
const [presetOptions, setPresetOptions] = useState([
|
||||
"administrator",
|
||||
"volunteer",
|
||||
"employee",
|
||||
]);
|
||||
const [rolePresetOptions, setRolePresetOptions] = useState([
|
||||
"domestic",
|
||||
"community",
|
||||
"economic",
|
||||
]);
|
||||
const [tagColors, setTagColors] = useState(new Map());
|
||||
|
||||
const getTagColor = (tag: string) => {
|
||||
if (!tagColors.has(tag)) {
|
||||
const colors = [
|
||||
"bg-cyan-100",
|
||||
"bg-blue-100",
|
||||
"bg-green-100",
|
||||
"bg-yellow-100",
|
||||
"bg-purple-100",
|
||||
];
|
||||
const randomColor =
|
||||
colors[Math.floor(Math.random() * colors.length)];
|
||||
setTagColors(new Map(tagColors).set(tag, randomColor));
|
||||
}
|
||||
return tagColors.get(tag);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
className={
|
||||
"ml-2 text-xs uppercase opacity-0 group-hover:opacity-100 text-gray-500 font-medium border border-gray-200 bg-white shadow hover:bg-gray-50 p-2 rounded-md"
|
||||
}
|
||||
onClick={toggleDrawer}
|
||||
>
|
||||
Open
|
||||
</button>
|
||||
<div className={drawerClassName}></div>
|
||||
<div className={drawerClassName}>
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<div className="flex flex-row items-center justify-between space-x-2">
|
||||
<span className="h-5 text-purple-200 w-5">
|
||||
<UserIcon />
|
||||
</span>
|
||||
<h2 className="text-lg text-gray-800 font-semibold">
|
||||
{rowContent.username}
|
||||
</h2>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={toggleFavorite}
|
||||
className="py-2 text-gray-500 hover:text-gray-800 mr-2"
|
||||
>
|
||||
{favoriteIcon}
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleDrawerFullScreen}
|
||||
className="py-2 text-gray-500 hover:text-gray-800 mr-2"
|
||||
>
|
||||
{iconComponent}
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleDrawer}
|
||||
className="py-2 text-gray-500 hover:text-gray-800"
|
||||
>
|
||||
<ChevronDoubleLeftIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<table className="p-4">
|
||||
<tbody className="items-center">
|
||||
<tr className="w-full text-xs items-center flex flex-row justify-between">
|
||||
<div className="flex flex-row space-x-2 text-gray-500 items-center">
|
||||
<td>
|
||||
<UserIcon className="h-4 w-4" />
|
||||
</td>
|
||||
<td className="w-32">Username</td>
|
||||
</div>
|
||||
<td className="w-3/4 w-3/4 p-2 pl-0">
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
value={tempRowContent.username}
|
||||
onChange={handleTempRowContentChange}
|
||||
onKeyDown={handleEnterPress}
|
||||
className="ml-2 w-full p-1 focus:outline-gray-200 hover:bg-gray-50"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="w-full text-xs items-center flex flex-row justify-between">
|
||||
<div className="flex flex-row space-x-2 text-gray-500 items-center">
|
||||
<td>
|
||||
<ListBulletIcon className="h-4 w-4" />
|
||||
</td>
|
||||
<td className="w-32">Role</td>
|
||||
</div>
|
||||
<td className="w-3/4 hover:bg-gray-50">
|
||||
<TagsInput
|
||||
presetValue={tempRowContent.role}
|
||||
presetOptions={presetOptions}
|
||||
setPresetOptions={setPresetOptions}
|
||||
getTagColor={getTagColor}
|
||||
setTagColors={setTagColors}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="w-full text-xs items-center flex flex-row justify-between">
|
||||
<div className="flex flex-row space-x-2 text-gray-500 items-center">
|
||||
<td>
|
||||
<EnvelopeIcon className="h-4 w-4" />
|
||||
</td>
|
||||
<td className="w-32">Email</td>
|
||||
</div>
|
||||
<td className="w-3/4 p-2 pl-0">
|
||||
<input
|
||||
type="text"
|
||||
name="email"
|
||||
value={tempRowContent.email}
|
||||
onChange={handleTempRowContentChange}
|
||||
onKeyDown={handleEnterPress}
|
||||
className="ml-2 w-80 p-1 font-normal hover:text-gray-400 focus:outline-gray-200 hover:bg-gray-50 underline text-gray-500"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="w-full text-xs items-center flex flex-row justify-between">
|
||||
<div className="flex flex-row space-x-2 text-gray-500 items-center">
|
||||
<td>
|
||||
<ListBulletIcon className="h-4 w-4" />
|
||||
</td>
|
||||
<td className="w-32">Type of Program</td>
|
||||
</div>
|
||||
<td className="w-3/4 hover:bg-gray-50">
|
||||
{/* {rowContent.program} */}
|
||||
<TagsInput
|
||||
presetValue={tempRowContent.program}
|
||||
presetOptions={rolePresetOptions}
|
||||
setPresetOptions={setRolePresetOptions}
|
||||
getTagColor={getTagColor}
|
||||
setTagColors={setTagColors}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Drawer;
|
||||
|
|
|
@ -8,24 +8,20 @@ import TagsInput from "@/components/TagsInput/Index";
|
|||
import Resource from "@/utils/models/Resource";
|
||||
|
||||
type ResourceTableProps = {
|
||||
data: Resource[],
|
||||
setData: Dispatch<SetStateAction<Resource[]>>
|
||||
}
|
||||
data: Resource[];
|
||||
setData: Dispatch<SetStateAction<Resource[]>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Table componenet used for displaying resources
|
||||
* @param props.data Stateful list of resources to be displayed by the table
|
||||
* @param props.setData State setter for the list of resources
|
||||
*/
|
||||
export default function ResourceTable({ data, setData }: ResourceTableProps ) {
|
||||
const columnHelper = createColumnHelper<Resource>();
|
||||
export default function ResourceTable({ data, setData }: ResourceTableProps) {
|
||||
const columnHelper = createColumnHelper<Resource>();
|
||||
|
||||
// Set up tag handling
|
||||
const programProps = useTagsHandler([
|
||||
"community",
|
||||
"domestic",
|
||||
"economic",
|
||||
])
|
||||
const programProps = useTagsHandler(["community", "domestic", "economic"]);
|
||||
|
||||
// Define Tanstack columns
|
||||
const columns: ColumnDef<Resource, any>[] = [
|
||||
|
@ -66,10 +62,7 @@ export default function ResourceTable({ data, setData }: ResourceTableProps ) {
|
|||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<TagsInput
|
||||
presetValue={info.getValue()}
|
||||
{...programProps}
|
||||
/>
|
||||
<TagsInput presetValue={info.getValue()} {...programProps} />
|
||||
),
|
||||
}),
|
||||
|
||||
|
@ -85,5 +78,5 @@ export default function ResourceTable({ data, setData }: ResourceTableProps ) {
|
|||
}),
|
||||
];
|
||||
|
||||
return <Table data={data} setData={setData} columns={columns}/>
|
||||
return <Table data={data} setData={setData} columns={columns} />;
|
||||
}
|
||||
|
|
|
@ -1,34 +1,38 @@
|
|||
import Drawer from "@/components/Drawer/Drawer";
|
||||
import DataPoint from "@/utils/models/DataPoint";
|
||||
import { Dispatch, SetStateAction, useState } from "react";
|
||||
|
||||
type RowOpenActionProps<T extends DataPoint> = {
|
||||
title: string,
|
||||
rowData: T,
|
||||
setData: Dispatch<SetStateAction<T[]>>
|
||||
}
|
||||
|
||||
export function RowOpenAction<T extends DataPoint>({ title, rowData, setData }: RowOpenActionProps<T>) {
|
||||
const [pageContent, setPageContent] = useState("");
|
||||
|
||||
const handleDrawerContentChange = (newContent: string) => {
|
||||
setPageContent(newContent);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="font-semibold group flex flex-row items-center justify-between pr-2">
|
||||
{title}
|
||||
<span>
|
||||
<Drawer
|
||||
title="My Drawer Title"
|
||||
editableContent={pageContent}
|
||||
rowContent={rowData}
|
||||
onSave={handleDrawerContentChange}
|
||||
setData={setData}
|
||||
>
|
||||
{pageContent}
|
||||
</Drawer>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import Drawer from "@/components/Drawer/Drawer";
|
||||
import DataPoint from "@/utils/models/DataPoint";
|
||||
import { Dispatch, SetStateAction, useState } from "react";
|
||||
|
||||
type RowOpenActionProps<T extends DataPoint> = {
|
||||
title: string;
|
||||
rowData: T;
|
||||
setData: Dispatch<SetStateAction<T[]>>;
|
||||
};
|
||||
|
||||
export function RowOpenAction<T extends DataPoint>({
|
||||
title,
|
||||
rowData,
|
||||
setData,
|
||||
}: RowOpenActionProps<T>) {
|
||||
const [pageContent, setPageContent] = useState("");
|
||||
|
||||
const handleDrawerContentChange = (newContent: string) => {
|
||||
setPageContent(newContent);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="font-semibold group flex flex-row items-center justify-between pr-2">
|
||||
{title}
|
||||
<span>
|
||||
<Drawer
|
||||
title="My Drawer Title"
|
||||
editableContent={pageContent}
|
||||
rowContent={rowData}
|
||||
onSave={handleDrawerContentChange}
|
||||
setData={setData}
|
||||
>
|
||||
{pageContent}
|
||||
</Drawer>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,108 +1,103 @@
|
|||
import { Bars2Icon } from "@heroicons/react/24/solid";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import useTagsHandler from "@/components/TagsInput/TagsHandler";
|
||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
|
||||
import Table from "@/components/Table/Table";
|
||||
import { RowOpenAction } from "@/components/Table/RowOpenAction";
|
||||
import TagsInput from "@/components/TagsInput/Index";
|
||||
import Service from "@/utils/models/Service";
|
||||
|
||||
type ServiceTableProps = {
|
||||
data: Service[],
|
||||
setData: Dispatch<SetStateAction<Service[]>>
|
||||
}
|
||||
|
||||
/**
|
||||
* Table componenet used for displaying services
|
||||
* @param props.data Stateful list of services to be displayed by the table
|
||||
* @param props.setData State setter for the list of services
|
||||
*/
|
||||
export default function ServiceTable({ data, setData }: ServiceTableProps ) {
|
||||
const columnHelper = createColumnHelper<Service>();
|
||||
|
||||
// Set up tag handling
|
||||
const programProps = useTagsHandler([
|
||||
"community",
|
||||
"domestic",
|
||||
"economic",
|
||||
])
|
||||
|
||||
// TODO: Dynamically or statically get full list of preset requirement tag options
|
||||
const requirementProps = useTagsHandler([
|
||||
'anonymous',
|
||||
'confidential',
|
||||
'referral required',
|
||||
'safety assessment',
|
||||
'intake required',
|
||||
'income eligibility',
|
||||
'initial assessment',
|
||||
])
|
||||
|
||||
// Define Tanstack columns
|
||||
const columns: ColumnDef<Service, any>[] = [
|
||||
columnHelper.accessor("name", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Name
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<RowOpenAction
|
||||
title={info.getValue()}
|
||||
rowData={info.row.original}
|
||||
setData={setData}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("status", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Status
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<span className="ml-2 text-gray-500">{info.getValue()}</span>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("program", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Program
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<TagsInput
|
||||
presetValue={info.getValue()}
|
||||
{...programProps}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("requirements", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Requirements
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
// TODO: Setup different tag handler for requirements
|
||||
<TagsInput
|
||||
presetValue={info.getValue()[0] !== "" ? info.getValue() : ["N/A"]}
|
||||
{...requirementProps}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
|
||||
columnHelper.accessor("summary", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Summary
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<span className="ml-2 text-gray-500">{info.getValue()}</span>
|
||||
),
|
||||
}),
|
||||
];
|
||||
|
||||
return <Table data={data} setData={setData} columns={columns} />
|
||||
};
|
||||
import { Bars2Icon } from "@heroicons/react/24/solid";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import useTagsHandler from "@/components/TagsInput/TagsHandler";
|
||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
|
||||
import Table from "@/components/Table/Table";
|
||||
import { RowOpenAction } from "@/components/Table/RowOpenAction";
|
||||
import TagsInput from "@/components/TagsInput/Index";
|
||||
import Service from "@/utils/models/Service";
|
||||
|
||||
type ServiceTableProps = {
|
||||
data: Service[];
|
||||
setData: Dispatch<SetStateAction<Service[]>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Table componenet used for displaying services
|
||||
* @param props.data Stateful list of services to be displayed by the table
|
||||
* @param props.setData State setter for the list of services
|
||||
*/
|
||||
export default function ServiceTable({ data, setData }: ServiceTableProps) {
|
||||
const columnHelper = createColumnHelper<Service>();
|
||||
|
||||
// Set up tag handling
|
||||
const programProps = useTagsHandler(["community", "domestic", "economic"]);
|
||||
|
||||
// TODO: Dynamically or statically get full list of preset requirement tag options
|
||||
const requirementProps = useTagsHandler([
|
||||
"anonymous",
|
||||
"confidential",
|
||||
"referral required",
|
||||
"safety assessment",
|
||||
"intake required",
|
||||
"income eligibility",
|
||||
"initial assessment",
|
||||
]);
|
||||
|
||||
// Define Tanstack columns
|
||||
const columns: ColumnDef<Service, any>[] = [
|
||||
columnHelper.accessor("name", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Name
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<RowOpenAction
|
||||
title={info.getValue()}
|
||||
rowData={info.row.original}
|
||||
setData={setData}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("status", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Status
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<span className="ml-2 text-gray-500">{info.getValue()}</span>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("program", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Program
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<TagsInput presetValue={info.getValue()} {...programProps} />
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("requirements", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Requirements
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
// TODO: Setup different tag handler for requirements
|
||||
<TagsInput
|
||||
presetValue={
|
||||
info.getValue()[0] !== "" ? info.getValue() : ["N/A"]
|
||||
}
|
||||
{...requirementProps}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
|
||||
columnHelper.accessor("summary", {
|
||||
header: () => (
|
||||
<>
|
||||
<Bars2Icon className="inline align-top h-4" /> Summary
|
||||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<span className="ml-2 text-gray-500">{info.getValue()}</span>
|
||||
),
|
||||
}),
|
||||
];
|
||||
|
||||
return <Table data={data} setData={setData} columns={columns} />;
|
||||
}
|
||||
|
|
|
@ -1,224 +1,225 @@
|
|||
import {
|
||||
Row,
|
||||
ColumnDef,
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
flexRender,
|
||||
createColumnHelper
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
ChangeEvent,
|
||||
useState,
|
||||
useEffect,
|
||||
Key,
|
||||
Dispatch,
|
||||
SetStateAction
|
||||
} from "react";
|
||||
import { TableAction } from "./TableAction";
|
||||
import { PlusIcon } from "@heroicons/react/24/solid";
|
||||
import { rankItem } from "@tanstack/match-sorter-utils";
|
||||
import { RowOptionMenu } from "./RowOptionMenu";
|
||||
import DataPoint from "@/utils/models/DataPoint";
|
||||
|
||||
type TableProps<T extends DataPoint> = {
|
||||
data: T[],
|
||||
setData: Dispatch<SetStateAction<T[]>>,
|
||||
columns: ColumnDef<T, any>[]
|
||||
};
|
||||
|
||||
/** Fuzzy search function */
|
||||
const fuzzyFilter = (
|
||||
row: Row<any>,
|
||||
columnId: string,
|
||||
value: any,
|
||||
addMeta: (meta: any) => void
|
||||
) => {
|
||||
// Rank the item
|
||||
const itemRank = rankItem(row.getValue(columnId), value);
|
||||
|
||||
// Store the ranking info
|
||||
addMeta(itemRank);
|
||||
|
||||
// Return if the item should be filtered in/out
|
||||
return itemRank.passed;
|
||||
};
|
||||
|
||||
/**
|
||||
* General componenet that holds shared functionality for any data table component
|
||||
* @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.columns Column definitions made with Tanstack columnHelper
|
||||
*/
|
||||
export default function Table<T extends DataPoint>({ data, setData, columns }: TableProps<T>) {
|
||||
const columnHelper = createColumnHelper<T>();
|
||||
|
||||
/** Sorting function based on visibility */
|
||||
const visibilitySort = (a: T, b: T) => (
|
||||
a.visible === b.visible
|
||||
? 0
|
||||
: a.visible ? -1 : 1
|
||||
)
|
||||
|
||||
// Sort data on load
|
||||
useEffect(() => {
|
||||
setData(prevData => prevData.sort(visibilitySort))
|
||||
}, [setData]);
|
||||
|
||||
// Data manipulation methods
|
||||
// TODO: Connect data manipulation methods to the database (deleteData, hideData, addData)
|
||||
const deleteData = (dataId: number) => {
|
||||
console.log(data);
|
||||
setData((currentData) =>
|
||||
currentData.filter((data) => data.id !== dataId)
|
||||
);
|
||||
};
|
||||
|
||||
const hideData = (dataId: number) => {
|
||||
console.log(`Toggling visibility for data with ID: ${dataId}`);
|
||||
setData(currentData => {
|
||||
const newData = currentData
|
||||
.map(data => (
|
||||
data.id === dataId
|
||||
? { ...data, visible: !data.visible }
|
||||
: data
|
||||
))
|
||||
.sort(visibilitySort);
|
||||
|
||||
console.log(newData);
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
|
||||
const addData = () => {
|
||||
setData([...data]);
|
||||
};
|
||||
|
||||
// Add data manipulation options to the first column
|
||||
columns.unshift(
|
||||
columnHelper.display({
|
||||
id: "options",
|
||||
cell: (props) => (
|
||||
<RowOptionMenu
|
||||
onDelete={() => deleteData(props.row.original.id)}
|
||||
onHide={() => hideData(props.row.original.id)}
|
||||
/>
|
||||
),
|
||||
})
|
||||
)
|
||||
|
||||
// Searching
|
||||
const [query, setQuery] = useState("");
|
||||
const handleSearchChange = (e: ChangeEvent) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
setQuery(String(target.value));
|
||||
};
|
||||
|
||||
const handleCellChange = (e: ChangeEvent, key: Key) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
console.log(key);
|
||||
};
|
||||
|
||||
// TODO: Filtering
|
||||
|
||||
// TODO: Sorting
|
||||
|
||||
// Define Tanstack table
|
||||
const table = useReactTable({
|
||||
columns,
|
||||
data,
|
||||
filterFns: {
|
||||
fuzzy: fuzzyFilter,
|
||||
},
|
||||
state: {
|
||||
globalFilter: query,
|
||||
},
|
||||
onGlobalFilterChange: setQuery,
|
||||
globalFilterFn: fuzzyFilter,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const handleRowData = (row: any) => {
|
||||
const rowData: any = {};
|
||||
row.cells.forEach((cell: any) => {
|
||||
rowData[cell.column.id] = cell.value;
|
||||
});
|
||||
// Use rowData object containing data from all columns for the current row
|
||||
console.log(rowData);
|
||||
return rowData;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row justify-end">
|
||||
<TableAction query={query} handleChange={handleSearchChange} />
|
||||
</div>
|
||||
<table className="w-full text-xs text-left rtl:text-right">
|
||||
<thead className="text-xs text-gray-500 capitalize">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header, i) => (
|
||||
<th
|
||||
scope="col"
|
||||
className={
|
||||
"p-2 border-gray-200 border-y font-medium " +
|
||||
(1 < i && i < columns.length - 1
|
||||
? "border-x"
|
||||
: "")
|
||||
}
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
// Individual row
|
||||
const isDataVisible = row.original.visible;
|
||||
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${
|
||||
!isDataVisible ? "bg-gray-200 text-gray-500" : ""
|
||||
}`;
|
||||
return (
|
||||
<tr className={rowClassNames} key={row.id}>
|
||||
{row.getVisibleCells().map((cell, i) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className={
|
||||
"[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none"
|
||||
}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td
|
||||
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
|
||||
colSpan={100}
|
||||
onClick={addData}
|
||||
>
|
||||
<span className="flex ml-1 text-gray-500">
|
||||
<PlusIcon className="inline h-4 mr-1" />
|
||||
New
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import {
|
||||
Row,
|
||||
ColumnDef,
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
flexRender,
|
||||
createColumnHelper,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
ChangeEvent,
|
||||
useState,
|
||||
useEffect,
|
||||
Key,
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
} from "react";
|
||||
import { TableAction } from "./TableAction";
|
||||
import { PlusIcon } from "@heroicons/react/24/solid";
|
||||
import { rankItem } from "@tanstack/match-sorter-utils";
|
||||
import { RowOptionMenu } from "./RowOptionMenu";
|
||||
import DataPoint from "@/utils/models/DataPoint";
|
||||
|
||||
type TableProps<T extends DataPoint> = {
|
||||
data: T[];
|
||||
setData: Dispatch<SetStateAction<T[]>>;
|
||||
columns: ColumnDef<T, any>[];
|
||||
};
|
||||
|
||||
/** Fuzzy search function */
|
||||
const fuzzyFilter = (
|
||||
row: Row<any>,
|
||||
columnId: string,
|
||||
value: any,
|
||||
addMeta: (meta: any) => void
|
||||
) => {
|
||||
// Rank the item
|
||||
const itemRank = rankItem(row.getValue(columnId), value);
|
||||
|
||||
// Store the ranking info
|
||||
addMeta(itemRank);
|
||||
|
||||
// Return if the item should be filtered in/out
|
||||
return itemRank.passed;
|
||||
};
|
||||
|
||||
/**
|
||||
* General componenet that holds shared functionality for any data table component
|
||||
* @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.columns Column definitions made with Tanstack columnHelper
|
||||
*/
|
||||
export default function Table<T extends DataPoint>({
|
||||
data,
|
||||
setData,
|
||||
columns,
|
||||
}: TableProps<T>) {
|
||||
const columnHelper = createColumnHelper<T>();
|
||||
|
||||
/** Sorting function based on visibility */
|
||||
const visibilitySort = (a: T, b: T) =>
|
||||
a.visible === b.visible ? 0 : a.visible ? -1 : 1;
|
||||
|
||||
// Sort data on load
|
||||
useEffect(() => {
|
||||
setData((prevData) => prevData.sort(visibilitySort));
|
||||
}, [setData]);
|
||||
|
||||
// Data manipulation methods
|
||||
// TODO: Connect data manipulation methods to the database (deleteData, hideData, addData)
|
||||
const deleteData = (dataId: number) => {
|
||||
console.log(data);
|
||||
setData((currentData) =>
|
||||
currentData.filter((data) => data.id !== dataId)
|
||||
);
|
||||
};
|
||||
|
||||
const hideData = (dataId: number) => {
|
||||
console.log(`Toggling visibility for data with ID: ${dataId}`);
|
||||
setData((currentData) => {
|
||||
const newData = currentData
|
||||
.map((data) =>
|
||||
data.id === dataId
|
||||
? { ...data, visible: !data.visible }
|
||||
: data
|
||||
)
|
||||
.sort(visibilitySort);
|
||||
|
||||
console.log(newData);
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
|
||||
const addData = () => {
|
||||
setData([...data]);
|
||||
};
|
||||
|
||||
// Add data manipulation options to the first column
|
||||
columns.unshift(
|
||||
columnHelper.display({
|
||||
id: "options",
|
||||
cell: (props) => (
|
||||
<RowOptionMenu
|
||||
onDelete={() => deleteData(props.row.original.id)}
|
||||
onHide={() => hideData(props.row.original.id)}
|
||||
/>
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
// Searching
|
||||
const [query, setQuery] = useState("");
|
||||
const handleSearchChange = (e: ChangeEvent) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
setQuery(String(target.value));
|
||||
};
|
||||
|
||||
const handleCellChange = (e: ChangeEvent, key: Key) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
console.log(key);
|
||||
};
|
||||
|
||||
// TODO: Filtering
|
||||
|
||||
// TODO: Sorting
|
||||
|
||||
// Define Tanstack table
|
||||
const table = useReactTable({
|
||||
columns,
|
||||
data,
|
||||
filterFns: {
|
||||
fuzzy: fuzzyFilter,
|
||||
},
|
||||
state: {
|
||||
globalFilter: query,
|
||||
},
|
||||
onGlobalFilterChange: setQuery,
|
||||
globalFilterFn: fuzzyFilter,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const handleRowData = (row: any) => {
|
||||
const rowData: any = {};
|
||||
row.cells.forEach((cell: any) => {
|
||||
rowData[cell.column.id] = cell.value;
|
||||
});
|
||||
// Use rowData object containing data from all columns for the current row
|
||||
console.log(rowData);
|
||||
return rowData;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row justify-end">
|
||||
<TableAction query={query} handleChange={handleSearchChange} />
|
||||
</div>
|
||||
<table className="w-full text-xs text-left rtl:text-right">
|
||||
<thead className="text-xs text-gray-500 capitalize">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header, i) => (
|
||||
<th
|
||||
scope="col"
|
||||
className={
|
||||
"p-2 border-gray-200 border-y font-medium " +
|
||||
(1 < i && i < columns.length - 1
|
||||
? "border-x"
|
||||
: "")
|
||||
}
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
// Individual row
|
||||
const isDataVisible = row.original.visible;
|
||||
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${
|
||||
!isDataVisible ? "bg-gray-200 text-gray-500" : ""
|
||||
}`;
|
||||
return (
|
||||
<tr className={rowClassNames} key={row.id}>
|
||||
{row.getVisibleCells().map((cell, i) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className={
|
||||
"[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none"
|
||||
}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td
|
||||
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
|
||||
colSpan={100}
|
||||
onClick={addData}
|
||||
>
|
||||
<span className="flex ml-1 text-gray-500">
|
||||
<PlusIcon className="inline h-4 mr-1" />
|
||||
New
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import { ArrowDownCircleIcon, AtSymbolIcon, Bars2Icon } from "@heroicons/react/24/solid";
|
||||
import {
|
||||
ArrowDownCircleIcon,
|
||||
AtSymbolIcon,
|
||||
Bars2Icon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import useTagsHandler from "@/components/TagsInput/TagsHandler";
|
||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
|
||||
|
@ -8,31 +12,27 @@ import TagsInput from "@/components/TagsInput/Index";
|
|||
import User from "@/utils/models/User";
|
||||
|
||||
type UserTableProps = {
|
||||
data: User[],
|
||||
setData: Dispatch<SetStateAction<User[]>>
|
||||
}
|
||||
data: User[];
|
||||
setData: Dispatch<SetStateAction<User[]>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Table componenet used for displaying users
|
||||
* @param props.data Stateful list of users to be displayed by the table
|
||||
* @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>();
|
||||
|
||||
|
||||
// Set up tag handling
|
||||
const roleProps = useTagsHandler([
|
||||
"administrator",
|
||||
"volunteer",
|
||||
"employee",
|
||||
])
|
||||
|
||||
const programProps = useTagsHandler([
|
||||
"community",
|
||||
"domestic",
|
||||
"economic",
|
||||
])
|
||||
|
||||
]);
|
||||
|
||||
const programProps = useTagsHandler(["community", "domestic", "economic"]);
|
||||
|
||||
// Define Tanstack columns
|
||||
const columns: ColumnDef<User, any>[] = [
|
||||
columnHelper.accessor("username", {
|
||||
|
@ -57,10 +57,7 @@ export default function UserTable({ data, setData }: UserTableProps ) {
|
|||
</>
|
||||
),
|
||||
cell: (info) => (
|
||||
<TagsInput
|
||||
presetValue={info.getValue()}
|
||||
{...roleProps}
|
||||
/>
|
||||
<TagsInput presetValue={info.getValue()} {...roleProps} />
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("email", {
|
||||
|
@ -74,7 +71,7 @@ export default function UserTable({ data, setData }: UserTableProps ) {
|
|||
{info.getValue()}
|
||||
</span>
|
||||
),
|
||||
}),
|
||||
}),
|
||||
columnHelper.accessor("program", {
|
||||
header: () => (
|
||||
<>
|
||||
|
@ -83,13 +80,10 @@ export default function UserTable({ data, setData }: UserTableProps ) {
|
|||
</>
|
||||
),
|
||||
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} />;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useState } from 'react';
|
||||
import { useState } from "react";
|
||||
|
||||
/**
|
||||
* 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 { presetOptions, setPresetOptions, getTagColor }
|
||||
}
|
||||
return { presetOptions, setPresetOptions, getTagColor };
|
||||
}
|
||||
|
|
|
@ -6,4 +6,4 @@ interface DataPoint {
|
|||
visible: boolean;
|
||||
}
|
||||
|
||||
export default DataPoint;
|
||||
export default DataPoint;
|
||||
|
|
Loading…
Reference in New Issue
Block a user