diff --git a/backend/app/models/tags.py b/backend/app/models/tags.py index 12990560..6dc87c14 100644 --- a/backend/app/models/tags.py +++ b/backend/app/models/tags.py @@ -9,7 +9,6 @@ class Tag(db.Model): tag_id = db.Column(db.Integer, primary_key=True, nullable=False) name = db.Column(db.String, unique=True, nullable=False) - status = db.Column(db.Enum("Deleted", "Active", name="status"), nullable=False) last_modified = db.Column( db.DateTime, server_default=db.func.now(), onupdate=db.func.now(), nullable=False ) diff --git a/backend/app/services/implementations/log_records_service.py b/backend/app/services/implementations/log_records_service.py index 17c33c20..4f7e23c7 100644 --- a/backend/app/services/implementations/log_records_service.py +++ b/backend/app/services/implementations/log_records_service.py @@ -25,8 +25,9 @@ def __init__(self, logger): def add_record(self, log_record): new_log_record = log_record.copy() + residents = new_log_record["residents"] - tag_names = new_log_record["tags"] + tags = new_log_record["tags"] del new_log_record["residents"] del new_log_record["tags"] @@ -34,7 +35,7 @@ def add_record(self, log_record): try: new_log_record = LogRecords(**new_log_record) self.construct_residents(new_log_record, residents) - self.construct_tags(new_log_record, tag_names) + self.construct_tags(new_log_record, tags) db.session.add(new_log_record) db.session.commit() @@ -51,12 +52,13 @@ def construct_residents(self, log_record, residents): raise Exception(f"Resident with id {resident_id} does not exist") log_record.residents.append(resident) - def construct_tags(self, log_record, tag_names): - for tag_name in tag_names: - tag = Tag.query.filter_by(name=tag_name).first() + def construct_tags(self, log_record, tags): + tags = list(set(tags)) + for tag_id in tags: + tag = Tag.query.filter_by(tag_id=tag_id).first() if not tag: - raise Exception(f"Tag with name {tag_name} does not exist") + raise Exception(f"Tag with id {tag_id} does not exist") log_record.tags.append(tag) def to_json_list(self, logs): @@ -146,12 +148,12 @@ def filter_by_date_range(self, date_range): return sql def filter_by_tags(self, tags): - if len(tags) >= 1: - sql_statement = f"\n'{tags[0]}'=ANY (tag_names)" + if type(tags) == list: + sql_statement = f"\n'{tags[0]}'=ANY (tag_ids)" for i in range(1, len(tags)): - sql_statement = sql_statement + f"\nAND '{tags[i]}'=ANY (tag_names)" + sql_statement = sql_statement + f"\nAND '{tags[i]}'=ANY (tag_ids)" return sql_statement - return f"\n'{tags}'=ANY (tag_names)" + return f"\n'{tags}'=ANY (tag_ids)" def filter_by_flagged(self, flagged): print(flagged) @@ -192,7 +194,7 @@ def join_resident_attributes(self): def join_tag_attributes(self): return "\nLEFT JOIN\n \ - (SELECT logs.log_id, ARRAY_AGG(tags.name) AS tag_names FROM log_records logs\n \ + (SELECT logs.log_id, ARRAY_AGG(tags.tag_id) AS tag_ids, ARRAY_AGG(tags.name) AS tag_names FROM log_records logs\n \ JOIN log_record_tag lrt ON logs.log_id = lrt.log_record_id\n \ JOIN tags ON lrt.tag_id = tags.tag_id\n \ GROUP BY logs.log_id \n \ diff --git a/frontend/src/APIClients/TagAPIClient.ts b/frontend/src/APIClients/TagAPIClient.ts new file mode 100644 index 00000000..5c53885f --- /dev/null +++ b/frontend/src/APIClients/TagAPIClient.ts @@ -0,0 +1,24 @@ +import { AxiosError } from "axios"; +import AUTHENTICATED_USER_KEY from "../constants/AuthConstants"; +import { getLocalStorageObjProperty } from "../utils/LocalStorageUtils"; +import baseAPIClient from "./BaseAPIClient"; +import { GetTagsResponse } from "../types/TagTypes"; + +const getTags = async (): Promise => { + try { + const bearerToken = `Bearer ${getLocalStorageObjProperty( + AUTHENTICATED_USER_KEY, + "accessToken", + )}`; + const { data } = await baseAPIClient.get(`/tags`, { + headers: { Authorization: bearerToken }, + }); + return data; + } catch (error) { + return null; + } +}; + +export default { + getTags, +}; diff --git a/frontend/src/components/forms/CreateLog.tsx b/frontend/src/components/forms/CreateLog.tsx index 1088daf1..d7f945e4 100644 --- a/frontend/src/components/forms/CreateLog.tsx +++ b/frontend/src/components/forms/CreateLog.tsx @@ -31,6 +31,7 @@ import { Col, Row } from "react-bootstrap"; import { AuthenticatedUser } from "../../types/AuthTypes"; import UserAPIClient from "../../APIClients/UserAPIClient"; import ResidentAPIClient from "../../APIClients/ResidentAPIClient"; +import TagAPIClient from "../../APIClients/TagAPIClient"; import { getLocalStorageObj } from "../../utils/LocalStorageUtils"; import AUTHENTICATED_USER_KEY from "../../constants/AuthConstants"; import LogRecordAPIClient from "../../APIClients/LogRecordAPIClient"; @@ -39,7 +40,8 @@ import { BuildingLabel } from "../../types/BuildingTypes"; import selectStyle from "../../theme/forms/selectStyles"; import { singleDatePickerStyle } from "../../theme/forms/datePickerStyles"; import { UserLabel } from "../../types/UserTypes"; -import { ResidentLabel } from "../../types/ResidentTypes"; +import { Resident, ResidentLabel } from "../../types/ResidentTypes"; +import { TagLabel } from "../../types/TagTypes"; import combineDateTime from "../../helper/combineDateTime"; type Props = { @@ -118,14 +120,15 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { ); const [buildingId, setBuildingId] = useState(-1); const [residents, setResidents] = useState([]); - const [tags, setTags] = useState([]); + const [tags, setTags] = useState([]); const [attnTo, setAttnTo] = useState(-1); const [notes, setNotes] = useState(""); const [flagged, setFlagged] = useState(false); const [employeeOptions, setEmployeeOptions] = useState([]); - const [residentOptions, setResidentOptions] = useState([]); + const [residentOptions, setResidentOptions] = useState([]); const [buildingOptions, setBuildingOptions] = useState([]); + const [tagOptions, setTagOptions] = useState([]); const [isCreateOpen, setCreateOpen] = React.useState(false); @@ -184,10 +187,14 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { }; const handleTagsChange = ( - selectedTags: MultiValue<{ label: string; value: string }>, + selectedTags: MultiValue, ) => { - const newTagsList = selectedTags.map((tag) => tag.value); - setTags(newTagsList); + const mutableSelectedTags: TagLabel[] = Array.from( + selectedTags, + ); + if (mutableSelectedTags !== null) { + setTags(mutableSelectedTags.map((tagLabel) => tagLabel.value)); + } }; const handleAttnToChange = ( @@ -206,7 +213,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { setNotesError(inputValue === ""); }; - // fetch resident + employee data for log creation + // fetch resident + employee + tag data for log creation const getLogEntryOptions = async () => { const buildingsData = await BuildingAPIClient.getBuildings(); @@ -239,6 +246,16 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { })); setEmployeeOptions(userLabels); } + + const tagsData = await TagAPIClient.getTags(); + if (tagsData && tagsData.tags.length !== 0) { + const tagLabels: TagLabel[] = tagsData.tags + .map((tag) => ({ + label: tag.name, + value: tag.tagId, + })); + setTagOptions(tagLabels); + } }; const handleCreateOpen = () => { @@ -426,9 +443,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { Tags logRecord.tags.includes(item.label), + )} /> diff --git a/frontend/src/components/pages/HomePage/HomePage.tsx b/frontend/src/components/pages/HomePage/HomePage.tsx index 49e5bfa4..77def219 100644 --- a/frontend/src/components/pages/HomePage/HomePage.tsx +++ b/frontend/src/components/pages/HomePage/HomePage.tsx @@ -10,7 +10,7 @@ import SearchAndFilters from "./SearchAndFilters"; import ExportCSVButton from "../../common/ExportCSVButton"; import { BuildingLabel } from "../../../types/BuildingTypes"; import { ResidentLabel } from "../../../types/ResidentTypes"; -import { Tag } from "../../../types/TagsTypes"; +import { TagLabel } from "../../../types/TagTypes"; import { UserLabel } from "../../../types/UserTypes"; import LogRecordAPIClient from "../../../APIClients/LogRecordAPIClient"; @@ -30,7 +30,7 @@ const HomePage = (): React.ReactElement => { const [employees, setEmployees] = useState([]); const [startDate, setStartDate] = useState(); const [endDate, setEndDate] = useState(); - const [tags, setTags] = useState([]); + const [tags, setTags] = useState([]); const [attentionTos, setAttentionTos] = useState([]); const [buildings, setBuildings] = useState([]); const [flagged, setFlagged] = useState(false); diff --git a/frontend/src/components/pages/HomePage/LogRecordsTable.tsx b/frontend/src/components/pages/HomePage/LogRecordsTable.tsx index 643d40ca..9aa4c864 100644 --- a/frontend/src/components/pages/HomePage/LogRecordsTable.tsx +++ b/frontend/src/components/pages/HomePage/LogRecordsTable.tsx @@ -28,8 +28,11 @@ import AuthContext from "../../../contexts/AuthContext"; import EditLog from "../../forms/EditLog"; import LogRecordAPIClient from "../../../APIClients/LogRecordAPIClient"; import ResidentAPIClient from "../../../APIClients/ResidentAPIClient"; +import TagAPIClient from "../../../APIClients/TagAPIClient"; import UserAPIClient from "../../../APIClients/UserAPIClient"; import BuildingAPIClient from "../../../APIClients/BuildingAPIClient"; +import { ResidentLabel } from "../../../types/ResidentTypes"; +import { TagLabel } from "../../../types/TagTypes"; import { UserLabel } from "../../../types/UserTypes"; import { BuildingLabel } from "../../../types/BuildingTypes"; import ConfirmationModal from "../../common/ConfirmationModal"; @@ -71,7 +74,8 @@ const LogRecordsTable = ({ // Dropdown option states const [employeeOptions, setEmployeeOptions] = useState([]); - const [residentOptions, setResidentOptions] = useState([]); + const [residentOptions, setResidentOptions] = useState([]); + const [tagOptions, setTagOptions] = useState([]); // Handle delete confirmation toggle const handleDeleteToggle = (logId: number) => { @@ -89,7 +93,7 @@ const LogRecordsTable = ({ })); }; - // fetch resident + employee data for log creation + // fetch resident + employee + tag data for log creation const getLogEntryOptions = async () => { const residentsData = await ResidentAPIClient.getResidents({ returnAll: true, @@ -123,6 +127,16 @@ const LogRecordsTable = ({ })); setEmployeeOptions(userLabels); } + + const tagsData = await TagAPIClient.getTags(); + if (tagsData && tagsData.tags.length !== 0) { + const tagLabels: TagLabel[] = tagsData.tags + .map((tag) => ({ + label: tag.name, + value: tag.tagId, + })); + setTagOptions(tagLabels); + } }; const deleteLogRecord = async (itemId: number) => { @@ -232,6 +246,7 @@ const LogRecordsTable = ({ toggleClose={() => handleEditToggle(record.logId)} employeeOptions={employeeOptions} residentOptions={residentOptions} + tagOptions={tagOptions} getRecords={getRecords} countRecords={countRecords} setUserPageNum={setUserPageNum} diff --git a/frontend/src/components/pages/HomePage/SearchAndFilters.tsx b/frontend/src/components/pages/HomePage/SearchAndFilters.tsx index 7f973c4b..b4e7d704 100644 --- a/frontend/src/components/pages/HomePage/SearchAndFilters.tsx +++ b/frontend/src/components/pages/HomePage/SearchAndFilters.tsx @@ -21,10 +21,11 @@ import selectStyle from "../../../theme/forms/selectStyles"; import { singleDatePickerStyle } from "../../../theme/forms/datePickerStyles"; import { BuildingLabel } from "../../../types/BuildingTypes"; import { Resident, ResidentLabel } from "../../../types/ResidentTypes"; -import { Tag } from "../../../types/TagsTypes"; +import { Tag, TagLabel } from "../../../types/TagTypes"; import { User, UserLabel } from "../../../types/UserTypes"; import UserAPIClient from "../../../APIClients/UserAPIClient"; import ResidentAPIClient from "../../../APIClients/ResidentAPIClient"; +import TagAPIClient from "../../../APIClients/TagAPIClient"; import BuildingAPIClient from "../../../APIClients/BuildingAPIClient"; import CreateToast from "../../common/Toasts"; @@ -33,7 +34,7 @@ type Props = { employees: UserLabel[]; startDate: Date | undefined; endDate: Date | undefined; - tags: Tag[]; + tags: TagLabel[]; attentionTos: UserLabel[]; buildings: BuildingLabel[]; flagged: boolean; @@ -41,19 +42,12 @@ type Props = { setEmployees: React.Dispatch>; setStartDate: React.Dispatch>; setEndDate: React.Dispatch>; - setTags: React.Dispatch>; + setTags: React.Dispatch>; setAttentionTos: React.Dispatch>; setBuildings: React.Dispatch>; setFlagged: React.Dispatch>; }; -// Replace this with the tags from the db once the API and table are made -const TAGS: Tag[] = [ - { label: "Tag A", value: "A" }, - { label: "Tag B", value: "B" }, - { label: "Tag C", value: "C" }, -]; - const SearchAndFilters = ({ residents, employees, @@ -75,6 +69,7 @@ const SearchAndFilters = ({ const [buildingOptions, setBuildingOptions] = useState([]); const [userLabels, setUserLabels] = useState(); const [residentLabels, setResidentLabels] = useState(); + const [tagLabels, setTagLabels] = useState(); const dateChangeToast = CreateToast(); @@ -117,6 +112,20 @@ const SearchAndFilters = ({ } }; + const getTags = async () => { + const data = await TagAPIClient.getTags(); + const tagsData = data?.tags; + if (tagsData) { + const labels = tagsData.map((tag: Tag) => { + return { + label: tag.name, + value: tag.tagId, + } as TagLabel; + }); + setTagLabels(labels); + } + }; + const handleBuildingChange = ( selectedBuildings: MultiValue, ) => { @@ -169,8 +178,8 @@ const SearchAndFilters = ({ setResidents(mutableSelectedResidents); }; - const handleTagsChange = (selectedTags: MultiValue) => { - const mutableSelectedTags: Tag[] = Array.from(selectedTags); + const handleTagsChange = (selectedTags: MultiValue) => { + const mutableSelectedTags: TagLabel[] = Array.from(selectedTags); setTags(mutableSelectedTags); }; @@ -189,6 +198,7 @@ const SearchAndFilters = ({ getBuildingsOptions(); getUsers(); getResidents(); + getTags(); }, []); return ( @@ -289,13 +299,12 @@ const SearchAndFilters = ({ Tags