From 7d77755fafb9b1cb090dcec1350e62afa5c99b7f Mon Sep 17 00:00:00 2001 From: helloanil Date: Thu, 10 Nov 2022 10:38:54 +0100 Subject: [PATCH 1/3] Job Postings Expiration Date --- apps/admin-panel/src/App.jsx | 6 ++ apps/api/common/models/tp-job-listing.js | 6 ++ apps/api/common/models/tp-job-listing.json | 3 + .../EditableJobPostings.tsx | 89 +++++++++++++++++-- .../src/pages/app/job-listing/JobListing.tsx | 6 ++ .../react-query/use-tpjoblisting-all-query.ts | 14 +++ .../use-tpjoblisting-create-mutation.ts | 1 + .../use-tpjoblisting-delete-mutation.ts | 2 + .../use-tpjoblisting-update-mutation.ts | 2 + .../redi-talent-pool/src/services/api/api.tsx | 33 +++++++ libs/shared-types/src/lib/TpJobListing.ts | 1 + 11 files changed, 154 insertions(+), 9 deletions(-) diff --git a/apps/admin-panel/src/App.jsx b/apps/admin-panel/src/App.jsx index 572020276..3a13c6d6d 100644 --- a/apps/admin-panel/src/App.jsx +++ b/apps/admin-panel/src/App.jsx @@ -1859,6 +1859,7 @@ const TpCompanyProfileShow = (props) => ( + @@ -1938,6 +1939,7 @@ const TpCompanyProfileEdit = (props) => ( + @@ -1995,6 +1997,7 @@ function tpJobListingListExporter(jobListings, fetchRelatedRecords) { employmentType, languageRequirements, salaryRange, + expiresAt, } = job return { @@ -2004,6 +2007,7 @@ function tpJobListingListExporter(jobListings, fetchRelatedRecords) { employmentType, languageRequirements, salaryRange, + expiresAt, } }) @@ -2049,6 +2053,7 @@ const TpJobListingShow = (props) => ( + ) @@ -2078,6 +2083,7 @@ const TpJobListingEdit = (props) => ( + ) diff --git a/apps/api/common/models/tp-job-listing.js b/apps/api/common/models/tp-job-listing.js index c02e07d12..ec4a4be8b 100644 --- a/apps/api/common/models/tp-job-listing.js +++ b/apps/api/common/models/tp-job-listing.js @@ -8,10 +8,16 @@ const app = require('../../server/server') module.exports = function (TpJobListing) { TpJobListing.observe('before save', function updateTimestamp(ctx, next) { const currentDate = new Date() + const expiryDate = new Date() + // MVP expiry date defaults to 30 days in the future + expiryDate.setDate(expiryDate.getDate() + 30) if (ctx.instance) { if (ctx.isNewInstance) { ctx.instance.createdAt = currentDate + if (ctx.instance.expiresAt === null){ + ctx.instance.expiresAt = expiryDate + } } ctx.instance.updatedAt = new Date() } else { diff --git a/apps/api/common/models/tp-job-listing.json b/apps/api/common/models/tp-job-listing.json index b56fb0fd8..7e5a0420f 100644 --- a/apps/api/common/models/tp-job-listing.json +++ b/apps/api/common/models/tp-job-listing.json @@ -9,6 +9,9 @@ "properties": { "createdAt": { "type": "date" + }, + "expiresAt": { + "type": "date" } }, "validations": [], diff --git a/apps/redi-talent-pool/src/components/organisms/company-profile-editables/EditableJobPostings.tsx b/apps/redi-talent-pool/src/components/organisms/company-profile-editables/EditableJobPostings.tsx index 658249173..8ff325a23 100644 --- a/apps/redi-talent-pool/src/components/organisms/company-profile-editables/EditableJobPostings.tsx +++ b/apps/redi-talent-pool/src/components/organisms/company-profile-editables/EditableJobPostings.tsx @@ -7,8 +7,10 @@ import { Icon, Modal, Checkbox, + FormDatePicker, } from '@talent-connect/shared-atomic-design-components' -import { TpJobListing, TpJobseekerProfile } from '@talent-connect/shared-types' +import { addMonths, subYears } from 'date-fns' +import { TpJobListing } from '@talent-connect/shared-types' import { desiredPositions, employmentTypes, @@ -20,7 +22,11 @@ import React, { useCallback, useState, useEffect } from 'react' import { Columns, Element } from 'react-bulma-components' import * as Yup from 'yup' import { useTpCompanyProfileQuery } from '../../../react-query/use-tpcompanyprofile-query' -import { useTpJobListingAllQuery } from '../../../react-query/use-tpjoblisting-all-query' +import { + useTpJobActiveListingQuery, + useTpJobListingAllQuery, +} from '../../../react-query/use-tpjoblisting-all-query' +import { useTpJobExpiredListingQuery } from '../../../react-query/use-tpjoblisting-all-query' import { useTpJobListingCreateMutation } from '../../../react-query/use-tpjoblisting-create-mutation' import { useTpJobListingDeleteMutation } from '../../../react-query/use-tpjoblisting-delete-mutation' import { useTpJobListingOneOfCurrentUserQuery } from '../../../react-query/use-tpjoblisting-one-query' @@ -28,25 +34,29 @@ import { useTpJobListingUpdateMutation } from '../../../react-query/use-tpjoblis import { EmptySectionPlaceholder } from '../../molecules/EmptySectionPlaceholder' import { JobListingCard } from '../JobListingCard' import JobPlaceholderCardUrl from './job-placeholder-card.svg' -import { get } from 'lodash' + import { objectEntries } from '@talent-connect/typescript-utilities' export function EditableJobPostings({ isJobPostingFormOpen, setIsJobPostingFormOpen, }) { - const { data: jobListings } = useTpJobListingAllQuery() + const { data: jobListings } = useTpJobActiveListingQuery() + const { data: expiredJobListings } = useTpJobExpiredListingQuery() const [isEditing, setIsEditing] = useState(false) const [idOfTpJobListingBeingEdited, setIdOfTpJobListingBeingEdited] = useState(null) // null = "new" const hasJobListings = jobListings?.length > 0 + const hasExpiredJobListings = expiredJobListings?.length > 0 const isEmpty = !hasJobListings + const isExpiredJobsEmpty = !hasExpiredJobListings const startAdding = useCallback(() => { setIdOfTpJobListingBeingEdited(null) // means "new" setIsEditing(true) }, []) + const startEditing = useCallback((id: string) => { setIdOfTpJobListingBeingEdited(id) setIsEditing(true) @@ -75,7 +85,7 @@ export function EditableJobPostings({ className="is-flex-grow-1" style={{ flexGrow: 1 }} > - Job postings + Active job postings
@@ -127,6 +137,45 @@ export function EditableJobPostings({ )}
+
+ + Expired job postings + +
+
+ {isExpiredJobsEmpty ? ( +
+ ) : ( + + {expiredJobListings?.map((jobListing) => ( + + startEditing(jobListing.id)} + /> + + ))} + + )} +
) => { + const onSubmit = (values: Partial) => { if (tpJobListingId === null) { // create new formik.setSubmitting(true) @@ -203,8 +256,13 @@ function ModalForm({ } } + const initialValues: Partial = { + ...jobListing, + expiresAt: jobListing?.expiresAt ? new Date(jobListing.expiresAt) : null, + } + const formik = useFormik({ - initialValues: jobListing, + initialValues, onSubmit, validationSchema, enableReinitialize: true, @@ -223,7 +281,7 @@ function ModalForm({ } }, [deleteMutation, setIsEditing, tpJobListingId]) - if (!formik.values) return null + if (!formik.values || isLoadingJobListing) return null return ( +
diff --git a/apps/redi-talent-pool/src/pages/app/job-listing/JobListing.tsx b/apps/redi-talent-pool/src/pages/app/job-listing/JobListing.tsx index a5ad9cdd6..e858df60f 100644 --- a/apps/redi-talent-pool/src/pages/app/job-listing/JobListing.tsx +++ b/apps/redi-talent-pool/src/pages/app/job-listing/JobListing.tsx @@ -170,6 +170,12 @@ export function JobListing() { {jobListing?.salaryRange ? jobListing.salaryRange : 'N/A'}
+
+ Expiry Date + + {jobListing?.expiresAt ? jobListing.expiresAt : 'N/A'} + +
diff --git a/apps/redi-talent-pool/src/react-query/use-tpjoblisting-all-query.ts b/apps/redi-talent-pool/src/react-query/use-tpjoblisting-all-query.ts index 752b19965..552157f7e 100644 --- a/apps/redi-talent-pool/src/react-query/use-tpjoblisting-all-query.ts +++ b/apps/redi-talent-pool/src/react-query/use-tpjoblisting-all-query.ts @@ -3,6 +3,8 @@ import { fetchAllTpJobListings, fetchAllTpJobListingsUsingFilters, TpJobListingFilters, + fetchExpiredTpJobListings, + fetchActiveTpJobListings, } from '../services/api/api' export function useTpJobListingAllQuery() { @@ -12,6 +14,18 @@ export function useTpJobListingAllQuery() { }) } +export function useTpJobExpiredListingQuery() { + return useQuery('expiredTpJobListings', fetchExpiredTpJobListings, { + refetchOnWindowFocus: false, + }) +} + +export function useTpJobActiveListingQuery() { + return useQuery('activeTpJobListings', fetchActiveTpJobListings, { + refetchOnWindowFocus: false, + }) +} + export function useBrowseTpJobListingsQuery(filters: TpJobListingFilters) { return useQuery(['browseTpJobListings', filters], () => fetchAllTpJobListingsUsingFilters(filters) diff --git a/apps/redi-talent-pool/src/react-query/use-tpjoblisting-create-mutation.ts b/apps/redi-talent-pool/src/react-query/use-tpjoblisting-create-mutation.ts index 325172245..a82d21591 100644 --- a/apps/redi-talent-pool/src/react-query/use-tpjoblisting-create-mutation.ts +++ b/apps/redi-talent-pool/src/react-query/use-tpjoblisting-create-mutation.ts @@ -7,6 +7,7 @@ export function useTpJobListingCreateMutation() { return useMutation(createCurrentUserTpJobListing, { onSuccess: (data) => { queryClient.invalidateQueries(['allTpJobListings']) + queryClient.invalidateQueries(['activeTpJobListings']) }, }) } diff --git a/apps/redi-talent-pool/src/react-query/use-tpjoblisting-delete-mutation.ts b/apps/redi-talent-pool/src/react-query/use-tpjoblisting-delete-mutation.ts index 0cc6f940f..d0b3e1c26 100644 --- a/apps/redi-talent-pool/src/react-query/use-tpjoblisting-delete-mutation.ts +++ b/apps/redi-talent-pool/src/react-query/use-tpjoblisting-delete-mutation.ts @@ -7,6 +7,8 @@ export function useTpJobListingDeleteMutation() { return useMutation(deleteCurrentUserTpJobListing, { onSuccess: (data) => { queryClient.invalidateQueries(['allTpJobListings']) + queryClient.invalidateQueries(['expiredTpJobListings']) + queryClient.invalidateQueries(['activeTpJobListings']) queryClient.invalidateQueries(['oneTpJobListing', data.id]) }, }) diff --git a/apps/redi-talent-pool/src/react-query/use-tpjoblisting-update-mutation.ts b/apps/redi-talent-pool/src/react-query/use-tpjoblisting-update-mutation.ts index 062e64d07..194ddb84f 100644 --- a/apps/redi-talent-pool/src/react-query/use-tpjoblisting-update-mutation.ts +++ b/apps/redi-talent-pool/src/react-query/use-tpjoblisting-update-mutation.ts @@ -7,6 +7,8 @@ export function useTpJobListingUpdateMutation(id: string) { return useMutation(updateCurrentUserTpJobListing, { onSuccess: (data) => { queryClient.invalidateQueries(['allTpJobListings']) + queryClient.invalidateQueries(['activeTpJobListings']) + queryClient.invalidateQueries(['expiredTpJobListings']) queryClient.invalidateQueries(['oneTpJobListing', data.id]) }, }) diff --git a/apps/redi-talent-pool/src/services/api/api.tsx b/apps/redi-talent-pool/src/services/api/api.tsx index 30ce1ce5c..62b892bf8 100644 --- a/apps/redi-talent-pool/src/services/api/api.tsx +++ b/apps/redi-talent-pool/src/services/api/api.tsx @@ -308,6 +308,8 @@ export async function fetchAllTpJobListingsUsingFilters({ const filterFederalStates = federalStates?.length !== 0 ? { inq: federalStates } : undefined + + const currentDate = new Date() return http( `${API_URL}/tpJobListings?filter=${JSON.stringify({ @@ -316,6 +318,10 @@ export async function fetchAllTpJobListingsUsingFilters({ // like: 'Carlotta3', // options: 'i', // }, + or: [ + {expiresAt:{gt:currentDate}}, + {$exists: false} + ], and: [ { relatesToPositions: filterRelatedPositions, @@ -340,6 +346,7 @@ export async function fetchAllTpJobListingsUsingFilters({ export async function fetchAllTpJobListings(): Promise> { const userId = getAccessTokenFromLocalStorage().userId + const currentDate = new Date() const resp = await http(`${API_URL}/redUsers/${userId}/tpJobListings`) // TODO: remove the `.filter()`. It @@ -349,6 +356,32 @@ export async function fetchAllTpJobListings(): Promise> { return resp.data.filter((listing) => !listing.dummy) } +export async function fetchExpiredTpJobListings(): Promise> { + const userId = getAccessTokenFromLocalStorage().userId + const currentDate = new Date() + const resp = await http(`${API_URL}/redUsers/${userId}/tpJobListings?filter=${JSON.stringify({ + where:{ + expiresAt:{lt:currentDate} + } + })}`) + + return resp.data +} + +export async function fetchActiveTpJobListings(): Promise> { + const userId = getAccessTokenFromLocalStorage().userId + const currentDate = new Date() + const resp = await http(`${API_URL}/redUsers/${userId}/tpJobListings?filter=${JSON.stringify({ + where:{ + or: [ + {expiresAt:{gt:currentDate}}, + {$exists: false}, + ], + } + })}`) + return resp.data +} + export async function fetchOneTpJobListingOfCurrentUser( id: string ): Promise { diff --git a/libs/shared-types/src/lib/TpJobListing.ts b/libs/shared-types/src/lib/TpJobListing.ts index fc63dd402..ef68bc9bb 100644 --- a/libs/shared-types/src/lib/TpJobListing.ts +++ b/libs/shared-types/src/lib/TpJobListing.ts @@ -16,5 +16,6 @@ export type TpJobListing = { tpCompanyProfile?: TpCompanyProfile createdAt: Date + expiresAt?: Date updatedAt: Date } From 039fcd2a048b6561a38c016453e4c30ee38e1546 Mon Sep 17 00:00:00 2001 From: helloanil Date: Sun, 25 Dec 2022 17:29:43 +0100 Subject: [PATCH 2/3] Fix failing backend endpoints for joblistings --- .../redi-talent-pool/src/services/api/api.tsx | 38 +++++++++---------- .../src/lib/atoms/FormDatePicker.tsx | 22 ++++++----- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/apps/redi-talent-pool/src/services/api/api.tsx b/apps/redi-talent-pool/src/services/api/api.tsx index 62b892bf8..e39b75600 100644 --- a/apps/redi-talent-pool/src/services/api/api.tsx +++ b/apps/redi-talent-pool/src/services/api/api.tsx @@ -308,7 +308,7 @@ export async function fetchAllTpJobListingsUsingFilters({ const filterFederalStates = federalStates?.length !== 0 ? { inq: federalStates } : undefined - + const currentDate = new Date() return http( @@ -318,10 +318,7 @@ export async function fetchAllTpJobListingsUsingFilters({ // like: 'Carlotta3', // options: 'i', // }, - or: [ - {expiresAt:{gt:currentDate}}, - {$exists: false} - ], + or: [{ expiresAt: { gt: currentDate } }, { exists: false }], and: [ { relatesToPositions: filterRelatedPositions, @@ -356,14 +353,18 @@ export async function fetchAllTpJobListings(): Promise> { return resp.data.filter((listing) => !listing.dummy) } -export async function fetchExpiredTpJobListings(): Promise> { +export async function fetchExpiredTpJobListings(): Promise< + Array +> { const userId = getAccessTokenFromLocalStorage().userId const currentDate = new Date() - const resp = await http(`${API_URL}/redUsers/${userId}/tpJobListings?filter=${JSON.stringify({ - where:{ - expiresAt:{lt:currentDate} - } - })}`) + const resp = await http( + `${API_URL}/redUsers/${userId}/tpJobListings?filter=${JSON.stringify({ + where: { + expiresAt: { lt: currentDate }, + }, + })}` + ) return resp.data } @@ -371,14 +372,13 @@ export async function fetchExpiredTpJobListings(): Promise> export async function fetchActiveTpJobListings(): Promise> { const userId = getAccessTokenFromLocalStorage().userId const currentDate = new Date() - const resp = await http(`${API_URL}/redUsers/${userId}/tpJobListings?filter=${JSON.stringify({ - where:{ - or: [ - {expiresAt:{gt:currentDate}}, - {$exists: false}, - ], - } - })}`) + const resp = await http( + `${API_URL}/redUsers/${userId}/tpJobListings?filter=${JSON.stringify({ + where: { + or: [{ expiresAt: { gt: currentDate } }, { exists: false }], + }, + })}` + ) return resp.data } diff --git a/libs/shared-atomic-design-components/src/lib/atoms/FormDatePicker.tsx b/libs/shared-atomic-design-components/src/lib/atoms/FormDatePicker.tsx index 59ad9d3ad..f9f897d43 100644 --- a/libs/shared-atomic-design-components/src/lib/atoms/FormDatePicker.tsx +++ b/libs/shared-atomic-design-components/src/lib/atoms/FormDatePicker.tsx @@ -12,15 +12,19 @@ interface PickerTriggerProps { onClick?: () => void } -const PickerTrigger = (placeholder: string) => ({ - value, - onClick, -}: PickerTriggerProps) => ( -
- - -
-) +const PickerTrigger = + (placeholder: string) => + ({ value, onClick }: PickerTriggerProps) => + ( +
+ + +
+ ) interface FormDatePickerProps { name: string From 69c56e8db1fd27e284370b0354765555fdab5f42 Mon Sep 17 00:00:00 2001 From: helloanil Date: Sun, 26 Feb 2023 21:25:53 +0100 Subject: [PATCH 3/3] Filter all joblistings by expiresAt at profile page --- .../EditableJobPostingForm.tsx | 322 +++++++++++++++ .../EditableJobPostings.tsx | 369 ++---------------- .../react-query/use-tpjoblisting-all-query.ts | 14 - .../redi-talent-pool/src/services/api/api.tsx | 29 -- 4 files changed, 353 insertions(+), 381 deletions(-) create mode 100644 apps/redi-talent-pool/src/components/organisms/company-profile-editables/EditableJobPostingForm.tsx diff --git a/apps/redi-talent-pool/src/components/organisms/company-profile-editables/EditableJobPostingForm.tsx b/apps/redi-talent-pool/src/components/organisms/company-profile-editables/EditableJobPostingForm.tsx new file mode 100644 index 000000000..0ce1de8a9 --- /dev/null +++ b/apps/redi-talent-pool/src/components/organisms/company-profile-editables/EditableJobPostingForm.tsx @@ -0,0 +1,322 @@ +import { addMonths, subYears } from 'date-fns' +import { Element } from 'react-bulma-components' +import { useCallback } from 'react' +import { useFormik } from 'formik' +import * as Yup from 'yup' + +import { + Button, + Checkbox, + FormDatePicker, + FormInput, + FormSelect, + Heading, + Modal, + TextEditor, +} from '@talent-connect/shared-atomic-design-components' +import { TpJobListing, TpJobseekerProfile } from '@talent-connect/shared-types' +import { + desiredPositions, + employmentTypes, + germanFederalStates, + topSkills, +} from '@talent-connect/talent-pool/config' +import { objectEntries } from '@talent-connect/typescript-utilities' + +import { useTpCompanyProfileQuery } from '../../../react-query/use-tpcompanyprofile-query' +import { useTpJobListingCreateMutation } from '../../../react-query/use-tpjoblisting-create-mutation' +import { useTpJobListingDeleteMutation } from '../../../react-query/use-tpjoblisting-delete-mutation' +import { useTpJobListingOneOfCurrentUserQuery } from '../../../react-query/use-tpjoblisting-one-query' +import { useTpJobListingUpdateMutation } from '../../../react-query/use-tpjoblisting-update-mutation' + +interface ModalFormProps { + tpJobListingId: string + isEditing: boolean + setIsEditing: (boolean) => void +} + +export function EditableJobPostingForm({ + isEditing, + setIsEditing, + tpJobListingId, +}: ModalFormProps) { + const { data, isLoading: isLoadingJobListing } = + useTpJobListingOneOfCurrentUserQuery(tpJobListingId) + + const { data: currentUserTpCompanyProfile } = useTpCompanyProfileQuery() + + const jobListing = tpJobListingId + ? data + : buildBlankJobListing(currentUserTpCompanyProfile?.id) + + const createMutation = useTpJobListingCreateMutation() + const updateMutation = useTpJobListingUpdateMutation(tpJobListingId) + const deleteMutation = useTpJobListingDeleteMutation() + + const onSubmit = (values: Partial, { resetForm }) => { + if (tpJobListingId === null) { + // create new + formik.setSubmitting(true) + createMutation.mutate(values, { + onSettled: () => { + formik.setSubmitting(false) + }, + onSuccess: () => { + setIsEditing(false) + resetForm() + }, + }) + } else { + // update existing + formik.setSubmitting(true) + updateMutation.mutate(values, { + onSettled: () => { + formik.setSubmitting(false) + }, + onSuccess: () => { + setIsEditing(false) + resetForm() + }, + }) + } + } + + const initialValues: Partial = { + ...jobListing, + expiresAt: jobListing?.expiresAt ? new Date(jobListing.expiresAt) : null, + } + + const formik = useFormik({ + initialValues, + onSubmit, + validationSchema, + enableReinitialize: true, + }) + + const handleDelete = useCallback(() => { + if ( + window.confirm('Are you certain you wish to delete this job posting?') + ) { + deleteMutation.mutate(tpJobListingId, { + onSuccess: () => { + setIsEditing(false) + }, + }) + setIsEditing(false) + } + }, [deleteMutation, setIsEditing, tpJobListingId]) + + if (!formik.values || isLoadingJobListing) return null + + return ( + + {formik.values && ( + + + Publish job postings on Talent Pool + + + Job Posting + + + Add the job postings you want to publish to jobseekers at ReDI + School. + + + + + + Remote working is possible for this job listing + + + + We use a standardised list of skills and positions to help with the + matching process of our candidates. Please select the top 6 skills + you think are necessary for succeeding in this job, and up to 3 + position titles that match this job. We will use those to suggest + potential matches. + + + + + + + + +
+ +
+
+ + +
+ {tpJobListingId ? ( + + ) : null} +
+ + )} + + ) +} + +const MIN_CHARS_COUNT = 200 + +const validationSchema = Yup.object().shape({ + title: Yup.string().required('Please provide a job title'), + location: Yup.string().required('Please provide a location'), + summary: Yup.string() + .required('Please provide job description') + .min(MIN_CHARS_COUNT), + relatesToPositions: Yup.array().min( + 1, + 'Please select at least one related position' + ), + idealTechnicalSkills: Yup.array() + .min(1, 'Please select at least one relevant technical skill') + .max(6, 'Please select up to six skills'), + employmentType: Yup.mixed().required('Please select an employment type'), + languageRequirements: Yup.string().required( + 'Please specify the language requirement(s)' + ), + expiresAt: Yup.date().nullable(true).label('Expiry Date'), +}) + +function buildBlankJobListing( + tpCompanyProfileId: string +): Partial { + return { + title: '', + location: '', + summary: '', + relatesToPositions: [], + idealTechnicalSkills: [], + employmentType: '', + languageRequirements: '', + salaryRange: '', + tpCompanyProfileId, + } +} + +const formTopSkills = topSkills.map(({ id, label }) => ({ + value: id, + label, +})) + +const formEmploymentType = employmentTypes.map(({ id, label }) => ({ + value: id, + label, +})) + +const formRelatedPositions = desiredPositions.map(({ id, label }) => ({ + value: id, + label, +})) + +const federalStatesOptions = objectEntries(germanFederalStates).map( + ([value, label]) => ({ + value, + label, + }) +) diff --git a/apps/redi-talent-pool/src/components/organisms/company-profile-editables/EditableJobPostings.tsx b/apps/redi-talent-pool/src/components/organisms/company-profile-editables/EditableJobPostings.tsx index f8804a08e..da3af0c70 100644 --- a/apps/redi-talent-pool/src/components/organisms/company-profile-editables/EditableJobPostings.tsx +++ b/apps/redi-talent-pool/src/components/organisms/company-profile-editables/EditableJobPostings.tsx @@ -1,57 +1,41 @@ -import { - Button, - FormInput, - FormSelect, - Heading, - Icon, - Modal, - Checkbox, - FormDatePicker, - TextEditor, -} from '@talent-connect/shared-atomic-design-components' -import { addMonths, subYears } from 'date-fns' -import { TpJobListing, TpJobseekerProfile } from '@talent-connect/shared-types' -import { - desiredPositions, - employmentTypes, - germanFederalStates, - topSkills, -} from '@talent-connect/talent-pool/config' -import { useFormik } from 'formik' +import { Icon } from '@talent-connect/shared-atomic-design-components' +import { isBefore } from 'date-fns' import { useCallback, useState, useEffect } from 'react' import { Columns, Element } from 'react-bulma-components' -import * as Yup from 'yup' -import { useTpCompanyProfileQuery } from '../../../react-query/use-tpcompanyprofile-query' -import { - useTpJobActiveListingQuery, - useTpJobListingAllQuery, -} from '../../../react-query/use-tpjoblisting-all-query' -import { useTpJobExpiredListingQuery } from '../../../react-query/use-tpjoblisting-all-query' -import { useTpJobListingCreateMutation } from '../../../react-query/use-tpjoblisting-create-mutation' -import { useTpJobListingDeleteMutation } from '../../../react-query/use-tpjoblisting-delete-mutation' -import { useTpJobListingOneOfCurrentUserQuery } from '../../../react-query/use-tpjoblisting-one-query' -import { useTpJobListingUpdateMutation } from '../../../react-query/use-tpjoblisting-update-mutation' + +import { useTpJobListingAllQuery } from '../../../react-query/use-tpjoblisting-all-query' + import { EmptySectionPlaceholder } from '../../molecules/EmptySectionPlaceholder' import { JobListingCard } from '../JobListingCard' +import { EditableJobPostingForm } from './EditableJobPostingForm' import JobPlaceholderCardUrl from './job-placeholder-card.svg' -import { objectEntries } from '@talent-connect/typescript-utilities' - export function EditableJobPostings({ isJobPostingFormOpen, setIsJobPostingFormOpen, }) { - const { data: jobListings } = useTpJobActiveListingQuery() - const { data: expiredJobListings } = useTpJobExpiredListingQuery() + const { data: jobListings } = useTpJobListingAllQuery() + + const activeJobListings = jobListings?.filter( + (jobListing) => + jobListing.expiresAt == null || + isBefore(new Date(), new Date(jobListing.expiresAt)) + ) + + const hasActiveJobListings = activeJobListings?.length > 0 + + const expiredJobListings = jobListings?.filter( + (jobListing) => + jobListing.expiresAt != null && + isBefore(new Date(jobListing.expiresAt), new Date()) + ) + + const hasExpiredJobListings = expiredJobListings?.length > 0 + const [isEditing, setIsEditing] = useState(false) const [idOfTpJobListingBeingEdited, setIdOfTpJobListingBeingEdited] = useState(null) // null = "new" - const hasJobListings = jobListings?.length > 0 - const hasExpiredJobListings = expiredJobListings?.length > 0 - const isEmpty = !hasJobListings - const isExpiredJobsEmpty = !hasExpiredJobListings - const startAdding = useCallback(() => { setIdOfTpJobListingBeingEdited(null) // means "new" setIsEditing(true) @@ -96,9 +80,8 @@ export function EditableJobPostings({
- -
- {isEmpty ? ( +
+ {!hasActiveJobListings ? ( setIsEditing(true)} @@ -130,7 +113,7 @@ export function EditableJobPostings({ ) : ( - {jobListings?.map((jobListing) => ( + {activeJobListings?.map((jobListing) => ( )}
+
+
- {isExpiredJobsEmpty ? ( + {!hasExpiredJobListings ? (
- ) } - -const MIN_CHARS_COUNT = 200 - -const validationSchema = Yup.object().shape({ - title: Yup.string().required('Please provide a job title'), - location: Yup.string().required('Please provide a location'), - summary: Yup.string() - .required('Please provide job description') - .min(MIN_CHARS_COUNT), - relatesToPositions: Yup.array().min( - 1, - 'Please select at least one related position' - ), - idealTechnicalSkills: Yup.array() - .min(1, 'Please select at least one relevant technical skill') - .max(6, 'Please select up to six skills'), - employmentType: Yup.mixed().required('Please select an employment type'), - languageRequirements: Yup.string().required( - 'Please specify the language requirement(s)' - ), - expiresAt: Yup.date().nullable(true).label('Expiry Date'), -}) - -interface ModalFormProps { - tpJobListingId: string - isEditing: boolean - setIsEditing: (boolean) => void -} - -function ModalForm({ - isEditing, - setIsEditing, - tpJobListingId, -}: ModalFormProps) { - const { data, isLoading: isLoadingJobListing } = - useTpJobListingOneOfCurrentUserQuery(tpJobListingId) - - const { data: currentUserTpCompanyProfile } = useTpCompanyProfileQuery() - - const jobListing = tpJobListingId - ? data - : buildBlankJobListing(currentUserTpCompanyProfile?.id) - - const createMutation = useTpJobListingCreateMutation() - const updateMutation = useTpJobListingUpdateMutation(tpJobListingId) - const deleteMutation = useTpJobListingDeleteMutation() - - const onSubmit = (values: Partial, { resetForm }) => { - if (tpJobListingId === null) { - // create new - formik.setSubmitting(true) - createMutation.mutate(values, { - onSettled: () => { - formik.setSubmitting(false) - }, - onSuccess: () => { - setIsEditing(false) - resetForm() - }, - }) - } else { - // update existing - formik.setSubmitting(true) - updateMutation.mutate(values, { - onSettled: () => { - formik.setSubmitting(false) - }, - onSuccess: () => { - setIsEditing(false) - resetForm() - }, - }) - } - } - - const initialValues: Partial = { - ...jobListing, - expiresAt: jobListing?.expiresAt ? new Date(jobListing.expiresAt) : null, - } - - const formik = useFormik({ - initialValues, - onSubmit, - validationSchema, - enableReinitialize: true, - }) - - const handleDelete = useCallback(() => { - if ( - window.confirm('Are you certain you wish to delete this job posting?') - ) { - deleteMutation.mutate(tpJobListingId, { - onSuccess: () => { - setIsEditing(false) - }, - }) - setIsEditing(false) - } - }, [deleteMutation, setIsEditing, tpJobListingId]) - - if (!formik.values || isLoadingJobListing) return null - - return ( - - {formik.values && ( - - - Publish job postings on Talent Pool - - - Job Posting - - - Add the job postings you want to publish to jobseekers at ReDI - School. - - - - - - Remote working is possible for this job listing - - - - We use a standardised list of skills and positions to help with the - matching process of our candidates. Please select the top 6 skills - you think are necessary for succeeding in this job, and up to 3 - position titles that match this job. We will use those to suggest - potential matches. - - - - - - - - -
- -
-
- - -
- {tpJobListingId ? ( - - ) : null} -
- - )} - - ) -} - -function buildBlankJobListing( - tpCompanyProfileId: string -): Partial { - return { - title: '', - location: '', - summary: '', - relatesToPositions: [], - idealTechnicalSkills: [], - employmentType: '', - languageRequirements: '', - salaryRange: '', - tpCompanyProfileId, - } -} - -const formTopSkills = topSkills.map(({ id, label }) => ({ - value: id, - label, -})) - -const formEmploymentType = employmentTypes.map(({ id, label }) => ({ - value: id, - label, -})) - -const formRelatedPositions = desiredPositions.map(({ id, label }) => ({ - value: id, - label, -})) - -const federalStatesOptions = objectEntries(germanFederalStates).map( - ([value, label]) => ({ - value, - label, - }) -) diff --git a/apps/redi-talent-pool/src/react-query/use-tpjoblisting-all-query.ts b/apps/redi-talent-pool/src/react-query/use-tpjoblisting-all-query.ts index 552157f7e..752b19965 100644 --- a/apps/redi-talent-pool/src/react-query/use-tpjoblisting-all-query.ts +++ b/apps/redi-talent-pool/src/react-query/use-tpjoblisting-all-query.ts @@ -3,8 +3,6 @@ import { fetchAllTpJobListings, fetchAllTpJobListingsUsingFilters, TpJobListingFilters, - fetchExpiredTpJobListings, - fetchActiveTpJobListings, } from '../services/api/api' export function useTpJobListingAllQuery() { @@ -14,18 +12,6 @@ export function useTpJobListingAllQuery() { }) } -export function useTpJobExpiredListingQuery() { - return useQuery('expiredTpJobListings', fetchExpiredTpJobListings, { - refetchOnWindowFocus: false, - }) -} - -export function useTpJobActiveListingQuery() { - return useQuery('activeTpJobListings', fetchActiveTpJobListings, { - refetchOnWindowFocus: false, - }) -} - export function useBrowseTpJobListingsQuery(filters: TpJobListingFilters) { return useQuery(['browseTpJobListings', filters], () => fetchAllTpJobListingsUsingFilters(filters) diff --git a/apps/redi-talent-pool/src/services/api/api.tsx b/apps/redi-talent-pool/src/services/api/api.tsx index e92592199..2a7a278ae 100644 --- a/apps/redi-talent-pool/src/services/api/api.tsx +++ b/apps/redi-talent-pool/src/services/api/api.tsx @@ -371,35 +371,6 @@ export async function fetchAllTpJobListings(): Promise> { return resp.data.filter((listing) => !listing.dummy) } -export async function fetchExpiredTpJobListings(): Promise< - Array -> { - const userId = getAccessTokenFromLocalStorage().userId - const currentDate = new Date() - const resp = await http( - `${API_URL}/redUsers/${userId}/tpJobListings?filter=${JSON.stringify({ - where: { - expiresAt: { lt: currentDate }, - }, - })}` - ) - - return resp.data -} - -export async function fetchActiveTpJobListings(): Promise> { - const userId = getAccessTokenFromLocalStorage().userId - const currentDate = new Date() - const resp = await http( - `${API_URL}/redUsers/${userId}/tpJobListings?filter=${JSON.stringify({ - where: { - or: [{ expiresAt: { gt: currentDate } }, { exists: false }], - }, - })}` - ) - return resp.data -} - export async function fetchOneTpJobListingOfCurrentUser( id: string ): Promise {