diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index a8e802be..539f4d28 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -14,6 +14,7 @@ def init_app(app): from .residents import Residents from .buildings import Buildings from .log_record_residents import LogRecordResidents + from .log_record_attn_tos import LogRecordAttnTos app.app_context().push() db.init_app(app) diff --git a/backend/app/models/log_record_attn_tos.py b/backend/app/models/log_record_attn_tos.py new file mode 100644 index 00000000..11069a90 --- /dev/null +++ b/backend/app/models/log_record_attn_tos.py @@ -0,0 +1,34 @@ +from sqlalchemy import inspect +from sqlalchemy.orm.properties import ColumnProperty + +from . import db + + +class LogRecordAttnTos(db.Model): + __tablename__ = "log_record_attn_tos" + + id = db.Column(db.Integer, primary_key=True, nullable=False) + log_record_id = db.Column( + db.Integer, db.ForeignKey("log_records.log_id"), nullable=False + ) + attn_to_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) + + def to_dict(self, include_relationships=False): + # define the entities table + cls = type(self) + + mapper = inspect(cls) + formatted = {} + for column in mapper.attrs: + field = column.key + attr = getattr(self, field) + # if it's a regular column, extract the value + if isinstance(column, ColumnProperty): + formatted[field] = attr + # otherwise, it's a relationship field + # (currently not applicable, but may be useful for entity groups) + elif include_relationships: + # recursively format the relationship + # don't format the relationship's relationships + formatted[field] = [obj.to_dict() for obj in attr] + return formatted diff --git a/backend/app/models/log_records.py b/backend/app/models/log_records.py index 2243a524..e0a1e470 100644 --- a/backend/app/models/log_records.py +++ b/backend/app/models/log_records.py @@ -10,7 +10,6 @@ class LogRecords(db.Model): datetime = db.Column(db.DateTime(timezone=True), nullable=False) flagged = db.Column(db.Boolean, nullable=False) attn_to = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True) - # TODO: replace open String fields with VarChar(NUM_CHARS) note = db.Column(db.String, nullable=False) building_id = db.Column(db.Integer, db.ForeignKey("buildings.id"), nullable=False) tags = db.relationship( @@ -19,6 +18,9 @@ class LogRecords(db.Model): residents = db.relationship( "Residents", secondary="log_record_residents", back_populates="log_records" ) + attn_tos = db.relationship( + "User", secondary="log_record_attn_tos", back_populates="log_records" + ) building = db.relationship("Buildings", back_populates="log_record") def to_dict(self, include_relationships=False): diff --git a/backend/app/models/user.py b/backend/app/models/user.py index 161f4f4e..890c3aeb 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -22,6 +22,9 @@ class User(db.Model): last_modified = db.Column( db.DateTime, server_default=func.now(), onupdate=func.now(), nullable=False ) + log_records = db.relationship( + "LogRecords", secondary="log_record_attn_tos", back_populates="attn_tos" + ) __table_args__ = ( db.CheckConstraint( diff --git a/backend/app/services/implementations/log_records_service.py b/backend/app/services/implementations/log_records_service.py index 5d88d772..2cb9a871 100644 --- a/backend/app/services/implementations/log_records_service.py +++ b/backend/app/services/implementations/log_records_service.py @@ -28,9 +28,11 @@ def add_record(self, log_record): residents = new_log_record["residents"] tags = new_log_record["tags"] + attn_tos = new_log_record["attn_tos"] del new_log_record["residents"] del new_log_record["tags"] + del new_log_record["attn_tos"] new_log_record["datetime"] = datetime.fromisoformat( new_log_record["datetime"].replace("Z", "+00:00") @@ -40,6 +42,7 @@ def add_record(self, log_record): new_log_record = LogRecords(**new_log_record) self.construct_residents(new_log_record, residents) self.construct_tags(new_log_record, tags) + self.construct_attn_tos(new_log_record, attn_tos) db.session.add(new_log_record) db.session.commit() @@ -65,6 +68,17 @@ def construct_tags(self, log_record, tags): raise Exception(f"Tag with id {tag_id} does not exist") log_record.tags.append(tag) + def construct_attn_tos(self, log_record, attn_tos): + attn_tos = list(set(attn_tos)) + for attn_to_id in attn_tos: + attn_to = User.query.filter_by(id=attn_to_id).first() + + if not attn_to: + raise Exception( + f"Employee (attn_to) with id {attn_to_id} does not exist" + ) + log_record.attn_tos.append(attn_to) + def to_json_list(self, logs): try: logs_list = [] @@ -78,18 +92,12 @@ def to_json_list(self, logs): "last_name": log[3], }, "residents": log[4] if log[4] else [], - "attn_to": { - "id": log[5], - "first_name": log[6], - "last_name": log[7], - } - if log[5] - else None, - "building": {"id": log[8], "name": log[9]}, - "tags": log[10] if log[10] else [], - "note": log[11], - "flagged": log[12], - "datetime": log[13].isoformat(), + "attn_tos": log[5] if log[5] else [], + "building": {"id": log[6], "name": log[7]}, + "tags": log[8] if log[8] else [], + "note": log[9], + "flagged": log[10], + "datetime": log[11].isoformat(), } ) return logs_list @@ -124,11 +132,13 @@ def filter_by_residents(self, residents): def filter_by_attn_tos(self, attn_tos): if type(attn_tos) == list: - sql_statement = f"\nattn_to={attn_tos[0]}" + sql_statement = f"\n'{attn_tos[0]}'=ANY (attn_to_ids)" for i in range(1, len(attn_tos)): - sql_statement = sql_statement + f"\nOR attn_to={attn_tos[i]}" + sql_statement = ( + sql_statement + f"\nAND '{attn_tos[i]}'=ANY (attn_to_ids)" + ) return sql_statement - return f"\nattn_to={attn_tos}" + return f"\n'{attn_tos}'=ANY (attn_to_ids)" def filter_by_date_range(self, date_range): sql = "" @@ -208,6 +218,14 @@ def join_tag_attributes(self): GROUP BY logs.log_id \n \ ) t ON logs.log_id = t.log_id\n" + def join_attn_to_attributes(self): + return "\nLEFT JOIN\n \ + (SELECT logs.log_id, ARRAY_AGG(users.id) AS attn_to_ids, ARRAY_AGG(CONCAT(users.first_name, ' ', users.last_name)) AS attn_to_names FROM log_records logs\n \ + JOIN log_record_attn_tos lrat ON logs.log_id = lrat.log_record_id\n \ + JOIN users ON lrat.attn_to_id = users.id\n \ + GROUP BY logs.log_id \n \ + ) at ON logs.log_id = at.log_id\n" + def get_log_records( self, page_number, @@ -223,9 +241,7 @@ def get_log_records( employees.first_name AS employee_first_name,\n \ employees.last_name AS employee_last_name,\n \ r.residents,\n \ - logs.attn_to,\n \ - attn_tos.first_name AS attn_to_first_name,\n \ - attn_tos.last_name AS attn_to_last_name,\n \ + at.attn_to_names, \n \ buildings.id AS building_id,\n \ buildings.name AS building_name,\n \ t.tag_names, \n \ @@ -233,12 +249,12 @@ def get_log_records( logs.flagged,\n \ logs.datetime\n \ FROM log_records logs\n \ - LEFT JOIN users attn_tos ON logs.attn_to = attn_tos.id\n \ JOIN users employees ON logs.employee_id = employees.id\n \ JOIN buildings on logs.building_id = buildings.id" sql += self.join_resident_attributes() sql += self.join_tag_attributes() + sql += self.join_attn_to_attributes() sql += self.filter_log_records(filters) sql += f"\nORDER BY datetime {sort_direction}" @@ -262,12 +278,12 @@ def count_log_records(self, filters=None): sql = "SELECT\n \ COUNT(*)\n \ FROM log_records logs\n \ - LEFT JOIN users attn_tos ON logs.attn_to = attn_tos.id\n \ JOIN users employees ON logs.employee_id = employees.id\n \ JOIN buildings on logs.building_id = buildings.id" sql += self.join_resident_attributes() sql += self.join_tag_attributes() + sql += self.join_attn_to_attributes() sql += self.filter_log_records(filters) num_results = db.session.execute(text(sql)) @@ -287,38 +303,19 @@ def delete_log_record(self, log_id): ) log_record_to_delete.residents = [] log_record_to_delete.tags = [] + log_record_to_delete.attn_tos = [] db.session.delete(log_record_to_delete) db.session.commit() def update_log_record(self, log_id, updated_log_record): - if "attn_to" in updated_log_record: - LogRecords.query.filter_by(log_id=log_id).update( - { - LogRecords.attn_to: updated_log_record["attn_to"], - } - ) - else: - LogRecords.query.filter_by(log_id=log_id).update( - { - LogRecords.attn_to: None, - } - ) - if "tags" in updated_log_record: - log_record = LogRecords.query.filter_by(log_id=log_id).first() - if log_record: - log_record.tags = [] - self.construct_tags(log_record, updated_log_record["tags"]) - else: - LogRecords.query.filter_by(log_id=log_id).update( - { - LogRecords.tags: None, - } - ) - log_record = LogRecords.query.filter_by(log_id=log_id).first() if log_record: log_record.residents = [] + log_record.tags = [] + log_record.attn_tos = [] self.construct_residents(log_record, updated_log_record["residents"]) + self.construct_tags(log_record, updated_log_record["tags"]) + self.construct_attn_tos(log_record, updated_log_record["attn_tos"]) updated_log_record = LogRecords.query.filter_by(log_id=log_id).update( { diff --git a/backend/migrations/versions/51ad56d133e9_create_log_records_attn_to_junction_.py b/backend/migrations/versions/51ad56d133e9_create_log_records_attn_to_junction_.py new file mode 100644 index 00000000..4d338d55 --- /dev/null +++ b/backend/migrations/versions/51ad56d133e9_create_log_records_attn_to_junction_.py @@ -0,0 +1,42 @@ +"""create log records attn to junction table + +Revision ID: 51ad56d133e9 +Revises: eff8a5a7fda3 +Create Date: 2024-04-21 00:02:19.646800 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "51ad56d133e9" +down_revision = "eff8a5a7fda3" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "log_record_attn_tos", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("log_record_id", sa.Integer(), nullable=False), + sa.Column("attn_to_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["attn_to_id"], + ["users.id"], + ), + sa.ForeignKeyConstraint( + ["log_record_id"], + ["log_records.log_id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("log_record_attn_tos") + # ### end Alembic commands ### diff --git a/frontend/src/APIClients/LogRecordAPIClient.ts b/frontend/src/APIClients/LogRecordAPIClient.ts index bc73a347..8a2cb06c 100644 --- a/frontend/src/APIClients/LogRecordAPIClient.ts +++ b/frontend/src/APIClients/LogRecordAPIClient.ts @@ -97,7 +97,7 @@ const createLog = async ({ note, tags, buildingId, - attnTo, + attnTos, }: CreateLogRecordParams): Promise => { try { const bearerToken = `Bearer ${getLocalStorageObjProperty( @@ -114,7 +114,7 @@ const createLog = async ({ note, tags, buildingId, - attnTo, + attnTos, }, { headers: { Authorization: bearerToken } }, ); @@ -148,7 +148,7 @@ const editLogRecord = async ({ note, tags, buildingId, - attnTo, + attnTos, }: EditLogRecordParams): Promise => { try { const bearerToken = `Bearer ${getLocalStorageObjProperty( @@ -163,7 +163,7 @@ const editLogRecord = async ({ datetime, flagged, note, - attnTo, + attnTos, tags, buildingId, }, diff --git a/frontend/src/components/forms/CreateLog.tsx b/frontend/src/components/forms/CreateLog.tsx index 6ea1291a..7876de68 100644 --- a/frontend/src/components/forms/CreateLog.tsx +++ b/frontend/src/components/forms/CreateLog.tsx @@ -39,6 +39,7 @@ import { combineDateTime, getFormattedTime } from "../../helper/dateHelpers"; import CreateToast from "../common/Toasts"; import { getLocalStorageObj } from "../../helper/localStorageHelpers"; import { SingleDatepicker } from "../common/Datepicker"; +import { UserStatus } from "../../types/UserTypes"; type Props = { getRecords: (pageNumber: number) => Promise; @@ -76,7 +77,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props): React.R const [buildingId, setBuildingId] = useState(-1); const [residents, setResidents] = useState([]); const [tags, setTags] = useState([]); - const [attnTo, setAttnTo] = useState(-1); + const [attnTos, setAttnTos] = useState([]); const [notes, setNotes] = useState(""); const [flagged, setFlagged] = useState(false); @@ -143,13 +144,14 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props): React.R } }; - const handleAttnToChange = ( - selectedOption: SingleValue<{ label: string; value: number }>, + const handleAttnTosChange = ( + selectedAttnTos: MultiValue, ) => { - if (selectedOption !== null) { - setAttnTo(selectedOption.value); - } else { - setAttnTo(-1); + const mutableSelectedAttnTos: SelectLabel[] = Array.from( + selectedAttnTos, + ); + if (mutableSelectedAttnTos !== null) { + setAttnTos(mutableSelectedAttnTos.map((attnToLabel) => attnToLabel.value)); } }; @@ -187,7 +189,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props): React.R const usersData = await UserAPIClient.getUsers({ returnAll: true }); if (usersData && usersData.users.length !== 0) { const userLabels: SelectLabel[] = usersData.users - .filter((user) => user.userStatus === "Active") + .filter((user) => user.userStatus === UserStatus.ACTIVE) .map((user) => ({ label: `${user.firstName} ${user.lastName}`, value: user.id, @@ -216,7 +218,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props): React.R setBuildingId(-1); setResidents([]); setTags([]); - setAttnTo(-1); + setAttnTos([]); setNotes(""); // reset all error states @@ -248,7 +250,6 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props): React.R setNotesError(true) return; } - const attentionTo = attnTo === -1 ? undefined : attnTo; setLoading(true) @@ -260,7 +261,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props): React.R note: notes, tags, buildingId, - attnTo: attentionTo, + attnTos, }); if (res != null) { newToast("Log record added", "Successfully added log record.", "success") @@ -387,12 +388,13 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props): React.R - Attention To + Attention Tos item.value === logRecord.attnTo?.id, + defaultValue={employeeOptions.filter((item) => + logRecord.attnTos.includes(item.label) )} /> diff --git a/frontend/src/components/forms/ViewLog.tsx b/frontend/src/components/forms/ViewLog.tsx index b4f99b18..5c94c7f0 100644 --- a/frontend/src/components/forms/ViewLog.tsx +++ b/frontend/src/components/forms/ViewLog.tsx @@ -32,6 +32,7 @@ type Props = { toggleEdit: () => void; residentOptions: SelectLabel[]; tagOptions: SelectLabel[]; + employeeOptions: SelectLabel[]; allowEdit: boolean; }; @@ -42,6 +43,7 @@ const ViewLog = ({ toggleEdit, residentOptions, tagOptions, + employeeOptions, allowEdit, }: Props): React.ReactElement => { const handleEdit = () => { @@ -158,17 +160,19 @@ const ViewLog = ({ - Attention To - Attention Tos +