diff --git a/apps/admin-panel/src/App.jsx b/apps/admin-panel/src/App.jsx index 26f845b21..d2f83d218 100644 --- a/apps/admin-panel/src/App.jsx +++ b/apps/admin-panel/src/App.jsx @@ -1866,6 +1866,7 @@ const TpCompanyProfileShow = (props) => ( + @@ -1946,6 +1947,7 @@ const TpCompanyProfileEdit = (props) => ( + @@ -2003,6 +2005,7 @@ function tpJobListingListExporter(jobListings, fetchRelatedRecords) { employmentType, languageRequirements, salaryRange, + expiresAt, } = job return { @@ -2012,6 +2015,7 @@ function tpJobListingListExporter(jobListings, fetchRelatedRecords) { employmentType, languageRequirements, salaryRange, + expiresAt, } }) @@ -2057,6 +2061,7 @@ const TpJobListingShow = (props) => ( + ) @@ -2086,6 +2091,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 511c5d8ff..55eca911e 100644 --- a/apps/api/common/models/tp-job-listing.js +++ b/apps/api/common/models/tp-job-listing.js @@ -14,10 +14,16 @@ const DOMPurify = createDOMPurify(window) 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() ctx.instance.summary = DOMPurify.sanitize(ctx.instance.summary) 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/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 92ec85166..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,51 +1,46 @@ -import { - Button, - FormInput, - FormSelect, - Heading, - Icon, - Modal, - Checkbox, - 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 { 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 { useTpJobListingAllQuery } 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 { 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 } = 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 isEmpty = !hasJobListings - const startAdding = useCallback(() => { setIdOfTpJobListingBeingEdited(null) // means "new" setIsEditing(true) }, []) + const startEditing = useCallback((id: string) => { setIdOfTpJobListingBeingEdited(id) setIsEditing(true) @@ -79,15 +74,14 @@ export function EditableJobPostings({ className="is-flex-grow-1" style={{ flexGrow: 1 }} > - Job postings + Active job postings
- -
- {isEmpty ? ( +
+ {!hasActiveJobListings ? ( setIsEditing(true)} @@ -119,7 +113,7 @@ export function EditableJobPostings({ ) : ( - {jobListings?.map((jobListing) => ( + {activeJobListings?.map((jobListing) => (
- +
+ + Expired job postings + +
+
+ {!hasExpiredJobListings ? ( +
+ ) : ( + + {expiredJobListings?.map((jobListing) => ( + + startEditing(jobListing.id)} + /> + + ))} + + )} +
+
+ ) } - -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)' - ), -}) - -interface ModalFormProps { - tpJobListingId: string - isEditing: boolean - setIsEditing: (boolean) => void -} - -function ModalForm({ - isEditing, - setIsEditing, - tpJobListingId, -}: ModalFormProps) { - const { data } = 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 formik = useFormik({ - initialValues: jobListing, - 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) 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/pages/app/job-listing/JobListing.tsx b/apps/redi-talent-pool/src/pages/app/job-listing/JobListing.tsx index b7f95d350..f0eb3577c 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 @@ -173,6 +173,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-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 a82e0d64b..610cdd72e 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]) queryClient.invalidateQueries(['oneTpJobListingOfCurrentUser', 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 c45d0baf4..6a18b0835 100644 --- a/apps/redi-talent-pool/src/services/api/api.tsx +++ b/apps/redi-talent-pool/src/services/api/api.tsx @@ -327,6 +327,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({ where: { @@ -334,6 +336,7 @@ export async function fetchAllTpJobListingsUsingFilters({ // like: 'Carlotta3', // options: 'i', // }, + or: [{ expiresAt: { gt: currentDate } }, { exists: false }], and: [ { relatesToPositions: filterRelatedPositions, @@ -369,6 +372,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 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 }