diff --git a/backend/api/academics/hiring.py b/backend/api/academics/hiring.py index 6f60a24d5..dd0d90648 100644 --- a/backend/api/academics/hiring.py +++ b/backend/api/academics/hiring.py @@ -170,6 +170,7 @@ def get_hiring_summary_overview( page_size: int = 100, order_by: str = "", filter: str = "", + flagged: HiringAssignmentFlagFilter = HiringAssignmentFlagFilter.ALL, subject: User = Depends(registered_user), hiring_service: HiringService = Depends(), ) -> Paginated[HiringAssignmentSummaryOverview]: @@ -180,7 +181,7 @@ def get_hiring_summary_overview( page=page, page_size=page_size, order_by=order_by, filter=filter ) return hiring_service.get_hiring_summary_overview( - subject, term_id, pagination_params + subject, term_id, flagged, pagination_params ) diff --git a/backend/entities/academics/hiring/hiring_assignment_entity.py b/backend/entities/academics/hiring/hiring_assignment_entity.py index fffb1da18..b32b2722f 100644 --- a/backend/entities/academics/hiring/hiring_assignment_entity.py +++ b/backend/entities/academics/hiring/hiring_assignment_entity.py @@ -106,6 +106,9 @@ class HiringAssignmentEntity(EntityBase): # Stores the timestamp for the last time the assignment was updated. modified: Mapped[datetime] = mapped_column(DateTime, nullable=False) + # Stores whether the assignment is flagged for further review in the summary. + flagged: Mapped[bool] = mapped_column(Boolean, default=False) + @classmethod def from_draft_model(cls, overview: HiringAssignmentDraft) -> Self: return cls( @@ -119,6 +122,7 @@ def from_draft_model(cls, overview: HiringAssignmentDraft) -> Self: position_number=overview.position_number, epar=overview.epar, i9=overview.i9, + flagged=overview.flagged, notes=overview.notes, created=overview.created, modified=overview.modified, @@ -134,6 +138,7 @@ def to_overview_model(self) -> HiringAssignmentOverview: epar=self.epar, i9=self.i9, notes=self.notes, + flagged=self.flagged, ) def to_summary_overview_model(self) -> HiringAssignmentSummaryOverview: @@ -159,6 +164,7 @@ def to_summary_overview_model(self) -> HiringAssignmentSummaryOverview: epar=self.epar, i9=self.i9, notes=self.notes, + flagged=self.flagged, ) def to_csv_row(self) -> HiringAssignmentCsvRow: diff --git a/backend/migrations/versions/0a57afd03df5_migration_for_applicant_flagging.py b/backend/migrations/versions/0a57afd03df5_migration_for_applicant_flagging.py new file mode 100644 index 000000000..63a5687df --- /dev/null +++ b/backend/migrations/versions/0a57afd03df5_migration_for_applicant_flagging.py @@ -0,0 +1,28 @@ +"""Migration for applicant-flagging + +Revision ID: 0a57afd03df5 +Revises: a9f09b49d862 +Create Date: 2025-11-24 16:09:49.309576 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '0a57afd03df5' +down_revision = 'a9f09b49d862' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('academics__hiring__assignment', sa.Column('flagged', sa.Boolean(), nullable=False, default=False)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('academics__hiring__assignment', 'flagged') + # ### end Alembic commands ### diff --git a/backend/models/academics/hiring/hiring_assignment.py b/backend/models/academics/hiring/hiring_assignment.py index 695811060..fde5270ac 100644 --- a/backend/models/academics/hiring/hiring_assignment.py +++ b/backend/models/academics/hiring/hiring_assignment.py @@ -18,6 +18,10 @@ class HiringAssignmentStatus(Enum): COMMIT = "Commit" FINAL = "Final" +class HiringAssignmentFlagFilter(Enum): + ALL = "all" + FLAGGED = "flagged" + NOT_FLAGGED = "not_flagged" class HiringAssignmentDraft(BaseModel): id: int | None = None @@ -31,6 +35,7 @@ class HiringAssignmentDraft(BaseModel): epar: str i9: bool notes: str + flagged: bool created: datetime modified: datetime @@ -44,6 +49,7 @@ class HiringAssignmentOverview(BaseModel): epar: str i9: bool notes: str + flagged: bool class HiringAssignmentSummaryOverview(BaseModel): @@ -61,6 +67,7 @@ class HiringAssignmentSummaryOverview(BaseModel): epar: str i9: bool notes: str + flagged: bool class HiringAssignmentCsvRow(BaseModel): diff --git a/backend/services/academics/hiring.py b/backend/services/academics/hiring.py index 15726edda..3014c354e 100644 --- a/backend/services/academics/hiring.py +++ b/backend/services/academics/hiring.py @@ -739,6 +739,7 @@ def update_hiring_assignment( assignment_entity.epar = assignment.epar assignment_entity.i9 = assignment.i9 assignment_entity.notes = assignment.notes + assignment_entity.flagged = assignment.flagged assignment_entity.modified = datetime.now() self._session.commit() @@ -807,7 +808,11 @@ def update_hiring_level(self, subject: User, level: HiringLevel) -> HiringLevel: return level_entity.to_model() def get_hiring_summary_overview( - self, subject: User, term_id: str, pagination_params: PaginationParams + self, + subject: User, + term_id: str, + flagged: HiringAssignmentFlagFilter, + pagination_params: PaginationParams, ) -> Paginated[HiringAssignmentSummaryOverview]: """ Returns the hires to show on a summary page for a given term. @@ -815,6 +820,7 @@ def get_hiring_summary_overview( Args: subject: The user making the request term_id: The term to get assignments for + flagged: Filter for flagged assignments ('flagged', 'not_flagged', or 'all') pagination_params: Parameters for pagination and filtering Raises: @@ -851,26 +857,32 @@ def get_hiring_summary_overview( ) base_query = base_query.where(criteria) - # 5. Create count query from base query + # 5. Apply flagged filter if present + if flagged == HiringAssignmentFlagFilter.FLAGGED: + base_query = base_query.where(HiringAssignmentEntity.flagged.is_(True)) + elif flagged == HiringAssignmentFlagFilter.NOT_FLAGGED: + base_query = base_query.where(HiringAssignmentEntity.flagged.is_(False)) + + # 6. Create count query from base query count_query = select(func.count()).select_from(base_query.subquery()) - # 6. Create assignment query with eager loading + # 7. Create assignment query with eager loading assignment_query = base_query.options( joinedload(HiringAssignmentEntity.course_site) .joinedload(CourseSiteEntity.sections) .joinedload(SectionEntity.staff), ) - # 7. Apply pagination + # 8. Apply pagination offset = pagination_params.page * pagination_params.page_size limit = pagination_params.page_size assignment_query = assignment_query.offset(offset).limit(limit) - # 8. Execute queries + # 9. Execute queries length = self._session.scalar(count_query) or 0 assignment_entities = self._session.scalars(assignment_query).unique().all() - # 9. Build and return response + # 10. Build and return response return Paginated( items=[ assignment.to_summary_overview_model() diff --git a/backend/test/services/academics/hiring/hiring_data.py b/backend/test/services/academics/hiring/hiring_data.py index f272da7f6..8feeb28f1 100644 --- a/backend/test/services/academics/hiring/hiring_data.py +++ b/backend/test/services/academics/hiring/hiring_data.py @@ -252,6 +252,23 @@ notes="Some notes here", created=datetime.now(), modified=datetime.now(), + flagged=False, +) + +hiring_assignment_flagged = HiringAssignmentDraft( + id=1, + user_id=user_data.student.id, + term_id=term_data.current_term.id, + course_site_id=office_hours_data.comp_110_site.id, + level=uta_level, + status=HiringAssignmentStatus.COMMIT, + position_number="sample", + epar="12345", + i9=True, + notes="Some notes here", + created=datetime.now(), + modified=datetime.now(), + flagged=True, ) updated_hiring_assignment = HiringAssignmentDraft( @@ -267,6 +284,7 @@ notes="Some notes here", created=datetime.now(), modified=datetime.now(), + flagged=False, ) new_hiring_assignment = HiringAssignmentDraft( @@ -282,9 +300,26 @@ notes="Some notes here", created=datetime.now(), modified=datetime.now(), + flagged=False, +) + +new_flagged_hiring_assignment = HiringAssignmentDraft( + id=3, + user_id=user_data.instructor.id, + term_id=term_data.current_term.id, + course_site_id=office_hours_data.comp_110_site.id, + level=uta_level, + status=HiringAssignmentStatus.FINAL, + position_number="sample", + epar="12345", + i9=True, + notes="Some notes here", + created=datetime.now(), + modified=datetime.now(), + flagged=True, ) -hiring_assignments = [hiring_assignment] +hiring_assignments = [hiring_assignment, new_flagged_hiring_assignment] def insert_fake_data(session: Session): diff --git a/backend/test/services/academics/hiring/hiring_test.py b/backend/test/services/academics/hiring/hiring_test.py index ff8f45e4a..47ac5ce5c 100644 --- a/backend/test/services/academics/hiring/hiring_test.py +++ b/backend/test/services/academics/hiring/hiring_test.py @@ -33,6 +33,8 @@ from ...office_hours.office_hours_data import fake_data_fixture as insert_order_5 from .hiring_data import fake_data_fixture as insert_order_6 +from backend.models.pagination import PaginationParams + # Test data from ... import user_data @@ -207,6 +209,14 @@ def test_update_hiring_assignment_not_found(hiring_svc: HiringService): ) pytest.fail() +def test_update_hiring_assigment_flag(hiring_svc: HiringService): + """Ensures that the admin can update the flagged status of a hiring assignment.""" + assignment = hiring_svc.update_hiring_assignment( + user_data.root, hiring_data.hiring_assignment_flagged + ) + assert assignment is not None + assert assignment.flagged is True + def test_delete_hiring_assignment(hiring_svc: HiringService): """Ensures that the admin can delete hiring assignments.""" @@ -303,3 +313,55 @@ def test_get_phd_applicants(hiring_svc: HiringService): assert len(applicants) > 0 for applicant in applicants: assert applicant.program_pursued in {"PhD", "PhD (ABD)"} + + +def test_get_hiring_summary_overview_all(hiring_svc: HiringService): + """Test that the hiring summary overview returns all assignments.""" + term_id = term_data.current_term.id + pagination_params = PaginationParams(page=0, page_size=10, order_by="", filter="") + summary = hiring_svc.get_hiring_summary_overview( + user_data.root, term_id, "all", pagination_params + ) + assert summary is not None + assert len(summary.items) > 0 + assert all( + assignment.flagged in [True, False] for assignment in summary.items + ) + + +def test_get_hiring_summary_overview_flagged(hiring_svc: HiringService): + """Test that the hiring summary overview filters for flagged assignments.""" + term_id = term_data.current_term.id + pagination_params = PaginationParams(page=0, page_size=10, order_by="", filter="") + summary = hiring_svc.get_hiring_summary_overview( + user_data.root, term_id, "flagged", pagination_params + ) + assert summary is not None + assert len(summary.items) > 0 + assert all(assignment.flagged is True for assignment in summary.items) + + +def test_get_hiring_summary_overview_not_flagged(hiring_svc: HiringService): + """Test that the hiring summary overview filters for not flagged assignments.""" + term_id = term_data.current_term.id + pagination_params = PaginationParams(page=0, page_size=10, order_by="", filter="") + summary = hiring_svc.get_hiring_summary_overview( + user_data.root, term_id, "not_flagged", pagination_params + ) + assert summary is not None + assert len(summary.items) > 0 + assert all(assignment.flagged is False for assignment in summary.items) + + +def test_get_hiring_summary_overview_invalid_flagged(hiring_svc: HiringService): + """Test that an invalid flagged filter returns all flagged/non-flagged assignments.""" + term_id = term_data.current_term.id + pagination_params = PaginationParams(page=0, page_size=10, order_by="", filter="") + summary = hiring_svc.get_hiring_summary_overview( + user_data.root, term_id, "invalid_flagged", pagination_params + ) + + assert len(summary.items) > 0 + assert all(assignment.flagged in [True, False] for assignment in summary.items) + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2745f7dff..87ab2b405 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -336,6 +336,21 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/@angular-devkit/architect/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular-devkit/architect/node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -391,7 +406,6 @@ "integrity": "sha512-yuC2vN4VL48JhnsaOa9J/o0Jl+cxOklRNQp5J2/ypMuRROaVCrZAPiX+ChSHh++kHYMpj8+ggNrrUwRNfMKACQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@angular-devkit/core": "18.2.21", "jsonc-parser": "3.3.1", @@ -411,7 +425,6 @@ "integrity": "sha512-Lno6GNbJME85wpc/uqn+wamBxvfZJZFYSH8+oAkkyjU/hk8r5+X8DuyqsKAa0m8t46zSTUsonHsQhVe5vgrZeQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ajv": "8.17.1", "ajv-formats": "3.0.1", @@ -539,6 +552,7 @@ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.3.1.tgz", "integrity": "sha512-mexSwaikVE2s+GDhB9fuagEvxbnKHWsqLlO7/R2nY9tTUxBO3drWe3p0D5GxG/EsEyzZU+86ED867q/JmAiVvw==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -689,6 +703,7 @@ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.4.tgz", "integrity": "sha512-5UzrN854pnQH+Qw6XZRxx2zWkcOxKrzWPLXe+gHFxFhxWUZfJKGcTJeAj8bnmyb+C3lqBbGpoNQPQ8pFXQGEaQ==", "license": "MIT", + "peer": true, "dependencies": { "parse5": "^8.0.0", "tslib": "^2.3.0" @@ -897,6 +912,21 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/@angular/cli/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular/cli/node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -951,6 +981,7 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.1.tgz", "integrity": "sha512-7Ru3BO4MOBQRMu9GJS+061cUsevKNsNAMxXnQtcqEaNyntUg2v0XiMdv4I7pQGtkQjFK17bKAxQ97jqxJfqsRQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -967,6 +998,7 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.1.tgz", "integrity": "sha512-zRYAdAG/hsJegXapKxElLU6Q5in8UG9Pbxyh90k89qsZwkuv+CfxVY5OBS2xjk1azt808++yhjfvbO/Em+HMKg==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -980,6 +1012,7 @@ "integrity": "sha512-aFfGHi/ApYxmvF4cCS0TypcviQ/Xy+0fwTTrLC8znPC1vObBn0DUA0I6D5dP+xlOTx8PFLkgndNYa2f6RIluvg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "7.28.3", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -1042,6 +1075,7 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.1.tgz", "integrity": "sha512-O03k9ivZ2CvoHXiXGH5WKlWlTtxF2UGMwGXWnV54vGViHwNcvU5Z3h6Ve6mdU9dYMHK9sGljYZnkRpwI3B8mnQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -1067,6 +1101,7 @@ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.1.tgz", "integrity": "sha512-P7cmfK1ldXS8KuPTwwIUTZs5AxhbPNumlumq+nfNJZAxv8/PQJh2W729M/EKHG8rB8cXjoo1K+olExnJNPVDTw==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -1102,6 +1137,7 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.1.tgz", "integrity": "sha512-JiQWRvyVZDH0N9p+pnMOuTFGaw7jPakWDQCJBOBBLdE6AyOiy8YPBImRMrjNNIEqg36h1a8H32rBorf2TL3ExA==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -1214,6 +1250,7 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2461,6 +2498,7 @@ "integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@inquirer/checkbox": "^4.2.1", "@inquirer/confirm": "^5.1.14", @@ -4755,6 +4793,21 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/@schematics/angular/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@schematics/angular/node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -4967,6 +5020,7 @@ "integrity": "sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -5024,6 +5078,7 @@ "integrity": "sha512-U7iTAtGgJk6DPX9wIWPPOlt1gO57097G06gIcl0N0EEnNw8RGD62c+2/DiP/zL7KrkqnnqF7gtFGR7YgzPllTA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.1.0", "@typescript-eslint/types": "8.1.0", @@ -5139,6 +5194,7 @@ "integrity": "sha512-ypRueFNKTIFwqPeJBfeIpxZ895PQhNyH4YID6js0UoBImWYoSjBsahUn9KMiJXh94uOjVBgHD9AmkyPsPnFwJA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.1.0", @@ -5231,6 +5287,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5458,8 +5515,7 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/base64id": { "version": "2.0.0", @@ -5520,7 +5576,6 @@ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -5619,6 +5674,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -5653,7 +5709,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -5870,6 +5925,7 @@ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -6023,7 +6079,6 @@ "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.8" } @@ -6262,7 +6317,6 @@ "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "clone": "^1.0.2" }, @@ -6703,6 +6757,7 @@ "integrity": "sha512-JfiKJrbx0506OEerjK2Y1QlldtBxkAlLxT5OEcRF8uaQ86noDe2k31Vw9rnSWv+MXZHj7OOUV/dA0AhdLFcyvA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", @@ -6763,6 +6818,7 @@ "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -8112,8 +8168,7 @@ "url": "https://feross.org/support" } ], - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/ignore": { "version": "5.3.2", @@ -8318,7 +8373,6 @@ "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -8356,7 +8410,6 @@ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -8499,7 +8552,8 @@ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.2.0.tgz", "integrity": "sha512-tSAtdrvWybZkQmmaIoDgnvHG8ORUNw5kEVlO5CvrXj02Jjr9TZrmjFq7FUiOUzJiOP2wLGYT6PgrQgQF4R1xiw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/js-tokens": { "version": "4.0.0", @@ -8611,6 +8665,7 @@ "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -8952,6 +9007,7 @@ "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", @@ -9082,7 +9138,6 @@ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" @@ -9100,7 +9155,6 @@ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -9117,7 +9171,6 @@ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -9135,7 +9188,6 @@ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -9361,7 +9413,6 @@ "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } @@ -9539,7 +9590,6 @@ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -10293,7 +10343,6 @@ "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", @@ -10318,7 +10367,6 @@ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -10335,7 +10383,6 @@ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -10353,7 +10400,6 @@ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "restore-cursor": "^3.1.0" }, @@ -10367,7 +10413,6 @@ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mimic-fn": "^2.1.0" }, @@ -10384,7 +10429,6 @@ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" @@ -10398,8 +10442,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/ora/node_modules/supports-color": { "version": "7.2.0", @@ -10407,7 +10450,6 @@ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -10808,6 +10850,7 @@ "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11089,6 +11132,7 @@ "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -11476,7 +11520,6 @@ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -11790,6 +11833,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -12246,7 +12290,6 @@ "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">= 8" } @@ -12375,7 +12418,6 @@ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -12695,7 +12737,8 @@ "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tuf-js": { "version": "3.1.0", @@ -12770,6 +12813,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12911,8 +12955,7 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/utils-merge": { "version": "1.0.1", @@ -12961,6 +13004,7 @@ "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -13150,7 +13194,6 @@ "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "defaults": "^1.0.3" } @@ -13425,6 +13468,7 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -13443,7 +13487,8 @@ "version": "0.15.1", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", - "license": "MIT" + "license": "MIT", + "peer": true } } } diff --git a/frontend/src/app/hiring/dialogs/create-assignment-dialog/create-assignment.dialog.ts b/frontend/src/app/hiring/dialogs/create-assignment-dialog/create-assignment.dialog.ts index 824bd1113..e008807ec 100644 --- a/frontend/src/app/hiring/dialogs/create-assignment-dialog/create-assignment.dialog.ts +++ b/frontend/src/app/hiring/dialogs/create-assignment-dialog/create-assignment.dialog.ts @@ -81,6 +81,7 @@ export class CreateAssignmentDialog { epar: this.createAssignmentForm.get('epar')!.value ?? '', i9: this.createAssignmentForm.get('i9')!.value ?? false, notes: this.createAssignmentForm.get('notes')!.value ?? '', + flagged: false, created: new Date(), modified: new Date() }; diff --git a/frontend/src/app/hiring/dialogs/edit-assignment-dialog/edit-assignment.dialog.ts b/frontend/src/app/hiring/dialogs/edit-assignment-dialog/edit-assignment.dialog.ts index 290178b6d..3eb790585 100644 --- a/frontend/src/app/hiring/dialogs/edit-assignment-dialog/edit-assignment.dialog.ts +++ b/frontend/src/app/hiring/dialogs/edit-assignment-dialog/edit-assignment.dialog.ts @@ -97,6 +97,7 @@ export class EditAssignmentDialog { epar: this.editAssignmentForm.get('epar')!.value ?? '', i9: this.editAssignmentForm.get('i9')!.value ?? false, notes: this.editAssignmentForm.get('notes')!.value ?? '', + flagged: false, created: new Date(), // Will be overwritten anyway modified: new Date() }; diff --git a/frontend/src/app/hiring/dialogs/quick-create-assignment-dialog/quick-create-assignment.dialog.ts b/frontend/src/app/hiring/dialogs/quick-create-assignment-dialog/quick-create-assignment.dialog.ts index c7aec00ba..a4f8cfd83 100644 --- a/frontend/src/app/hiring/dialogs/quick-create-assignment-dialog/quick-create-assignment.dialog.ts +++ b/frontend/src/app/hiring/dialogs/quick-create-assignment-dialog/quick-create-assignment.dialog.ts @@ -142,6 +142,7 @@ export class QuickCreateAssignmentDialog { epar: this.createAssignmentForm.get('epar')!.value ?? '', i9: this.createAssignmentForm.get('i9')!.value ?? false, notes: this.createAssignmentForm.get('notes')!.value ?? '', + flagged: false, created: new Date(), modified: new Date() }; diff --git a/frontend/src/app/hiring/hiring-summary/hiring-summary.component.css b/frontend/src/app/hiring/hiring-summary/hiring-summary.component.css index 6f542b9c9..3386d22ac 100644 --- a/frontend/src/app/hiring/hiring-summary/hiring-summary.component.css +++ b/frontend/src/app/hiring/hiring-summary/hiring-summary.component.css @@ -14,6 +14,9 @@ mat-card-header { .header-actions { margin-left: auto; + display: flex; + align-items: center; + gap: 0.5em; } .term-selector { @@ -41,4 +44,13 @@ mat-form-field.pos_number { max-width: 100% !important; margin-right: 32px !important; } -} \ No newline at end of file +} + +.flag-icon-btn { + transition: color 0.15s ease; +} + +.flag-selected { + font-variation-settings: + 'FILL' 1; +} diff --git a/frontend/src/app/hiring/hiring-summary/hiring-summary.component.html b/frontend/src/app/hiring/hiring-summary/hiring-summary.component.html index c9fe1bf9b..1e29dc8fa 100644 --- a/frontend/src/app/hiring/hiring-summary/hiring-summary.component.html +++ b/frontend/src/app/hiring/hiring-summary/hiring-summary.component.html @@ -8,7 +8,16 @@ {{ selectedTerm().name }} Onboarding
- + + All + Flagged + Not Flagged + + + Select Term
- + +
+ + + + + + - + + + + - - + + - - + + - - + + - - + + - - + +
Flagged
+ + -
Name
-
Name
-
-

