<?php
declare(strict_types=1);
namespace ApiBundle\Controller;
use AppBundle\MobileJobOffer\DTO\MobileJobOfferDetailsDTO;
use AppBundle\MobileJobOffer\Request\MobileJobOfferSearchRequest;
use AppBundle\MobileJobOffer\Service\InterestSubmissionService;
use AppBundle\MobileJobOffer\Service\MobileJobOfferSearchService;
use AppBundle\Repository\MobileJobOfferRepository;
use Exception;
use OpenApi\Attributes as OA;
use Pimcore\Model\DataObject\MobileJobOffer;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class JobOfferController extends ApiController
{
private MobileJobOfferSearchService $mobileJobOfferSearchService;
private MobileJobOfferRepository $mobileJobOfferRepository;
private InterestSubmissionService $interestSubmissionService;
public function __construct(MobileJobOfferSearchService $mobileJobOfferSearchService, MobileJobOfferRepository $mobileJobOfferRepository, InterestSubmissionService $interestSubmissionService, string $apiKey)
{
$this->mobileJobOfferSearchService = $mobileJobOfferSearchService;
$this->mobileJobOfferRepository = $mobileJobOfferRepository;
$this->interestSubmissionService = $interestSubmissionService;
parent::__construct($apiKey);
}
#[Route('/api/job-offers', name: 'api_job_offers_list', methods: ['GET'])]
#[OA\Get(path: '/api/job-offers', description: 'Zwraca listę ofert pracy z możliwością filtrowania po mieście, stanowisku, rodzaju umowy, lokalizacji GPS, dacie oraz stronnicowaniem.', summary: 'Lista ofert pracy z filtrami i paginacją', tags: ['Job Offers'])]
#[OA\Parameter(name: 'X-API-KEY', description: 'Klucz API wymagany do autoryzacji', in: 'header', required: true, schema: new OA\Schema(type: 'string', example: 'your-api-key'))]
#[OA\Parameter(name: 'cityIds', description: 'Lista ID miast, oddzielone przecinkami (np. 1,2,3)', in: 'query', required: false, schema: new OA\Schema(type: 'string', example: '1,2,3'))]
#[OA\Parameter(name: 'positionIds', description: 'Lista ID stanowisk, oddzielone przecinkami (np. 1,2,3)', in: 'query', required: false, schema: new OA\Schema(type: 'string', example: '1,2,5'))]
#[OA\Parameter(name: 'contractTypeIds', description: 'Lista ID rodzajów umów, oddzielone przecinkami (np. 1,3)', in: 'query', required: false, schema: new OA\Schema(type: 'string', example: '1,3'))]
#[OA\Parameter(name: 'gpsPoints', description: "Tablica punktów GPS w formacie 'lat,lon'. Użyj gpsPoints[]=52.2297,21.0122&gpsPoints[]=50.06143,19.93658", in: 'query', required: false, schema: new OA\Schema(
type: 'array',
items: new OA\Items(description: 'Format: latitude,longitude', type: 'string', example: '52.2297,21.0122'),
), example: ['52.2297,21.0122', '50.06143,19.93658'], style: 'form', explode: true)]
#[OA\Parameter(name: 'gpsRadius', description: 'Promień w kilometrach wokół punktu GPS do filtrowania ofert', in: 'query', required: false, schema: new OA\Schema(type: 'number', format: 'float', example: 10))]
#[OA\Parameter(name: 'dateFrom', description: 'Początkowa data zakresu w formacie YYYY-MM-DD', in: 'query', required: false, schema: new OA\Schema(type: 'string', format: 'date', example: '2025-09-01'))]
#[OA\Parameter(name: 'dateTo', description: 'Końcowa data zakresu w formacie YYYY-MM-DD', in: 'query', required: false, schema: new OA\Schema(type: 'string', format: 'date', example: '2025-09-30'))]
#[OA\Parameter(name: 'limit', description: 'Ilość ofert do zwrócenia na stronie (stronnicowanie)', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 20, example: 20))]
#[OA\Parameter(name: 'offset', description: 'Przesunięcie wyników (ilość ofert do pominięcia) dla stronnicowania', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 0, example: 0))]
#[OA\Response(
response: 200,
description: 'Lista ofert pracy',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'success', type: 'boolean', example: true),
new OA\Property(
property: 'data',
properties: [
new OA\Property(property: 'hasMore', description: 'Czy są kolejne strony wyników', type: 'boolean', example: true),
new OA\Property(property: 'totalCount', description: 'Całkowita liczba ofert spełniających kryteria', type: 'integer', example: 42),
new OA\Property(
property: 'offers',
type: 'array',
items: new OA\Items(
properties: [
new OA\Property(property: 'id', type: 'integer', example: 7423),
new OA\Property(property: 'nameFirstLine', type: 'string', example: 'Magisterka farmacji'),
new OA\Property(property: 'nameSecondLine', type: 'string', example: 'Magister farmacji'),
new OA\Property(property: 'cityId', type: 'integer', example: 1),
new OA\Property(property: 'cityName', type: 'string', example: 'Kraków'),
new OA\Property(property: 'hourlyRateSummary', type: 'string', example: '25-30 zł/h'),
new OA\Property(property: 'publicationDate', type: 'string', format: 'date-time', example: '2025-09-10T12:00:00Z'),
],
type: 'object',
),
),
],
type: 'object',
),
],
type: 'object',
),
)]
public function list(Request $request): JsonResponse
{
if (!$this->validateApiKey($request)) {
return $this->returnInvalidApiKeyError();
}
try {
$searchRequest = MobileJobOfferSearchRequest::fromRequest($request);
$offer = $this->mobileJobOfferSearchService->searchOffer($searchRequest);
return new JsonResponse([
'success' => true,
'data' => $offer,
]);
} catch (Exception $e) {
return new JsonResponse([
'success' => false,
'error' => $e->getMessage(),
], Response::HTTP_BAD_REQUEST);
}
}
#[Route('/api/job-offers/{id}', name: 'api_job_offers_details', methods: ['GET'])]
#[OA\Get(path: '/api/job-offers/{id}', description: 'Endpoint zwraca szczegółowe informacje o konkretnej ofercie pracy na podstawie jej ID.', summary: 'Szczegóły oferty pracy', tags: ['Job Offers'])]
#[OA\Parameter(name: 'X-API-KEY', description: 'Klucz API wymagany do autoryzacji', in: 'header', required: true, schema: new OA\Schema(type: 'string', example: 'your-api-key'))]
#[OA\Parameter(name: 'id', description: 'ID oferty pracy', in: 'path', required: true, schema: new OA\Schema(type: 'integer', example: 7423))]
#[OA\Response(
response: 200,
description: 'Szczegóły oferty pracy',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'success', type: 'boolean', example: true),
new OA\Property(
property: 'data',
properties: [
new OA\Property(property: 'id', type: 'integer', example: 7423),
new OA\Property(property: 'nameFirstLine', type: 'string', example: 'Magisterka farmacji'),
new OA\Property(property: 'nameSecondLine', type: 'string', example: 'Magister farmacji'),
new OA\Property(property: 'cityId', type: 'integer', example: 1),
new OA\Property(property: 'cityName', type: 'string', example: 'Kraków'),
new OA\Property(property: 'hourlyRateSummary', type: 'string', example: '25-30 zł/h'),
new OA\Property(
property: 'gpsPoint',
properties: [
new OA\Property(property: 'lat', type: 'number', format: 'float', example: 50.0514238),
new OA\Property(property: 'lng', type: 'number', format: 'float', example: 19.9415525),
],
type: 'object',
),
new OA\Property(
property: 'sections',
description: 'Sekcje opisu oferty, każda z tytułem i treścią',
type: 'array',
items: new OA\Items(
properties: [
new OA\Property(property: 'title', type: 'string', example: 'Opis stanowiska'),
new OA\Property(property: 'descriptions', type: 'string', example: 'Opis szczegółowy stanowiska pracy...'),
],
type: 'object',
),
),
],
type: 'object',
),
],
type: 'object',
),
)]
public function details(Request $request, int $id): JsonResponse
{
if (!$this->validateApiKey($request)) {
return $this->returnInvalidApiKeyError();
}
$mobile = $this->mobileJobOfferRepository->find($id);
if (!$mobile instanceof MobileJobOffer) {
return new JsonResponse([
'success' => false,
'message' => 'Offer not found',
], Response::HTTP_NOT_FOUND);
}
$data = MobileJobOfferDetailsDTO::createFromMobileJobOffer($mobile);
return new JsonResponse([
'success' => true,
'data' => $data,
]);
}
#[Route('/api/job-offers/interest', name: 'api_job_offers_interest', methods: ['POST'])]
#[OA\Post(path: '/api/job-offers/interest', description: 'Przyjmuje zgłoszenie zainteresowania ofertą pracy z wybranymi terminami oraz danymi kontaktowymi użytkownika.', summary: 'Zgłoś zainteresowanie ofertą pracy na wybrane terminy', tags: ['Job Offers'])]
#[OA\Parameter(name: 'X-API-KEY', description: 'Klucz API wymagany do autoryzacji', in: 'header', required: true, schema: new OA\Schema(type: 'string', example: 'your-api-key'))]
#[OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: ['offerId', 'selectedDateIds', 'phone'],
properties: [
new OA\Property(property: 'offerId', description: 'ID oferty pracy', type: 'integer', example: 7423, nullable: false),
new OA\Property(
property: 'selectedDateIds',
description: 'Tablica ID terminów, którymi użytkownik jest zainteresowany',
type: 'array',
items: new OA\Items(type: 'integer'),
minItems: 1,
example: [0, 2],
),
new OA\Property(property: 'phone', description: 'Numer telefonu kontaktowego', type: 'string', example: '+48123456789', nullable: false),
new OA\Property(property: 'futureContactConsent', description: 'Zgoda na przyszły kontakt', type: 'boolean', example: true, nullable: false),
],
type: 'object',
),
)]
#[OA\Response(
response: 200,
description: 'Potwierdzenie przyjęcia zgłoszenia zainteresowania',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'success', type: 'boolean', example: true),
new OA\Property(property: 'message', type: 'string', example: 'The submission has been accepted.'),
],
type: 'object',
),
)]
#[OA\Response(
response: 404,
description: 'Błąd walidacji danych wejściowych',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'success', type: 'boolean', example: false),
new OA\Property(property: 'message', type: 'string', example: 'Selected offer does not exist.'),
],
type: 'object',
),
)]
public function submitInterest(Request $request): JsonResponse
{
if (!$this->validateApiKey($request)) {
return $this->returnInvalidApiKeyError();
}
$data = json_decode($request->getContent(), true);
$offer = $this->mobileJobOfferSearchService->offerExists($data['offerId']);
if (!$offer) {
return new JsonResponse([
'success' => false,
'message' => 'Selected offer does not exist.',
], Response::HTTP_NOT_FOUND);
}
if (isset($data['selectedDateIds']) && !$this->mobileJobOfferSearchService->datesExist($data['offerId'], $data['selectedDateIds'])) {
return new JsonResponse([
'success' => false,
'message' => 'At least one of the selected dates does not exist.',
], Response::HTTP_NOT_FOUND);
}
$this->interestSubmissionService->submitInterest($data);
return new JsonResponse([
'success' => true,
'message' => 'The submission has been accepted.',
]);
}
}