From d258fd2ecfd8fd33616f95db51844c69b5f87298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?An=C4=B1l=20Akarsu?= Date: Tue, 1 Oct 2024 21:45:09 +0200 Subject: [PATCH] Refactor: Add datePosted filter for Job Listings (#977) * Refactor: Add datePosted filter for Job Listings - Added the datePosted field to the FindAllVisibleTpJobListingsArgsFilter input type in the types.ts file. - Updated the fetchAllTpJobListingsUsingFilters function in the api.tsx file to include the datePosted parameter when making API requests. - Modified the TpJobListingsService class in the tp-job-listings.service.ts file to filter job listings based on the datePosted field. - Updated the JobListingCard.generated.ts file to include the isFromCareerPartner field in the JobListingCardJobListingPropFragment type. - Modified the FilterDropdown.tsx file to add support for single select options. - Updated the JobListingCard.tsx file to display a "Promoted" label for job listings posted by ReDI Career Partners. * fix: add datePosted in clearFilters * Conditionally apply career partner sorting based on datePosted value in BrowseJobseeker component * Add 'Any time' option to job listing date filter and refactor date handling - Introduced `jobListingCreatedDate` and `jobListingCreatedDateInAge` constants for better date filter management. - Updated `BrowseJobseeker.tsx` to use these constants and handle 'Any time' option correctly. - Modified `tp-job-listings.service.ts` to utilize `jobListingCreatedDateInAge` for date calculations. --- ...-all-visible-tp-jobseeker-profiles.args.ts | 2 + .../tp-job-listings.service.ts | 14 +++++ .../src/assets/locales/en/translation.json | 4 ++ .../components/organisms/JobListingCard.tsx | 30 +++++++++-- .../JobListingCard.generated.ts | 3 +- .../JobListingCard.graphql | 1 + .../src/pages/app/browse/BrowseJobseeker.tsx | 52 +++++++++++++++++-- .../redi-talent-pool/src/services/api/api.tsx | 3 ++ .../src/utils/sort-job-listings.ts | 2 + libs/data-access/src/lib/types/types.ts | 1 + .../src/lib/molecules/FilterDropdown.scss | 7 ++- .../src/lib/molecules/FilterDropdown.tsx | 31 ++++++++--- .../config/src/lib/talent-pool-config.ts | 13 +++++ schema.graphql | 1 + 14 files changed, 147 insertions(+), 17 deletions(-) diff --git a/apps/nestjs-api/src/tp-job-listings/args/find-all-visible-tp-jobseeker-profiles.args.ts b/apps/nestjs-api/src/tp-job-listings/args/find-all-visible-tp-jobseeker-profiles.args.ts index ed7d91720..4dbe7a403 100644 --- a/apps/nestjs-api/src/tp-job-listings/args/find-all-visible-tp-jobseeker-profiles.args.ts +++ b/apps/nestjs-api/src/tp-job-listings/args/find-all-visible-tp-jobseeker-profiles.args.ts @@ -27,6 +27,8 @@ class FindAllVisibleTpJobListingsArgsFilter { isRemotePossible?: boolean + datePosted?: string + /** * Job Fair Boolean Field(s) * Uncomment & Rename (joins{Location}{Year}{Season}JobFair) the next field when there's an upcoming Job Fair diff --git a/apps/nestjs-api/src/tp-job-listings/tp-job-listings.service.ts b/apps/nestjs-api/src/tp-job-listings/tp-job-listings.service.ts index fff18ff11..c8b933fb5 100644 --- a/apps/nestjs-api/src/tp-job-listings/tp-job-listings.service.ts +++ b/apps/nestjs-api/src/tp-job-listings/tp-job-listings.service.ts @@ -11,6 +11,8 @@ import { TpJobListingStatus, } from '@talent-connect/common-types' import { deleteUndefinedProperties } from '@talent-connect/shared-utils' +import { jobListingCreatedDateInAge } from '@talent-connect/talent-pool/config' +import * as jsforce from 'jsforce' import { CurrentUserInfo } from '../auth/current-user.interface' import { SfApiTpJobListingsService } from '../salesforce-api/sf-api-tp-job-listings.service' import { TpCompanyRepresentativeRelationshipsService } from '../tp-company-profiles/tp-company-representative-relationships.service' @@ -67,6 +69,18 @@ export class TpJobListingsService { if (_filter.filter.isRemotePossible) { filter.Remote_Possible__c = true } + if (_filter.filter.datePosted) { + const createdDate = new Date() + + createdDate.setDate( + createdDate.getDate() - + jobListingCreatedDateInAge[_filter.filter.datePosted] + ) + + filter.CreatedDate = { + $gte: jsforce.SfDate.toDateTimeLiteral(createdDate), + } + } /** * Job Fair Boolean Field(s) * Uncomment & Rename (joins{Location}{Year}{Season}JobFair) the next block when there's an upcoming Job Fair diff --git a/apps/redi-talent-pool/src/assets/locales/en/translation.json b/apps/redi-talent-pool/src/assets/locales/en/translation.json index 17ca4f9fa..b7bbd9865 100644 --- a/apps/redi-talent-pool/src/assets/locales/en/translation.json +++ b/apps/redi-talent-pool/src/assets/locales/en/translation.json @@ -103,6 +103,10 @@ "question": "How can I browse Companies?", "answer": "Select the 'Browse' option located in the left navigation bar, then begin exploring the job postings. Feel free to use filters to refine your search for more specific results." }, + { + "question": "How do ReDI Career Partners' job listings benefit me as a job seeker?", + "answer": "Job listings from ReDI Career Partners are given higher visibility on our platform because these trusted companies actively source ReDI talent and support newcomers. By applying to their positions, you benefit from opportunities with employers who are committed to your success and closely collaborate with us to create more career opportunities in Germany." + }, { "question": "Will there be additional companies joining the Talent Pool?", "answer": "Yes, we are actively engaging with numerous companies and expanding our network of partner companies featured in the Talent Pool." diff --git a/apps/redi-talent-pool/src/components/organisms/JobListingCard.tsx b/apps/redi-talent-pool/src/components/organisms/JobListingCard.tsx index a20ace4f7..33c373052 100644 --- a/apps/redi-talent-pool/src/components/organisms/JobListingCard.tsx +++ b/apps/redi-talent-pool/src/components/organisms/JobListingCard.tsx @@ -1,9 +1,9 @@ -import { useMediaQuery, useTheme } from '@mui/material' +import { Tooltip, useMediaQuery, useTheme } from '@mui/material' import { CardTags } from '@talent-connect/shared-atomic-design-components' import { topSkillsIdToLabelMap } from '@talent-connect/talent-pool/config' import React from 'react' import { Card } from 'react-bulma-components' -import { NavLink } from 'react-router-dom' +import { Link, NavLink } from 'react-router-dom' import CardLocation from '../../../../../libs/shared-atomic-design-components/src/lib/atoms/CardLocation' import placeholderImage from '../../assets/images/company-placeholder-img.svg' import './JobListingCard.scss' @@ -15,6 +15,7 @@ interface JobListingCardProps { toggleFavorite?: (id: string) => void linkTo?: string timestamp?: string + showPromotedLabel?: boolean renderCTA?: () => React.ReactNode onClick?: (e: React.MouseEvent) => void @@ -25,6 +26,7 @@ export function JobListingCard({ linkTo, onClick, timestamp, + showPromotedLabel = false, renderCTA, }: JobListingCardProps) { const { @@ -71,8 +73,28 @@ export function JobListingCard({
{isTabletOrDesktop && renderCTA && renderCTA()} - {timestamp && ( -
{timestamp}
+ {timestamp && + (!showPromotedLabel || !jobListing.isFromCareerPartner) && ( +
{timestamp}
+ )} + {showPromotedLabel && jobListing.isFromCareerPartner && ( +
+ + This job listing is promoted because it is posted by a + ReDI Career Partner. You can learn more about why we do + this by visiting the{' '} + + FAQ section + + + } + placement="top" + > + Promoted + +
)}
diff --git a/apps/redi-talent-pool/src/components/organisms/jobseeker-profile-editables/JobListingCard.generated.ts b/apps/redi-talent-pool/src/components/organisms/jobseeker-profile-editables/JobListingCard.generated.ts index 22b5686ea..66092ed1f 100644 --- a/apps/redi-talent-pool/src/components/organisms/jobseeker-profile-editables/JobListingCard.generated.ts +++ b/apps/redi-talent-pool/src/components/organisms/jobseeker-profile-editables/JobListingCard.generated.ts @@ -1,7 +1,7 @@ // THIS FILE IS GENERATED, DO NOT EDIT! import * as Types from '@talent-connect/data-access'; -export type JobListingCardJobListingPropFragment = { __typename?: 'TpJobListing', id: string, title?: string | null, idealTechnicalSkills?: Array | null, companyName: string, profileAvatarImageS3Key?: string | null, createdAt: any, federalState?: Types.FederalState | null, location?: string | null, isRemotePossible?: boolean | null }; +export type JobListingCardJobListingPropFragment = { __typename?: 'TpJobListing', id: string, title?: string | null, idealTechnicalSkills?: Array | null, companyName: string, profileAvatarImageS3Key?: string | null, createdAt: any, federalState?: Types.FederalState | null, location?: string | null, isRemotePossible?: boolean | null, isFromCareerPartner: boolean }; export const JobListingCardJobListingPropFragmentDoc = ` fragment JobListingCardJobListingProp on TpJobListing { @@ -14,5 +14,6 @@ export const JobListingCardJobListingPropFragmentDoc = ` federalState location isRemotePossible + isFromCareerPartner } `; \ No newline at end of file diff --git a/apps/redi-talent-pool/src/components/organisms/jobseeker-profile-editables/JobListingCard.graphql b/apps/redi-talent-pool/src/components/organisms/jobseeker-profile-editables/JobListingCard.graphql index 5e8c31433..bf1d71cb0 100644 --- a/apps/redi-talent-pool/src/components/organisms/jobseeker-profile-editables/JobListingCard.graphql +++ b/apps/redi-talent-pool/src/components/organisms/jobseeker-profile-editables/JobListingCard.graphql @@ -8,4 +8,5 @@ fragment JobListingCardJobListingProp on TpJobListing { federalState location isRemotePossible + isFromCareerPartner } diff --git a/apps/redi-talent-pool/src/pages/app/browse/BrowseJobseeker.tsx b/apps/redi-talent-pool/src/pages/app/browse/BrowseJobseeker.tsx index b63373fc1..76a47f790 100644 --- a/apps/redi-talent-pool/src/pages/app/browse/BrowseJobseeker.tsx +++ b/apps/redi-talent-pool/src/pages/app/browse/BrowseJobseeker.tsx @@ -19,6 +19,7 @@ import { employmentTypes, employmentTypesIdToLabelMap, germanFederalStates, + jobListingCreatedDate, topSkills, topSkillsIdToLabelMap, } from '@talent-connect/talent-pool/config' @@ -31,6 +32,7 @@ import { Redirect } from 'react-router-dom' import { ArrayParam, BooleanParam, + StringParam, useQueryParams, withDefault, } from 'use-query-params' @@ -61,6 +63,7 @@ export function BrowseJobseeker() { federalStates: withDefault(ArrayParam, []), onlyFavorites: withDefault(BooleanParam, undefined), isRemotePossible: withDefault(BooleanParam, undefined), + datePosted: withDefault(StringParam, undefined), /** * Job Fair Boolean Field(s) * Uncomment & Rename (joins{Location}{Year}{Season}JobFair) the next field when there's an upcoming Job Fair @@ -74,6 +77,7 @@ export function BrowseJobseeker() { const federalStates = query.federalStates as FederalState[] const onlyFavorites = query.onlyFavorites const isRemotePossible = query.isRemotePossible + const datePosted = query.datePosted /** * Job Fair Boolean Field(s) * Uncomment & Rename (joins{Location}{Year}{Season}JobFair) the next field when there's an upcoming Job Fair @@ -88,6 +92,7 @@ export function BrowseJobseeker() { employmentTypes: employmentType, federalStates, isRemotePossible, + datePosted, /** * Job Fair Boolean Field(s) * Uncomment & Rename (joins{Location}{Year}{Season}JobFair) the next field when there's an upcoming Job Fair @@ -102,8 +107,9 @@ export function BrowseJobseeker() { * - Backend currently supports only sorting by one field, which is used for sorting by date * - All fetch job listing queries are using one findAll query, which means this sort would have unexpected side effects */ - const jobListings = - jobListingsQuery.data?.tpJobListings.sort(careerPartnerSortFn) + const jobListings = !datePosted + ? jobListingsQuery.data?.tpJobListings.sort(careerPartnerSortFn) + : jobListingsQuery.data?.tpJobListings const isFavorite = (jobListingId) => favouritedTpJobListingsQuery.data?.tpJobseekerFavoritedJobListings?.some( @@ -155,6 +161,13 @@ export function BrowseJobseeker() { })) } + const onSelectDatePosted = (item) => { + setQuery((latestQuery) => ({ + ...latestQuery, + datePosted: item ?? undefined, + })) + } + const toggleFilters = (filtersArr, filterName, item) => { const newFilters = toggleValueInArray(filtersArr, item) setQuery((latestQuery) => ({ ...latestQuery, [filterName]: newFilters })) @@ -180,6 +193,7 @@ export function BrowseJobseeker() { employmentType: [], federalStates: [], isRemotePossible: undefined, + datePosted: undefined, /** * Job Fair Boolean Field(s) * Uncomment & Rename (joins{Location}{Year}{Season}JobFair) the next field when there's an upcoming Job Fair @@ -194,7 +208,8 @@ export function BrowseJobseeker() { idealTechnicalSkills.length !== 0 || employmentType.length !== 0 || federalStates.length !== 0 || - isRemotePossible + isRemotePossible || + datePosted /** * Job Fair Boolean Field(s) * Uncomment & Rename (joins{Location}{Year}{Season}JobFair) the next field when there's an upcoming Job Fair @@ -301,7 +316,15 @@ export function BrowseJobseeker() { } /> -
+
+ +
@@ -400,6 +423,17 @@ export function BrowseJobseeker() { onClickHandler={toggleRemoteAvailableFilter} /> )} + {datePosted && ( + item.value === datePosted) + ?.label + } + onClickHandler={() => onSelectDatePosted('')} + /> + )} {/* * Job Fair Boolean Field(s) * Uncomment & Rename (joins{Location}{Year}{Season}JobFair) the next FilterTag when there's an upcoming Job Fair @@ -443,6 +477,7 @@ export function BrowseJobseeker() { addSuffix: true, } )}`} + showPromotedLabel /> ))} @@ -498,3 +533,12 @@ const germanFederalStatesOptions = objectEntries(germanFederalStates).map( label, }) ) + +// Adding the 'Any time' option to the date posted filter as undefined cannot be used as a key +const datePostedOptions = [ + { value: null, label: 'Any time' }, + ...objectEntries(jobListingCreatedDate).map(([value, label]) => ({ + value, + label, + })), +] diff --git a/apps/redi-talent-pool/src/services/api/api.tsx b/apps/redi-talent-pool/src/services/api/api.tsx index 8771899a3..4c6a58229 100644 --- a/apps/redi-talent-pool/src/services/api/api.tsx +++ b/apps/redi-talent-pool/src/services/api/api.tsx @@ -324,6 +324,7 @@ export interface TpJobListingFilters { employmentType: string[] federalStates: string[] isRemotePossible: boolean + datePosted?: string } export async function fetchAllTpJobListingsUsingFilters({ @@ -332,6 +333,7 @@ export async function fetchAllTpJobListingsUsingFilters({ employmentType, federalStates, isRemotePossible, + datePosted, }: TpJobListingFilters): Promise> { const filterRelatedPositions = relatedPositions && relatedPositions.length !== 0 @@ -365,6 +367,7 @@ export async function fetchAllTpJobListingsUsingFilters({ employmentType: filterEmploymentTypeOptions, federalState: filterFederalStates, isRemotePossible, + createdAt: datePosted, }, ], }, diff --git a/apps/redi-talent-pool/src/utils/sort-job-listings.ts b/apps/redi-talent-pool/src/utils/sort-job-listings.ts index f8914653e..c74f7f6d0 100644 --- a/apps/redi-talent-pool/src/utils/sort-job-listings.ts +++ b/apps/redi-talent-pool/src/utils/sort-job-listings.ts @@ -1,6 +1,8 @@ import { TpJobListing } from '@talent-connect/data-access' export const careerPartnerSortFn = (a: TpJobListing, b: TpJobListing) => { + console.log('sorting') + if (a.isFromCareerPartner && !b.isFromCareerPartner) { return -1 } else if (!a.isFromCareerPartner && b.isFromCareerPartner) { diff --git a/libs/data-access/src/lib/types/types.ts b/libs/data-access/src/lib/types/types.ts index 1d8eba5f3..c9767b9f2 100644 --- a/libs/data-access/src/lib/types/types.ts +++ b/libs/data-access/src/lib/types/types.ts @@ -239,6 +239,7 @@ export enum FederalState { } export type FindAllVisibleTpJobListingsArgsFilter = { + datePosted?: InputMaybe; employmentTypes?: InputMaybe>; federalStates?: InputMaybe>; isRemotePossible?: InputMaybe; diff --git a/libs/shared-atomic-design-components/src/lib/molecules/FilterDropdown.scss b/libs/shared-atomic-design-components/src/lib/molecules/FilterDropdown.scss index 0505a8c71..80ada0438 100644 --- a/libs/shared-atomic-design-components/src/lib/molecules/FilterDropdown.scss +++ b/libs/shared-atomic-design-components/src/lib/molecules/FilterDropdown.scss @@ -44,7 +44,7 @@ box-shadow: 0 3px 15px 0 rgba(0, 0, 0, 0.12), 0 2px 4px -1px rgba(0, 0, 0, 0.2); max-height: 210px; - overflow: scroll; + overflow: auto; &--show { display: block; @@ -57,6 +57,11 @@ + li { margin-top: 0.5rem; } + + .listItem--singleSelect { + display: flex; + cursor: pointer; + } } } } diff --git a/libs/shared-atomic-design-components/src/lib/molecules/FilterDropdown.tsx b/libs/shared-atomic-design-components/src/lib/molecules/FilterDropdown.tsx index e04b76c90..59c3b75bf 100644 --- a/libs/shared-atomic-design-components/src/lib/molecules/FilterDropdown.tsx +++ b/libs/shared-atomic-design-components/src/lib/molecules/FilterDropdown.tsx @@ -8,9 +8,10 @@ type Item = { label: string; value: string } interface Props { label: string className: string - selected: string[] + selected?: string[] items: Item[] onChange: (item: string) => void + singleSelect?: boolean } const baseClass = 'filter-dropdown' @@ -21,6 +22,7 @@ const FilterDropdown = ({ selected, items, onChange, + singleSelect = false, }: Props) => { const filterDropdown = useRef(null) const [showDropdown, setShowDropdown] = useState(false) @@ -31,6 +33,11 @@ const FilterDropdown = ({ } }, []) + const handleSingleSelectOptionClick = (item: string) => { + onChange(item) + setShowDropdown(false) + } + useEffect(() => { document.addEventListener('click', handleClick) return () => { @@ -66,12 +73,22 @@ const FilterDropdown = ({ > {items.map((item) => (
  • - onChange(item.value)} - checked={selected.includes(item.value)} - > - {item.label} - + {singleSelect ? ( + handleSingleSelectOptionClick(item.value)} + > + {item.label} + + ) : ( + onChange(item.value)} + checked={selected.includes(item.value)} + > + {item.label} + + )}
  • ))} diff --git a/libs/talent-pool/config/src/lib/talent-pool-config.ts b/libs/talent-pool/config/src/lib/talent-pool-config.ts index 964024413..4f9b90161 100644 --- a/libs/talent-pool/config/src/lib/talent-pool-config.ts +++ b/libs/talent-pool/config/src/lib/talent-pool-config.ts @@ -749,3 +749,16 @@ export const germanFederalStates = { THUERINGEN: 'Thüringen', OUTSIDE_GERMANY: 'Outside Germany', } as const + +export const jobListingCreatedDate = { + PAST_24_HOURS: 'Past 24 hours', + PAST_WEEK: 'Past Week', + PAST_MONTH: 'Past Month', +} as const + +export const jobListingCreatedDateInAge = { + ANY_TIME: 0, + PAST_24_HOURS: 1, + PAST_WEEK: 7, + PAST_MONTH: 30, +} as const diff --git a/schema.graphql b/schema.graphql index e609bb336..4897873ce 100644 --- a/schema.graphql +++ b/schema.graphql @@ -217,6 +217,7 @@ enum FederalState { } input FindAllVisibleTpJobListingsArgsFilter { + datePosted: String employmentTypes: [TpEmploymentType!] federalStates: [FederalState!] isRemotePossible: Boolean