{{ element.user.first_name }} {{ element.user.last_name }}
{{ element.user.pid }}

+
+

+ {{ element.user.first_name }} {{ element.user.last_name }}
{{ element.user.pid }} +

Course
-

{{ element.course }}
+

{{ element.course }}
{{ element.instructors }}

Hiring Level
-

+

${{ element.level.salary.toFixed(2) }} @@ -59,79 +86,73 @@

-
Epar
-
+
Epar
-
Pos #
-
+
Pos #
-
Notes
-
+
Notes
+ [(ngModel)]="element.notes" + (change)="updateAssignment({ assignment: element })">
-
I9?
-
+
I9?
+ [(ngModel)]="element.i9" + (change)="updateAssignment({ assignment: element })" /> -
Status
-
+
Status
+ [(ngModel)]="element.status" + (change)="updateAssignment({ assignment: element })"> Commit Final
@@ -142,4 +163,4 @@ (page)="handlePageEvent($event)">
- + \ No newline at end of file diff --git a/frontend/src/app/hiring/hiring-summary/hiring-summary.component.ts b/frontend/src/app/hiring/hiring-summary/hiring-summary.component.ts index 70d502005..288bfeb72 100644 --- a/frontend/src/app/hiring/hiring-summary/hiring-summary.component.ts +++ b/frontend/src/app/hiring/hiring-summary/hiring-summary.component.ts @@ -19,6 +19,7 @@ import { HiringAssignmentSummaryOverview } from '../hiring.models'; import { PageEvent } from '@angular/material/paginator'; +import { MatSnackBar } from '@angular/material/snack-bar'; const DEFAULT_PAGINATION_PARAMS = { page: 0, @@ -28,10 +29,10 @@ const DEFAULT_PAGINATION_PARAMS = { } as PaginationParams; @Component({ - selector: 'app-hiring-summary', - templateUrl: './hiring-summary.component.html', - styleUrl: './hiring-summary.component.css', - standalone: false + selector: 'app-hiring-summary', + templateUrl: './hiring-summary.component.html', + styleUrl: './hiring-summary.component.css', + standalone: false }) export class HiringSummaryComponent { /** Route for the routing module */ @@ -53,22 +54,30 @@ export class HiringSummaryComponent { return this.terms.find((term) => term.id === this.selectedTermId())!; }); + /** Current filter mode for applicants*/ + public filterMode: WritableSignal<'all' | 'flagged' | 'not_flagged'> = + signal('all'); + /** Effect that updates the hiring data when the selected term changes. */ selectedTermEffect = effect(() => { - if (this.selectedTermId()) { - const term = this.terms.find( - (term) => term.id === this.selectedTermId() - )!; - // Load paginated data - this.assignmentsPaginator.changeApiRoute( - `/api/hiring/summary/${term.id}` - ); - - this.assignmentsPaginator - .loadPage(this.previousPaginationParams) - .subscribe((page) => { - this.assignmentsPage.set(page); - }); + const termId = this.selectedTermId(); + // We check for termId existence to avoid running this before data is ready + if (termId) { + // Pass the current filter mode to the URL construction + this.updatePaginatorUrl(termId, this.filterMode()); + this.refreshData(); + } + }); + + /** Effect that updates the hiring data when the filter mode changes. */ + filterModeEffect = effect(() => { + const mode = this.filterMode(); + const termId = this.selectedTermId(); + + // Only update if we have a term selected + if (termId) { + this.updatePaginatorUrl(termId, mode); + this.refreshData(); } }); @@ -81,6 +90,7 @@ export class HiringSummaryComponent { DEFAULT_PAGINATION_PARAMS; public displayedColumns: string[] = [ + 'flagged', 'name', 'course', 'level', @@ -103,15 +113,13 @@ export class HiringSummaryComponent { paginationParams.filter = this.searchBarQuery(); // Refresh the data - this.assignmentsPaginator.loadPage(paginationParams).subscribe((page) => { - this.assignmentsPage.set(page); - this.previousPaginationParams = paginationParams; - }); + this.refreshData(paginationParams); }); /** Constructor */ constructor( private route: ActivatedRoute, + private snackbar: MatSnackBar, protected hiringService: HiringService, protected academicsService: AcademicsService ) { @@ -122,18 +130,38 @@ export class HiringSummaryComponent { }; this.terms = data.terms; - this.selectedTermId.set(data.currentTerm?.id ?? undefined); + const termId = data.currentTerm?.id; + this.selectedTermId.set(termId); // Load paginated data + const initialUrl = termId + ? `/api/hiring/summary/${termId}?flagged=${this.filterMode()}` + : ''; + this.assignmentsPaginator = new Paginator( - `/api/hiring/summary/${data.currentTerm!.id}` + initialUrl + ); + + if (termId) { + this.refreshData(); + } + } + + /** Helper to update the paginator API URL with term and flag params */ + private updatePaginatorUrl(termId: string, flagMode: string) { + this.assignmentsPaginator.changeApiRoute( + `/api/hiring/summary/${termId}?flagged=${flagMode}` ); + } - this.assignmentsPaginator - .loadPage(this.previousPaginationParams) - .subscribe((page) => { - this.assignmentsPage.set(page); - }); + /** Helper to trigger the loadPage logic */ + private refreshData( + params: PaginationParams = this.previousPaginationParams + ) { + this.assignmentsPaginator.loadPage(params).subscribe((page) => { + this.assignmentsPage.set(page); + this.previousPaginationParams = params; + }); } /** Handles a pagination event for the future office hours table */ @@ -141,15 +169,30 @@ export class HiringSummaryComponent { let paginationParams = this.assignmentsPage()!.params; paginationParams.page = e.pageIndex; paginationParams.page_size = e.pageSize; - this.assignmentsPaginator.loadPage(paginationParams).subscribe((page) => { - this.assignmentsPage.set(page); - this.previousPaginationParams = paginationParams; + this.refreshData(paginationParams); + } + + toggleAssignmentFlag(assignment: HiringAssignmentSummaryOverview) { + // Optimistically update the flag state + assignment.flagged = !assignment.flagged; + // Call the service to update. If there is an error, we will rollback + // the optimistic update and display a snackbar notification. + this.updateAssignment({ + assignment, + onError: () => { + assignment.flagged = !assignment.flagged; + } }); } - /** Save changes */ - updateAssignment(assignmentIndex: number) { - let assignment = this.assignmentsPage()!.items[assignmentIndex]!; + /** Handles updating an assignment for all inputs */ + updateAssignment({ + assignment, + onError + }: { + assignment: HiringAssignmentSummaryOverview; + onError?: () => void; + }) { let draft: HiringAssignmentDraft = { id: assignment.id, user_id: assignment.user.id, @@ -162,10 +205,34 @@ export class HiringSummaryComponent { epar: assignment.epar, i9: assignment.i9, notes: assignment.notes, + flagged: assignment.flagged, created: new Date(), // will be overrided modified: new Date() }; - this.hiringService.updateHiringAssignment(draft).subscribe((_) => {}); + // Call the service to update. If there is an error, we will rollback + // the optimistic update and display a snackbar notification. + this.hiringService.updateHiringAssignment(draft).subscribe({ + error: (_) => { + onError?.(); + this.snackbar.open( + 'Failed to update assignment. Please try again.', + 'Close', + { duration: 5000 } + ); + } + }); + } + + /** Gets ordered hiring assignments */ + getOrderedAssignments(): HiringAssignmentSummaryOverview[] { + const assignments = this.assignmentsPage()?.items ?? []; + if (assignments.length === 0) { + return []; + } + + return [...assignments].sort( + (a, b) => Number(b.flagged) - Number(a.flagged) + ); } /** Export CSV button pressed */ diff --git a/frontend/src/app/hiring/hiring.models.ts b/frontend/src/app/hiring/hiring.models.ts index 5ba3bd823..572d2efce 100644 --- a/frontend/src/app/hiring/hiring.models.ts +++ b/frontend/src/app/hiring/hiring.models.ts @@ -90,6 +90,7 @@ export interface HiringAssignmentDraft { notes: string; created: Date; modified: Date; + flagged: boolean; } export interface HiringAssignmentOverview { @@ -101,6 +102,7 @@ export interface HiringAssignmentOverview { epar: string; i9: boolean; notes: string; + flagged: boolean; } export const hiringAssignmentOverviewToDraft = ( @@ -121,6 +123,7 @@ export const hiringAssignmentOverviewToDraft = ( epar: assignment.epar, i9: assignment.i9, notes: assignment.notes, + flagged: assignment.flagged, created: new Date(), // overwritten anyway modified: new Date() // overwritten anyway }; @@ -161,6 +164,7 @@ export interface HiringAssignmentSummaryOverview { epar: string; i9: boolean; notes: string; + flagged: boolean; } export interface ReleasedHiringAssignment { diff --git a/frontend/src/app/pagination.ts b/frontend/src/app/pagination.ts index 61d888f56..6e0753ce9 100644 --- a/frontend/src/app/pagination.ts +++ b/frontend/src/app/pagination.ts @@ -123,9 +123,11 @@ abstract class PaginatorAbstraction { // Stpres the previous pagination parameters used this.previousParams = paramStrings; + const separator = this.api.includes('?') ? '&' : '?'; + // Determines the query for the URL based on the new paramateres. let query = new URLSearchParams(paramStrings); - let route = this.api + '?' + query.toString(); + let route = this.api + separator + query.toString(); // Determine if an operator function is necessary if (operator) {