diff --git a/server/src/http/controllers/v2/jobs.controller.v2.ts b/server/src/http/controllers/v2/jobs.controller.v2.ts index 797912a6ff..33ced44908 100644 --- a/server/src/http/controllers/v2/jobs.controller.v2.ts +++ b/server/src/http/controllers/v2/jobs.controller.v2.ts @@ -108,6 +108,50 @@ export default (server: Server) => { } ) + server.post( + "/v2/_private/jobs/provided/:id", + { + schema: zRoutes.post["/v2/_private/jobs/provided/:id"], + onRequest: server.auth(zRoutes.post["/v2/_private/jobs/provided/:id"]), + config, + }, + async (req, res) => { + const { id } = req.params + const job = await getDbCollection("jobs_partners").findOne({ _id: id }) + if (!job) { + throw badRequest("Job does not exist") + } + + if (job.offer_status === JOB_STATUS_ENGLISH.POURVUE) { + throw badRequest("Job is already provided") + } + await getDbCollection("jobs_partners").findOneAndUpdate({ _id: id }, { $set: { offer_status: JOB_STATUS_ENGLISH.POURVUE } }) + return res.status(200).send({}) + } + ) + + server.post( + "/v2/_private/jobs/canceled/:id", + { + schema: zRoutes.post["/v2/_private/jobs/canceled/:id"], + onRequest: server.auth(zRoutes.post["/v2/_private/jobs/canceled/:id"]), + config, + }, + async (req, res) => { + const { id } = req.params + const job = await getDbCollection("jobs_partners").findOne({ _id: id }) + if (!job) { + throw badRequest("Job does not exists") + } + + if (job.offer_status === JOB_STATUS_ENGLISH.ANNULEE) { + throw badRequest("Job is already canceled") + } + await getDbCollection("jobs_partners").findOneAndUpdate({ _id: id }, { $set: { offer_status: JOB_STATUS_ENGLISH.ANNULEE } }) + return res.status(200).send({}) + } + ) + server.post( "/v2/jobs/extend/:id", { diff --git a/server/src/jobs/recruiters/recruiterOfferExpirationReminderJob.ts b/server/src/jobs/recruiters/recruiterOfferExpirationReminderJob.ts index 5d791dfd18..fde0d8c79f 100644 --- a/server/src/jobs/recruiters/recruiterOfferExpirationReminderJob.ts +++ b/server/src/jobs/recruiters/recruiterOfferExpirationReminderJob.ts @@ -1,6 +1,7 @@ import { internal } from "@hapi/boom" import { groupBy } from "lodash-es" import { ObjectId } from "mongodb" +import { LBA_ITEM_TYPE } from "shared/constants/lbaitem" import { JOB_STATUS } from "shared/models" import { getStaticFilePath } from "@/common/utils/getStaticFilePath" @@ -82,8 +83,8 @@ export const recruiterOfferExpirationReminderJob = async (threshold: number /* n job_type: job.job_type, job_level_label: job.job_level_label, job_start_date: dayjs(job.job_start_date).format("DD/MM/YYYY"), - supprimer: createCancelJobLink(userWithAccountToUserForToken(contactUser), job._id.toString()), - pourvue: createProvidedJobLink(userWithAccountToUserForToken(contactUser), job._id.toString()), + supprimer: createCancelJobLink(userWithAccountToUserForToken(contactUser), job._id.toString(), LBA_ITEM_TYPE.OFFRES_EMPLOI_LBA), + pourvue: createProvidedJobLink(userWithAccountToUserForToken(contactUser), job._id.toString(), LBA_ITEM_TYPE.OFFRES_EMPLOI_LBA), })), threshold, connectionUrl: createAuthMagicLink(userWithAccountToUserForToken(contactUser)), diff --git a/server/src/services/appLinks.service.ts b/server/src/services/appLinks.service.ts index 8581815c13..b2157ba2af 100644 --- a/server/src/services/appLinks.service.ts +++ b/server/src/services/appLinks.service.ts @@ -1,4 +1,5 @@ import { ApplicationIntention } from "shared/constants/application" +import { LBA_ITEM_TYPE } from "shared/constants/lbaitem" import { IJob } from "shared/models" import { IUserWithAccount } from "shared/models/userWithAccount.model" import { zRoutes } from "shared/routes" @@ -95,7 +96,7 @@ export function createCfaUnsubscribeToken(email: string, siret: string) { ) } -export function createCancelJobLink(user: UserForAccessToken, jobId: string, utmData: string | undefined = undefined) { +export function createCancelJobLink(user: UserForAccessToken, jobId: string, jobOrigin: LBA_ITEM_TYPE, utmData: string | undefined = undefined) { const token = generateAccessToken( user, [ @@ -108,16 +109,25 @@ export function createCancelJobLink(user: UserForAccessToken, jobId: string, utm querystring: undefined, }, }), + generateScope({ + schema: zRoutes.post["/v2/_private/jobs/canceled/:id"], + options: { + params: { + id: jobId, + }, + querystring: undefined, + }, + }), ], { expiresIn: "30d", } ) - return `${config.publicUrl}/espace-pro/offre/${jobId}/cancel?${utmData ? utmData : ""}&token=${token}` + return `${config.publicUrl}/espace-pro/offre/${jobOrigin}/${jobId}/cancel?${utmData ? utmData : ""}&token=${token}` } -export function createProvidedJobLink(user: UserForAccessToken, jobId: string, utmData: string | undefined = undefined) { +export function createProvidedJobLink(user: UserForAccessToken, jobId: string, jobOrigin: LBA_ITEM_TYPE, utmData: string | undefined = undefined) { const token = generateAccessToken( user, [ @@ -128,13 +138,20 @@ export function createProvidedJobLink(user: UserForAccessToken, jobId: string, u querystring: undefined, }, }), + generateScope({ + schema: zRoutes.post["/v2/_private/jobs/provided/:id"], + options: { + params: { id: jobId }, + querystring: undefined, + }, + }), ], { expiresIn: "30d", } ) - return `${config.publicUrl}/espace-pro/offre/${jobId}/provided?${utmData ? utmData : ""}&token=${token}` + return `${config.publicUrl}/espace-pro/offre/${jobOrigin}/${jobId}/provided?${utmData ? utmData : ""}&token=${token}` } export function createViewDelegationLink(email: string, establishment_id: string, job_id: string, siret_formateur: string) { diff --git a/server/src/services/application.service.ts b/server/src/services/application.service.ts index d90648352a..3435e9a4e6 100644 --- a/server/src/services/application.service.ts +++ b/server/src/services/application.service.ts @@ -467,12 +467,10 @@ const buildRecruiterEmailUrls = async (application: IApplication, applicant: IAp cancelJobUrl: "", } - if (application.job_id && user) { - urls.jobProvidedUrl = createProvidedJobLink(userForToken, application.job_id, utmRecruiterData) - urls.cancelJobUrl = createCancelJobLink(userForToken, application.job_id, utmRecruiterData) - } if (application.job_id) { urls.jobUrl = `${config.publicUrl}${getDirectJobPath(application.job_origin, application.job_id)}${utmRecruiterData}` + urls.jobProvidedUrl = createProvidedJobLink(userForToken, application.job_id, application.job_origin, utmRecruiterData) + urls.cancelJobUrl = createCancelJobLink(userForToken, application.job_id, application.job_origin, utmRecruiterData) } return urls @@ -1121,7 +1119,7 @@ export const processApplicationEmails = { throw internal("Email entreprise destinataire rejeté.") } }, - // get data from applicant + async sendCandidatEmail(application: IApplication, applicant: IApplicant) { const { job_origin } = application const { url: urlOfDetail, urlWithoutUtm: urlOfDetailNoUtm } = buildUrlsOfDetail(application) diff --git a/server/static/templates/mail-candidature-partenaire.mjml.ejs b/server/static/templates/mail-candidature-partenaire.mjml.ejs index c9110e9948..7060313ab2 100644 --- a/server/static/templates/mail-candidature-partenaire.mjml.ejs +++ b/server/static/templates/mail-candidature-partenaire.mjml.ejs @@ -207,19 +207,19 @@
- + ✅ Indiquer que mon offre est pourvue
- + 📝 Supprimer mon offre
- + 💬 Consulter la FAQ
diff --git a/server/static/templates/mail-candidature-spontanee.mjml.ejs b/server/static/templates/mail-candidature-spontanee.mjml.ejs index 3ac0f2422e..2ddb60599c 100644 --- a/server/static/templates/mail-candidature-spontanee.mjml.ejs +++ b/server/static/templates/mail-candidature-spontanee.mjml.ejs @@ -248,7 +248,7 @@
- 💬 Consulter la FAQ + 💬 Consulter la FAQ
diff --git a/server/static/templates/mail-candidature.mjml.ejs b/server/static/templates/mail-candidature.mjml.ejs index c9110e9948..7060313ab2 100644 --- a/server/static/templates/mail-candidature.mjml.ejs +++ b/server/static/templates/mail-candidature.mjml.ejs @@ -207,19 +207,19 @@
- + ✅ Indiquer que mon offre est pourvue
- + 📝 Supprimer mon offre
- + 💬 Consulter la FAQ
diff --git a/shared/constants/lbaitem.ts b/shared/constants/lbaitem.ts index dde86ab302..2cea34a98f 100644 --- a/shared/constants/lbaitem.ts +++ b/shared/constants/lbaitem.ts @@ -59,7 +59,7 @@ export const newItemTypeToOldItemType = (lbaItemType: LBA_ITEM_TYPE): LBA_ITEM_T case LBA_ITEM_TYPE.RECRUTEURS_LBA: return LBA_ITEM_TYPE_OLD.LBA case LBA_ITEM_TYPE.OFFRES_EMPLOI_PARTENAIRES: - return LBA_ITEM_TYPE_OLD.PE + return LBA_ITEM_TYPE_OLD.PARTNER_JOB case LBA_ITEM_TYPE.FORMATION: throw new Error("not used") default: diff --git a/shared/routes/v2/jobs.routes.v2.ts b/shared/routes/v2/jobs.routes.v2.ts index 0bad64c952..2b47bd3cc9 100644 --- a/shared/routes/v2/jobs.routes.v2.ts +++ b/shared/routes/v2/jobs.routes.v2.ts @@ -180,6 +180,40 @@ export const zJobsRoutesV2 = { }, }, post: { + "/v2/_private/jobs/provided/:id": { + method: "post", + path: "/v2/_private/jobs/provided/:id", + params: z + .object({ + id: zObjectId, + }) + .strict(), + response: { + "200": z.object({}).strict(), + }, + securityScheme: { + auth: "access-token", + access: null, + resources: {}, + }, + }, + "/v2/_private/jobs/canceled/:id": { + method: "post", + path: "/v2/_private/jobs/canceled/:id", + params: z + .object({ + id: zObjectId, + }) + .strict(), + response: { + "200": z.object({}).strict(), + }, + securityScheme: { + auth: "access-token", + access: null, + resources: {}, + }, + }, "/v2/jobs/provided/:id": { method: "post", path: "/v2/jobs/provided/:id", diff --git a/ui/components/ItemDetail/PartnerJobComponents/PartnerJobPostuler.tsx b/ui/components/ItemDetail/PartnerJobComponents/PartnerJobPostuler.tsx index 6651fb57ff..a5625db290 100644 --- a/ui/components/ItemDetail/PartnerJobComponents/PartnerJobPostuler.tsx +++ b/ui/components/ItemDetail/PartnerJobComponents/PartnerJobPostuler.tsx @@ -8,6 +8,8 @@ import CandidatureLba from "../CandidatureLba/CandidatureLba" import CandidatureParTelephone from "../CandidatureParTelephone" export const PartnerJobPostuler = ({ job, isCollapsedHeader }: { job: ILbaItemPartnerJob; isCollapsedHeader: boolean }) => { + // KBA fix enum shared/models/lbaItem.model.ts + if (["Pourvue", "Annulée"].includes(job.job.status)) return null if (job.contact?.email) { return ( diff --git a/ui/next.config.mjs b/ui/next.config.mjs index 224aa31d52..3fbd06d56c 100644 --- a/ui/next.config.mjs +++ b/ui/next.config.mjs @@ -168,6 +168,12 @@ const nextConfig = { destination: "/recherche-formation", permanent: true, }, + // KBA TODO WAR ROOM : to remove on 2025_03_10 : route without jobType will be obsolete + { + source: "/espace-pro/offre/:id/:option", + destination: "/espace-pro/offre/offres_emploi_lba/:id/:option", + permanent: true, + }, ] }, } diff --git a/ui/pages/espace-pro/offre/[jobId]/[option].tsx b/ui/pages/espace-pro/offre/[jobType]/[jobId]/[option].tsx similarity index 74% rename from ui/pages/espace-pro/offre/[jobId]/[option].tsx rename to ui/pages/espace-pro/offre/[jobType]/[jobId]/[option].tsx index 0791588416..cfe683bd98 100644 --- a/ui/pages/espace-pro/offre/[jobId]/[option].tsx +++ b/ui/pages/espace-pro/offre/[jobType]/[jobId]/[option].tsx @@ -7,36 +7,40 @@ import { getDirectJobPath } from "shared/metier/lbaitemutils" import Footer from "@/components/footer" import Navigation from "@/components/navigation" -import { cancelOffre, fillOffre } from "../../../../utils/api" +import { cancelOffre, cancelPartnerJob, fillOffre, providedPartnerJob } from "../../../../../utils/api" export default function MailActionsOnOffre() { const router = useRouter() - const { jobId, option, token } = router.query + const { jobId, option, token, jobType } = router.query as { jobId: string; option: string; token: string; jobType: LBA_ITEM_TYPE } const [result, setResult] = useState("") - const error = () => { - setResult("Une erreur s'est produite. Merci de contacter le support de La bonne alternance") - } - useEffect(() => { - if (jobId && option) { - if (option === "cancel") { - cancelOffre(jobId, token) - .then(() => { - setResult("ok") - }) - .catch(() => error()) - } + if (!jobId || !option || !jobType) return + + const jobActions = { + [LBA_ITEM_TYPE.OFFRES_EMPLOI_LBA]: { + cancel: cancelOffre, + provided: fillOffre, + }, + [LBA_ITEM_TYPE.OFFRES_EMPLOI_PARTENAIRES]: { + cancel: cancelPartnerJob, + provided: providedPartnerJob, + }, + } - if (option === "provided") { - fillOffre(jobId, token) - .then(() => { - setResult("ok") - }) - .catch(() => error()) - } + const action = jobActions[jobType]?.[option] + if (action && typeof action === "function") { + action(jobId, token) + .then(() => setResult("ok")) + .catch((error) => { + console.log(error) + setResult("Une erreur s'est produite. Merci de contacter le support de La bonne alternance") + return + }) + } else { + setResult("Unsupported action.") } - }, [jobId, option]) + }, [jobId, option, jobType]) const cssParameters = { background: "#fff1e5", diff --git a/ui/utils/api.ts b/ui/utils/api.ts index cb58b8534a..ff8d91911d 100644 --- a/ui/utils/api.ts +++ b/ui/utils/api.ts @@ -43,6 +43,10 @@ export const createOffreByToken = (establishment_id: string, newOffre: IJobCreat apiPost("/formulaire/:establishment_id/offre/by-token", { params: { establishment_id }, body: newOffre, headers: { authorization: `Bearer ${token}` } }) export const patchOffreDelegation = (jobId: string, siret: string) => apiPatch(`/formulaire/offre/:jobId/delegation`, { params: { jobId }, querystring: { siret_formateur: siret } }).catch(errorHandler) +// need a function to cancel partner jobs : add the job_origin from the application in the url - refactor ui/pages/espace-pro/offre/[jobId]/[option].tsx needed +export const cancelPartnerJob = (id, token) => apiPost("/v2/_private/jobs/canceled/:id", { params: { id }, headers: { authorization: `Bearer ${token}` } }) +export const providedPartnerJob = (id, token) => apiPost("/v2/_private/jobs/provided/:id", { params: { id }, headers: { authorization: `Bearer ${token}` } }) +// once offres_emploi_lba are definitly stored in jobs partners, we can move this call to /jobs/:jobId/cancel export const cancelOffre = (jobId, token) => apiPut(`/formulaire/offre/:jobId/cancel`, { params: { jobId }, headers: { authorization: `Bearer ${token}` } }) export const cancelOffreFromAdmin = (jobId: string, data: IRoutes["put"]["/formulaire/offre/f/:jobId/cancel"]["body"]["_input"]) => apiPut("/formulaire/offre/f/:jobId/cancel", { params: { jobId }, body: data })