<?php
namespace App\Controller;
use App\Entity\Blacklisting;
use App\Entity\CarryThroughData;
use App\Entity\ConversationMessage\ConversationMessage;
use App\Entity\DebuggingInfoBag;
use App\Entity\RecurrentJob;
use App\Entity\SeoTranslationParameters;
use App\Entity\UsageEvent;
use App\Entity\User;
use App\Entity\WantedJob;
use App\Entity\WantedJobsSearch\WantedJobsSearchParameters;
use App\Entity\WantedJobsSearch\WantedJobsSearchResult;
use App\Entity\WantedJobsSearch\WantedJobsSearchResultset;
use App\Event\SearchForWantedJobsStartedEvent;
use App\Event\SearchtermEnteredEvent;
use App\Exception\UncallableSearchPageException;
use App\Exception\UnknownZipcodeException;
use App\Form\WantedJobsSearchParametersType;
use App\Repository\ZipcodeCircumcirclesRepository;
use App\Service\AnonymousUserInfoService;
use App\Service\BlacklistingService;
use App\Service\CircuitbreakerService;
use App\Service\DebuggingService;
use App\Service\FeatureFlagService;
use App\Service\FeatureLimitationsService;
use App\Service\Membership\MembershipService;
use App\Service\NotificationService;
use App\Service\ProfileBlocksService;
use App\Service\RegistrationService;
use App\Service\SessionService;
use App\Service\UsageEventService;
use App\Service\WantedJobsSearchService;
use App\Value\GeneralApplicationSettingsValue;
use App\Value\PossibleAvailabilitiesValue;
use App\Value\ZipcodeRadiusesValue;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use FOS\UserBundle\Form\Factory\FactoryInterface;
use JanusHercules\DatawarehouseIntegration\Domain\Entity\BusinessEvent;
use JanusHercules\DatawarehouseIntegration\Domain\Service\BusinessEventDomainService;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class WantedJobsSearchController extends AbstractController implements DoAdditionalUserChecksControllerInterface
{
use SearchControllerHelper;
public function __construct(
private readonly FeatureFlagService $featureFlagService
) {
}
/** @throws Exception */
public function formAction(
Request $request,
TranslatorInterface $translator,
SessionService $sessionService,
SessionInterface $session,
FeatureLimitationsService $featureLimitationsService,
WantedJobsSearchService $wantedJobsSearchService
): Response {
/** @var User $user */
$user = $this->getUser();
if ($user instanceof User && $user->isJobseeker()) {
$this->addFlash(
'info',
$translator->trans('wanted_jobs_search.alert_jobseekers_not_allowed')
);
return $this->redirectToRoute('recurrent_jobs_search.form');
}
$formPrefillZipcode = '';
if ($user instanceof User && $user->isJobofferer()) {
$formPrefillZipcode = $user->getDefaultJoboffererProfile()->getZipcode() ?? '';
}
$formPrefillOccupationalFieldSearchterm = '';
if ($request->get('occupationalFieldSearchterm')) {
$formPrefillOccupationalFieldSearchterm = $request->get('occupationalFieldSearchterm');
}
$wantedJobsSearchParameters = new WantedJobsSearchParameters(
$formPrefillOccupationalFieldSearchterm,
$formPrefillZipcode,
ZipcodeRadiusesValue::ALL[1],
WantedJob::EXPERIENCE_NONE
);
$sessionService->resetProfileIdsForMultiMessageSelection($session);
$form = $this->createForm(
WantedJobsSearchParametersType::class,
$wantedJobsSearchParameters,
[
'action' => $this->generateUrl('wanted_jobs_search.results') . '#results',
'method' => 'GET'
]
);
$joboffererProfileCanEditZipcodeForWantedJobsSearch = true;
$joboffererProfileId = null;
if (!is_null($user) && !is_null($user->getDefaultJoboffererProfile())) {
$joboffererProfileCanEditZipcodeForWantedJobsSearch = $featureLimitationsService->joboffererProfileCanEditZipcodeForWantedJobsSearch($user->getDefaultJoboffererProfile());
$joboffererProfileId = $user->getDefaultJoboffererProfile()->getId();
}
$response = $this->render('/wanted_jobs_search/form.html.twig', [
'form' => $form->createView(),
'waitingInfo' => $wantedJobsSearchService->getWaitingInfo($user),
'joboffererProfileCanEditZipcodeForWantedJobsSearch' => $joboffererProfileCanEditZipcodeForWantedJobsSearch,
'joboffererProfileId' => $joboffererProfileId,
'limitedInSearch' => !is_null($user) && !is_null($user->getDefaultJoboffererProfile()) ? $user->getDefaultJoboffererProfile()->isLimitedInSearch() : false
]
);
$response->setVary('Cookie');
$response->setSharedMaxAge(60);
return $response;
}
public function profileSelectionForMultipleMessageAddApiAction(
Request $request,
SessionService $sessionService,
SessionInterface $session
): Response {
if ($request->getMethod() === Request::METHOD_OPTIONS) {
return new Response('', Response::HTTP_NO_CONTENT, ['Allow' => 'OPTIONS, POST']);
}
if (!is_null($request->get('jobseekerProfileId'))) {
$sessionService->addProfileIdForMultiMessageSelection($session, $request->get('jobseekerProfileId'));
}
return new Response('', Response::HTTP_NO_CONTENT);
}
public function profileSelectionForMultipleMessageDeleteApiAction(
Request $request,
SessionService $sessionService,
SessionInterface $session
): Response {
if ($request->getMethod() !== Request::METHOD_DELETE) {
return new Response('', Response::HTTP_NO_CONTENT, ['Allow' => 'DELETE']);
}
if (!is_null($request->get('jobseekerProfileId'))) {
$sessionService->deleteProfileIdForMultiMessageSelection($session, $request->get('jobseekerProfileId'));
}
return new Response('', Response::HTTP_NO_CONTENT);
}
public function profileSelectionForMultipleMessageGetApiAction(
Request $request,
SessionService $sessionService,
SessionInterface $session
): Response {
if ($request->getMethod() === Request::METHOD_OPTIONS) {
return new Response('', Response::HTTP_NO_CONTENT, ['Allow' => 'OPTIONS, POST']);
}
return new JsonResponse($sessionService->getProfileIdsForMultiMessageSelection($session));
}
/**
* @throws \Doctrine\DBAL\Driver\Exception
* @throws \Doctrine\DBAL\Exception
*/
public function resultsAction(
Request $request,
TranslatorInterface $translator,
EntityManagerInterface $em,
SessionService $sessionService,
SessionInterface $session,
DebuggingService $debuggingService,
BusinessEventDomainService $businessEventDomainService,
WantedJobsSearchService $wantedJobsSearchService,
LoggerInterface $logger,
NotificationService $notificationService,
UsageEventService $usageEventService,
EventDispatcherInterface $eventDispatcher,
AnonymousUserInfoService $anonymousUserInfoService,
ZipcodeCircumcirclesRepository $zipcodeCircumcirclesRepository,
ProfileBlocksService $profileBlocksService,
FeatureLimitationsService $featureLimitationsService,
CircuitbreakerService $circuitbreakerService,
BlacklistingService $blacklistingService,
MembershipService $membershipService,
FactoryInterface $formFactory,
RegistrationService $registrationService,
): Response {
$page = $this->getPage($request);
$showAnonymousResults = true;
$conversionEventId = null;
// whenever a new search is done, we forget about previously selected profiles for multi message
if (isset($_GET['searchButton']) && !isset($_POST['searchButton']) && $session->get('uriOfLastSuccessfulSearchResultsPage') !== $request->getRequestUri()) {
$sessionService->resetProfileIdsForMultiMessageSelection($session);
}
$receivingJobseekerProfileIds = $sessionService->getProfileIdsForMultiMessageSelection($session);
/** @var User $user */
$user = $this->getUser();
$userHasMembership = false;
if ($user instanceof User) {
if ($user->isJobofferer()) {
$userHasMembership = $membershipService->userHasActiveMembership($user);
$showAnonymousResults = false;
$notificationService->uncancelNotification($user, NotificationService::NOTIFICATION_TYPE_UNREAD_CONVERSATION_MESSAGES);
$notificationService->uncancelNotification($user, NotificationService::NOTIFICATION_TYPE_UNREAD_JOBRADAR_MATCHES);
if (!is_null($circuitbreakerService->checkForTooManySearchRequestsAndReturnSeverityLevel($user))) {
$this->addFlash(
'danger',
'Bei deinem Account wurden verdächtige Aktivitäten festgestellt und du kannst für ' . $circuitbreakerService->checkForTooManySearchRequestsAndReturnSeverityLevel($user) . ' keine Suchen mehr durchführen. Falls es sich um einen Fehler handeln sollte, melde dich gerne bei info@joboo.de.'
);
return $this->redirectToRoute('homepage');
}
}
if ($user->isJobseeker() && !$user->hasAtLeastOneAdminRole()) {
$this->addFlash(
'info',
$translator->trans('wanted_jobs_search.alert_jobseekers_not_allowed')
);
return $this->redirectToRoute('recurrent_jobs_search.form');
}
if ($blacklistingService->isEmailBlacklistedForType(
$user->getEmail(),
Blacklisting::BLACKLISTING_TYPE_SEARCH
)) {
$this->addFlash(
'danger',
'Bei deinem Account wurden verdächtige Aktivitäten festgestellt und du kannst vorerst keine Suchen mehr durchführen.
Falls es sich um einen Fehler handeln sollte, melde dich gerne bei info@joboo.de.'
);
return $this->redirectToRoute('homepage');
}
}
$form = $this->createForm(
WantedJobsSearchParametersType::class,
null,
[
'action' => $this->generateUrl('wanted_jobs_search.results') . '#results',
'method' => 'GET'
]
);
try {
$form->handleRequest($request);
} catch (Exception $e) {
$logger->warning($e->getMessage(), ['exception' => $e]);
$this->addFlash(
'danger',
$translator->trans('wanted_jobs_search.alert_parameters_not_allowed')
);
return $this->redirectToRoute('wanted_jobs_search.form');
}
if ($form->isSubmitted() && $form->isValid()) {
/** @var WantedJobsSearchParameters $searchParams */
$searchParams = $form->getData();
$enteredSearchtermForTracking = $searchParams->getFilterSearchterm();
if ($user instanceof User && $user->isJobofferer() && $user->getDefaultJoboffererProfile()->isLimitedInSearch()
&& !in_array($searchParams->getFilterZipcode(), $wantedJobsSearchService->getZipcodesBelongingToJoboffererProfile($user->getDefaultJoboffererProfile()))
) {
$this->addFlash(
'danger',
$translator->trans('wanted_jobs_search.alert_parameters_not_allowed')
);
return $this->redirectToRoute('wanted_jobs_search.form');
}
// Our heuristic here is very simple:
// If the request does not contain a query param "page", we assume that the user comes from submitting the
// search form, and not from navigating through the pages. This is what we consider a newly started search.
// This doesn't prevent the event from being dispatched on page reloads, or when repeatedly opening the
// page through other means (like a click on a bookmark, or in an email, etc).
if (is_null($request->get('page'))) {
$eventDispatcher->dispatch(
new SearchForWantedJobsStartedEvent($user, $searchParams, $request, true),
SearchForWantedJobsStartedEvent::class
);
}
if (is_null($user)) {
$anonymousUserInfoService->setSearchInformationWantedJobsSearch($searchParams);
}
$context = is_null($user)
? \App\Entity\SearchtermEnteredEvent::CONTEXT_WANTED_JOBS_SEARCH_ANONYMOUS
: \App\Entity\SearchtermEnteredEvent::CONTEXT_WANTED_JOBS_SEARCH_LOGGED_IN;
$numberOfResultsPerPage = $showAnonymousResults
? GeneralApplicationSettingsValue::WANTED_JOBS_SEARCH_ANONYMOUS_MAX_NUMBER_OF_RESULTS_PER_PAGE
: GeneralApplicationSettingsValue::WANTED_JOBS_SEARCH_NONANONYMOUS_MAX_NUMBER_OF_RESULTS_PER_PAGE;
$maximumTotalNumberOfResults = GeneralApplicationSettingsValue::WANTED_JOBS_SEARCH_MAX_NUMBER_OF_PAGES * $numberOfResultsPerPage;
$fullResultset = new WantedJobsSearchResultset($searchParams);
try {
$fullResultset = $wantedJobsSearchService->getResultset(
$searchParams,
$maximumTotalNumberOfResults,
0,
$showAnonymousResults,
$user,
$maximumTotalNumberOfResults
);
} catch (UnknownZipcodeException $e) {
$debuggingInfoBag = new DebuggingInfoBag(
'unknown-zipcode-wanted-jobs-search',
'Unknown zipcode ' . $searchParams->getFilterZipcode() . ' in wanted jobs search', '',
Logger::WARNING);
$debuggingInfoBag->setRequest($request);
if ($user) {
$debuggingInfoBag->setUser($user);
}
$debuggingService->log($debuggingInfoBag);
} catch (Exception $e) {
$userId = null;
if (!is_null($user)) {
$userId = $user->getId();
}
throw new Exception('An exception occurred while trying to search for wanted jobs: "' . $e->getMessage() . '". The query string is ' . $request->getQueryString() . ', and the userId is ' . print_r($userId, true), 0, $e);
}
$subscriptionUrl = null;
$joboffererSparseResultsTemplate = null;
if ($user instanceof User
&& $user->isJobofferer()
&& $fullResultset->getTotalNumberOfResults() <= 5
) {
if ($userHasMembership) {
$joboffererSparseResultsTemplate = 'jobofferer_modal_subscription.html.twig';
} else {
$businessEventDomainService->writeNewEvent(
BusinessEvent::EVENT_TYPE_SPARSE_RESULTS_AFTER_SEARCH,
$user
);
$subscriptionUrl = $this->generateUrl('account.subscription.jobofferer.index', []) . '?sparseResults';
$joboffererSparseResultsTemplate = 'jobofferer_modal_no_subscription.html.twig';
}
}
if ($fullResultset->getTotalNumberOfResults() > 0) { // We want to be able to return to this search result later
$session->set('uriOfLastSuccessfulSearchResultsPage', $request->getRequestUri());
}
$eventDispatcher->dispatch(
new SearchtermEnteredEvent($request, $user, $enteredSearchtermForTracking, null, null, $context, $fullResultset->getTotalNumberOfResults(), $fullResultset->getBlocksInfo()->getNumberOfResultsPerBlock()),
SearchtermEnteredEvent::class
);
if (!$showAnonymousResults) {
$usageEventService->eventHasOccurredForUser($user, UsageEvent::EVENT_TYPE_JOBOFFERER_HAS_USED_SEARCH_FOR_WANTED_JOBS);
if ($fullResultset->getTotalNumberOfResults() < 5) {
$businessEventDomainService->writeNewEvent(
BusinessEvent::EVENT_TYPE_WANTEDJOBSSEARCH_NOTENOUGHRESULTS,
$user,
null,
null,
json_encode([
'searchParams' => $searchParams,
'requestUri' => $request->getRequestUri()
])
);
} else {
$businessEventDomainService->writeNewEvent(
BusinessEvent::EVENT_TYPE_WANTEDJOBSSEARCH_ENOUGHRESULTS,
$user,
null,
null,
json_encode([
'searchParams' => $searchParams,
'requestUri' => $request->getRequestUri()
])
);
}
}
$fullResultset->sliceResults(0, $maximumTotalNumberOfResults);
try {
$resultsetForCurrentPage = $fullResultset->getResultsetForPage($fullResultset, $page, $numberOfResultsPerPage);
} catch (UncallableSearchPageException $e) {
return $this->render('/errors/notfound.html.twig', [], new Response(null, Response::HTTP_NOT_FOUND));
}
if ($showAnonymousResults) {
$newConversationMessageSubjectPrefill = '';
$newConversationMessageBodyPrefill = '';
$registrationCarryThroughData = new CarryThroughData();
$registrationCarryThroughData->setWantedJobsSearchParameters($searchParams);
} else {
$newConversationMessagePrefillOccupationalField = $searchParams->getFilterSearchtermForDisplay();
$newConversationMessageBodyPrefillAvailabilities = '';
foreach (PossibleAvailabilitiesValue::WEEKDAYS as $weekday) {
foreach (PossibleAvailabilitiesValue::TIMES_OF_DAY as $timeOfDay) {
$methodName = 'getFilterIsRequiredOn' . $weekday . $timeOfDay;
if ($searchParams->$methodName() === true) {
$newConversationMessageBodyPrefillAvailabilities .= '- ' .
$translator->trans('availabilities_weekday.' . $weekday) .
' ' .
$translator->trans('availabilities_time_of_day.' . $timeOfDay) .
"\n";
}
}
}
}
// Build an array of all jobseekers we already have contacted with a message in the past,
// in order to show this information on the results page
$conversationMessagesSentDatesForJobseekerProfiles = [];
if (!$showAnonymousResults) {
$jobseekerProfiles = [];
/** @var WantedJobsSearchResult $result */
foreach ($resultsetForCurrentPage->getResults() as $key => $result) {
/** @var WantedJob $wantedJob */
$wantedJob = $result->getWantedJob();
// Also, don't show results with a wantedJob by a user that we have blocked
if ($profileBlocksService->isProfileOrCustomerBlockedByProfile($user->getDefaultJoboffererProfile(), $wantedJob->getJobseekerProfile())) {
$resultsetForCurrentPage->removeResult($key);
} else {
if (in_array($wantedJob->getJobseekerProfile(), $jobseekerProfiles)) {
$resultsetForCurrentPage->removeResult($key);
} else {
$jobseekerProfiles[] = $wantedJob->getJobseekerProfile();
}
}
}
try {
$conversationMessageRepository = $em->getRepository(ConversationMessage::class);
$conversationMessagesSentDatesForJobseekerProfiles = $conversationMessageRepository->getLastSentDatesForReceiverProfiles(
$user->getDefaultJoboffererProfile(),
$jobseekerProfiles,
true
);
} catch (Exception $e) {
$logger->warning('Could not get last sent dates for jobseeker profiles of wanted jobs in search results', ['exception' => $e]);
}
}
if ($showAnonymousResults) {
$resultsArray = (array)$resultsetForCurrentPage->getResults();
$alreadyPortrayedJobseekers = [];
foreach ($resultsArray as $key => $result) {
if (in_array($result->getWantedJob()->getJobseekerProfile()->getId(), $alreadyPortrayedJobseekers)) {
unset($resultsArray[$key]);
} else {
$alreadyPortrayedJobseekers[] = $result->getWantedJob()->getJobseekerProfile()->getId();
}
}
shuffle($resultsArray);
$resultsetForCurrentPage->resetResults();
$resultsetForCurrentPage->setResults($resultsArray);
}
$joboffererProfileCanEditZipcodeForWantedJobsSearch = true;
$joboffererProfileId = null;
if (!is_null($user) && !is_null($user->getDefaultJoboffererProfile())) {
$joboffererProfileCanEditZipcodeForWantedJobsSearch = $featureLimitationsService->joboffererProfileCanEditZipcodeForWantedJobsSearch($user->getDefaultJoboffererProfile());
$joboffererProfileId = $user->getDefaultJoboffererProfile()->getId();
}
$response = $this->render('/wanted_jobs_search/results.html.twig', [
'jobofferer_template' => $joboffererSparseResultsTemplate,
'subscription_url' => $subscriptionUrl,
'showAnonymousResults' => $showAnonymousResults,
'form' => $form->createView(),
'resultsetForCurrentPage' => $resultsetForCurrentPage,
'totalNumberOfResults' => $fullResultset->getTotalNumberOfResults(),
'currentPage' => $page,
'searchParams' => $searchParams,
'seoTranslationParameters' => new SeoTranslationParameters(
$searchParams->getFilterSearchterm(),
$searchParams->getFilterZipcode(),
(string)$zipcodeCircumcirclesRepository->getCityForZipcode($searchParams->getFilterZipcode())
),
'numberOfResultsPerPage' => $numberOfResultsPerPage,
'conversationMessagesSentDatesForJobseekerProfiles' => $conversationMessagesSentDatesForJobseekerProfiles,
'maxNumberOfPages' => GeneralApplicationSettingsValue::WANTED_JOBS_SEARCH_MAX_NUMBER_OF_PAGES,
'receivingJobseekerProfileIds' => $receivingJobseekerProfileIds,
'numberOfReceivingJobseekerProfileIds' => sizeof($receivingJobseekerProfileIds),
'conversionEventId' => !is_null($user) && !$user->hasAtLeastOneAdminRole() ? $conversionEventId : null,
'waitingInfo' => $wantedJobsSearchService->getWaitingInfo($user),
'joboffererProfileCanEditZipcodeForWantedJobsSearch' => $joboffererProfileCanEditZipcodeForWantedJobsSearch,
'joboffererProfileId' => $joboffererProfileId,
'limitedInSearch' => !is_null($user) && !is_null($user->getDefaultJoboffererProfile()) ? $user->getDefaultJoboffererProfile()->isLimitedInSearch() : false]
);
if ($showAnonymousResults) { // Don't cache for logged-in users. If a user marks a result as "favorite", this would not be reflected upon page reload
$response->setVary('Cookie');
$response->setSharedMaxAge(60);
}
return $response;
} else {
return $this->render('/wanted_jobs_search/form.html.twig', [
'form' => $form->createView(),
'waitingInfo' => $wantedJobsSearchService->getWaitingInfo($user),
]
);
}
}
public function landingpageAction(
Request $request
): RedirectResponse {
return $this->redirectToRoute('homepage', [
'request' => $request
], 307);
}
/**
* @ParamConverter("recurrentJob", class="App\Entity\RecurrentJob", options={"id" = "recurrentJobId"}))
*/
public function specificRecurrentJobAction(
RecurrentJob $recurrentJob,
Request $request
): RedirectResponse {
$wantedJobsSearchParameters = WantedJobsSearchParameters::fromRecurrentJob($recurrentJob);
return $this->redirectToRoute('wanted_jobs_search.results',
[
'wanted_jobs_search_parameters' => $wantedJobsSearchParameters->asArray(),
]
);
}
}